feat: support both Trusted Publishing and Granular Access Tokens #99
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [main, develop] | |
| tags: | |
| - "v*" | |
| pull_request: | |
| branches: [main, develop] | |
| jobs: | |
| lint: | |
| name: Lint Code | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| cache: "npm" | |
| cache-dependency-path: "**/package-lock.json" | |
| - name: Install CLI dependencies | |
| working-directory: ./cli | |
| run: npm ci | |
| - name: Run ESLint (if configured) | |
| working-directory: ./cli | |
| run: npm run lint || echo "No lint script configured" | |
| continue-on-error: true | |
| test-cli: | |
| name: Test CLI | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node-version: [18, 20] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: "npm" | |
| cache-dependency-path: "cli/package-lock.json" | |
| - name: Install dependencies | |
| working-directory: ./cli | |
| run: npm ci | |
| - name: Build TypeScript (required for E2E tests) | |
| working-directory: ./cli | |
| run: npm run build | |
| - name: Run tests with coverage | |
| working-directory: ./cli | |
| run: npm test -- --coverage --coverageThreshold='{}' | |
| # Remove --coverageThreshold='{}' once coverage meets 70% threshold | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v3 | |
| with: | |
| files: ./cli/coverage/lcov.info | |
| flags: cli | |
| name: cli-coverage | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| if: matrix.node-version == 18 && always() | |
| continue-on-error: true | |
| build-cli: | |
| name: Build CLI | |
| runs-on: ubuntu-latest | |
| needs: [test-cli] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| cache: "npm" | |
| cache-dependency-path: "cli/package-lock.json" | |
| - name: Install dependencies | |
| working-directory: ./cli | |
| run: npm ci | |
| - name: Build TypeScript | |
| working-directory: ./cli | |
| run: npm run build | |
| - name: Verify build artifacts | |
| working-directory: ./cli | |
| run: | | |
| ls -la dist/ | |
| test -f dist/index.js | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cli-dist | |
| path: cli/dist/ | |
| test-backend: | |
| name: Test Backend | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| cache: "npm" | |
| cache-dependency-path: "backend/package-lock.json" | |
| - name: Check if backend exists | |
| run: test -d backend || (echo "Backend directory not found, skipping" && exit 0) | |
| - name: Install dependencies | |
| working-directory: ./backend | |
| run: npm ci | |
| continue-on-error: true | |
| - name: Run backend tests (when available) | |
| working-directory: ./backend | |
| run: npm test || echo "No tests configured yet" | |
| continue-on-error: true | |
| build-backend: | |
| name: Build Backend | |
| runs-on: ubuntu-latest | |
| needs: [test-backend] | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| cache: "npm" | |
| cache-dependency-path: "backend/package-lock.json" | |
| - name: Check if backend exists | |
| run: test -d backend || (echo "Backend directory not found, skipping" && exit 0) | |
| - name: Install dependencies | |
| working-directory: ./backend | |
| run: npm ci | |
| continue-on-error: true | |
| - name: Validate wrangler.toml exists | |
| working-directory: ./backend | |
| run: | | |
| test -f wrangler.toml || echo "Warning: wrangler.toml not found" | |
| continue-on-error: true | |
| security-scan: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run npm audit (CLI) | |
| working-directory: ./cli | |
| run: npm audit --audit-level=moderate || true | |
| continue-on-error: true | |
| - name: Run npm audit (Backend) | |
| working-directory: ./backend | |
| run: npm audit --audit-level=moderate || true | |
| continue-on-error: true | |
| integration-test: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [build-cli] | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| - name: Install CLI dependencies | |
| working-directory: ./cli | |
| run: npm ci | |
| - name: Build CLI | |
| working-directory: ./cli | |
| run: npm run build | |
| - name: Link CLI globally | |
| working-directory: ./cli | |
| run: npm link | |
| continue-on-error: true | |
| - name: Test CLI commands | |
| run: | | |
| guardscan --version || echo "guardscan --version failed" | |
| guardscan --help || echo "guardscan --help failed" | |
| guardscan init --no-telemetry || echo "guardscan init failed" | |
| continue-on-error: true | |
| - name: Run self-scan | |
| run: | | |
| cd cli | |
| guardscan security --files "src/**/*.ts" --no-telemetry || echo "Self-scan failed" | |
| continue-on-error: true | |
| publish-npm: | |
| name: Publish to NPM | |
| runs-on: ubuntu-latest | |
| needs: [build-cli, integration-test] | |
| if: | | |
| github.event_name == 'push' && | |
| (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && | |
| needs.build-cli.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install dependencies | |
| working-directory: ./cli | |
| run: npm ci | |
| - name: Build | |
| working-directory: ./cli | |
| run: npm run build | |
| - name: Extract version from package.json | |
| id: version | |
| working-directory: ./cli | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "tag=v$VERSION" >> $GITHUB_OUTPUT | |
| echo "Package version: $VERSION" | |
| - name: Verify tag matches package version (if tag push) | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| working-directory: ./cli | |
| run: | | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| TAG_VERSION=${GITHUB_REF#refs/tags/v} | |
| if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then | |
| echo "Error: Tag version ($TAG_VERSION) doesn't match package version ($PACKAGE_VERSION)" | |
| exit 1 | |
| fi | |
| echo "✓ Tag version matches package version: $PACKAGE_VERSION" | |
| - name: Publish to NPM (dry-run) | |
| if: github.ref == 'refs/heads/main' | |
| working-directory: ./cli | |
| run: npm publish --dry-run | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Verify NPM authentication and permissions | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| id: verify_npm | |
| working-directory: ./cli | |
| run: | | |
| PACKAGE_NAME=$(node -p "require('./package.json').name") | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| # Check if using Trusted Publishing (no token needed) or Granular Access Token | |
| if [ -n "${{ secrets.NPM_TOKEN }}" ]; then | |
| echo "Using NPM_TOKEN (Granular Access Token method)" | |
| # Verify authentication with token | |
| echo "Verifying NPM authentication..." | |
| NPM_USER=$(npm whoami --registry=https://registry.npmjs.org 2>&1) || { | |
| echo "Error: NPM authentication failed" | |
| echo "Please verify your NPM_TOKEN is a valid Granular Access Token with 'Publish' permissions" | |
| echo "Create one at: https://www.npmjs.com/settings/[YOUR_USERNAME]/tokens" | |
| exit 1 | |
| } | |
| echo "✓ Authenticated as: $NPM_USER" | |
| else | |
| echo "No NPM_TOKEN found - using Trusted Publishing (if configured)" | |
| echo "ℹ If Trusted Publishing is not set up, the publish step will fail" | |
| echo "Set up Trusted Publishing at: https://www.npmjs.com/settings/[YOUR_USERNAME]/publishing" | |
| fi | |
| echo "Checking package: $PACKAGE_NAME" | |
| # Try to check if package exists | |
| if npm view "$PACKAGE_NAME" version > /dev/null 2>&1; then | |
| echo "✓ Package exists on NPM" | |
| else | |
| echo "ℹ Package doesn't exist yet, will be created on first publish" | |
| fi | |
| # Check if version already exists | |
| if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version > /dev/null 2>&1; then | |
| echo "Error: Version $PACKAGE_VERSION already exists on NPM" | |
| echo "Please bump the version in package.json before publishing" | |
| exit 1 | |
| fi | |
| echo "✓ Pre-publish checks completed" | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish to NPM | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| working-directory: ./cli | |
| run: npm publish | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| deploy-backend: | |
| name: Deploy Backend to Cloudflare | |
| runs-on: ubuntu-latest | |
| needs: [build-backend] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.build-backend.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| - name: Install dependencies | |
| working-directory: ./backend | |
| run: npm ci | |
| - name: Deploy to Cloudflare Workers (staging) | |
| working-directory: ./backend | |
| run: echo "Deployment would happen here with wrangler deploy" | |
| # Uncomment when ready: | |
| # run: npx wrangler deploy --env staging | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| # Note: CLOUDFLARE_API_TOKEN secret warning is expected if secret is not configured | |
| # Production deployment (manual approval recommended) | |
| # - name: Deploy to production | |
| # working-directory: ./backend | |
| # run: npx wrangler deploy --env production | |
| # env: | |
| # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| create-release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [publish-npm] | |
| if: | | |
| github.event_name == 'push' && | |
| startsWith(github.ref, 'refs/tags/v') && | |
| needs.publish-npm.result == 'success' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "18" | |
| - name: Extract version from tag | |
| id: tag | |
| run: | | |
| TAG=${GITHUB_REF#refs/tags/} | |
| VERSION=${TAG#v} | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Tag: $TAG" | |
| echo "Version: $VERSION" | |
| - name: Verify package is published on NPM | |
| run: | | |
| PACKAGE_NAME="guardscan" | |
| PACKAGE_VERSION="${{ steps.tag.outputs.version }}" | |
| echo "Checking ${PACKAGE_NAME}@${PACKAGE_VERSION} on NPM..." | |
| # Give NPM registry a moment to reflect the new version | |
| sleep 10 | |
| if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then | |
| echo "✓ Package ${PACKAGE_NAME}@${PACKAGE_VERSION} is published on NPM" | |
| else | |
| echo "Error: Package ${PACKAGE_NAME}@${PACKAGE_VERSION} not found on NPM" | |
| echo "Release will not be created until package is published" | |
| exit 1 | |
| fi | |
| - name: Extract changelog entry | |
| id: changelog | |
| working-directory: ./cli | |
| run: | | |
| VERSION="${{ steps.tag.outputs.version }}" | |
| CHANGELOG_FILE="CHANGELOG.md" | |
| if [ ! -f "$CHANGELOG_FILE" ]; then | |
| echo "Error: $CHANGELOG_FILE not found" | |
| exit 1 | |
| fi | |
| # Extract the changelog section for this version | |
| # Look for pattern: ## [VERSION] - DATE | |
| # Extract until next version section or end of file | |
| awk -v version="$VERSION" ' | |
| BEGIN { in_section = 0; found = 0 } | |
| /^## \[/ { | |
| if (in_section) exit | |
| if ($0 ~ "\\[" version "\\]") { | |
| in_section = 1 | |
| found = 1 | |
| next | |
| } | |
| } | |
| in_section { | |
| if (/^## \[/) exit | |
| } | |
| END { | |
| if (!found) { | |
| print "## Changelog entry not found for version " version | |
| print "" | |
| print "Please ensure CHANGELOG.md contains an entry for version " version | |
| } | |
| } | |
| ' "$CHANGELOG_FILE" > /tmp/changelog_entry.txt | |
| CHANGELOG_CONTENT=$(cat /tmp/changelog_entry.txt) | |
| if [ -z "$CHANGELOG_CONTENT" ] || echo "$CHANGELOG_CONTENT" | grep -q "Changelog entry not found"; then | |
| CHANGELOG_CONTENT="## GuardScan ${{ steps.tag.outputs.tag }} | |
| Release notes for version ${{ steps.tag.outputs.version }}. | |
| See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ github.ref }}/cli/CHANGELOG.md) for details." | |
| fi | |
| { | |
| echo "content<<EOF" | |
| echo "$CHANGELOG_CONTENT" | |
| echo "EOF" | |
| } >> $GITHUB_OUTPUT | |
| echo "Extracted changelog entry:" | |
| cat /tmp/changelog_entry.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: GuardScan ${{ steps.tag.outputs.tag }} | |
| body: ${{ steps.changelog.outputs.content }} | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |