also looking at this
Gate releases on shipped artifact findings
#316
11 files
+454
-25
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -0,0 +1,35 @@ | ||
| 1 | name: Set up release scanners | |
| 2 | description: Install pinned Syft and Grype binaries for release artifact checks | |
| 3 | runs: | |
| 4 | using: composite | |
| 5 | steps: | |
| 6 | - name: Install Syft and Grype | |
| 7 | shell: bash | |
| 8 | run: | | |
| 9 | set -euo pipefail | |
| 10 | case "$RUNNER_OS" in | |
| 11 | Linux) | |
| 12 | syft_platform=linux_amd64 | |
| 13 | grype_platform=linux_amd64 | |
| 14 | syft_hash=caeedb81fb0491615f1ebd1761e4145d41ee86dd2cc7bf80669f9f5ad9d6133d | |
| 15 | grype_hash=3fa2dc4b924621ab65404cf08d0b8438d896d80ab949c9d5a4ca283c36004c9b | |
| 16 | ;; | |
| 17 | macOS) | |
| 18 | syft_platform=darwin_arm64 | |
| 19 | grype_platform=darwin_arm64 | |
| 20 | syft_hash=014d561b6d13059124155f74a6c5a9a99501f5e209313638dd884f39eb418ee6 | |
| 21 | grype_hash=500c9b2b6c089d21481815f57a553fabbd441ec7d1e79d95e3aaf40c3bfc7e36 | |
| 22 | ;; | |
| 23 | *) echo "Unsupported scanner platform: $RUNNER_OS" >&2; exit 1 ;; | |
| 24 | esac | |
| 25 | mkdir -p "$RUNNER_TEMP/release-scanners" | |
| 26 | cd "$RUNNER_TEMP/release-scanners" | |
| 27 | curl --fail --location --retry 3 --output syft.tar.gz \ | |
| 28 | "https://github.com/anchore/syft/releases/download/v1.52.0/syft_1.52.0_${syft_platform}.tar.gz" | |
| 29 | echo "$syft_hash syft.tar.gz" | shasum -a 256 --check | |
| 30 | tar -xzf syft.tar.gz syft | |
| 31 | curl --fail --location --retry 3 --output grype.tar.gz \ | |
| 32 | "https://github.com/anchore/grype/releases/download/v0.119.0/grype_0.119.0_${grype_platform}.tar.gz" | |
| 33 | echo "$grype_hash grype.tar.gz" | shasum -a 256 --check | |
| 34 | tar -xzf grype.tar.gz grype | |
| 35 | echo "$RUNNER_TEMP/release-scanners" >> "$GITHUB_PATH" | |
| @@ -278,7 +278,7 @@ jobs: | ||
| 278 | 278 | run: zig fmt --check src/ build.zig |
| 279 | 279 | |
| 280 | 280 | - name: Test failure-safe vulnerability scanning |
| 281 | run: bun test scripts/scan-sbom.test.ts | |
| 281 | run: bun test scripts/scan-sbom.test.ts scripts/scan-release-artifacts.test.ts | |
| 282 | 282 | |
| 283 | 283 | - name: Audit Zig C imports |
| 284 | 284 | run: bun run verify:cimports |
| @@ -26,7 +26,9 @@ jobs: | ||
| 26 | 26 | needs: validate-release |
| 27 | 27 | uses: ./.github/workflows/sbom.yml |
| 28 | 28 | with: |
| 29 | enforce_high: true | |
| 29 | # Keep the full-source report for review. The High gate runs against | |
| 30 | # the artifacts and production dependencies prepared for publication. | |
| 31 | enforce_high: false | |
| 30 | 32 | |
| 31 | 33 | pantry: |
| 32 | 34 | # The reusable SBOM workflow includes the vulnerability scan. Publishing |
| @@ -305,6 +307,13 @@ jobs: | ||
| 305 | 307 | --timeout 30m |
| 306 | 308 | codesign --verify --deep --strict "$binary" |
| 307 | 309 | rm "$archive" |
| 310 | ||
| 311 | - uses: ./.github/actions/setup-release-scanners | |
| 312 | ||
| 313 | - name: Scan binaries prepared for publication | |
| 314 | env: | |
| 315 | CRAFT_PLATFORM: ${{ runner.os == 'macOS' && 'macos' || 'linux' }} | |
| 316 | run: bun scripts/scan-release-artifacts.ts "$CRAFT_PLATFORM" packages/zig/zig-out "$RUNNER_TEMP/native-release-scan" | |
| 308 | 317 | done |
| 309 | 318 | |
| 310 | 319 | # ── Publish & Release ────────────────────────────── |
| @@ -535,16 +544,29 @@ jobs: | ||
| 535 | 544 | |
| 536 | 545 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 537 | 546 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 |
| 547 | - uses: actions/setup-node@v6 | |
| 548 | with: | |
| 549 | node-version: '24' | |
| 550 | registry-url: https://registry.npmjs.org | |
| 551 | package-manager-cache: false | |
| 538 | 552 | - name: Install Dependencies |
| 539 | 553 | run: bun install --frozen-lockfile |
| 540 | 554 | |
| 555 | - name: Install npm CLI with trusted publishing support | |
| 556 | run: npm install --global npm@11.15.0 | |
| 557 | ||
| 558 | - uses: ./.github/actions/setup-release-scanners | |
| 559 | ||
| 541 | 560 | - name: Test package regressions |
| 542 | 561 | run: bun run test:packages |
| 543 | 562 | |
| 544 | 563 | - name: Build and verify every npm artifact |
| 545 | run: bun run verify:npm | |
| 564 | run: bun scripts/npm-packages.ts --archive-dir "$RUNNER_TEMP/craft-npm-archives" | |
| 565 | ||
| 566 | - name: Scan packed npm artifacts and production dependencies | |
| 567 | run: bun scripts/scan-release-artifacts.ts npm "$RUNNER_TEMP/craft-npm-archives" "$RUNNER_TEMP/npm-release-scan" | |
| 546 | 568 | |
| 547 | - name: Publish to npm | |
| 548 | run: pantry publish --npm --access public | |
| 569 | - name: Publish scanned npm archives | |
| 570 | run: bun scripts/publish-npm-archives.ts "$RUNNER_TEMP/craft-npm-archives" | |
| 549 | 571 | env: |
| 550 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| 572 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| @@ -34,16 +34,26 @@ test('registry indexing waits for the final manifest, macOS downloads and attach | ||
| 34 | 34 | |
| 35 | 35 | test('npm publication follows artifact validation, and macOS downloads run on both matching architectures', () => { |
| 36 | 36 | const npm = release.jobs.npm.steps!.map(step => step.run ?? '') |
| 37 | const verify = npm.indexOf('bun run verify:npm') | |
| 37 | const verify = npm.indexOf('bun scripts/npm-packages.ts --archive-dir "$RUNNER_TEMP/craft-npm-archives"') | |
| 38 | const scan = npm.indexOf('bun scripts/scan-release-artifacts.ts npm "$RUNNER_TEMP/craft-npm-archives" "$RUNNER_TEMP/npm-release-scan"') | |
| 39 | const publish = npm.indexOf('bun scripts/publish-npm-archives.ts "$RUNNER_TEMP/craft-npm-archives"') | |
| 38 | 40 | expect(verify).toBeGreaterThan(-1) |
| 39 | expect(npm.indexOf('pantry publish --npm --access public')).toBeGreaterThan(verify) | |
| 41 | expect(scan).toBeGreaterThan(verify) | |
| 42 | expect(publish).toBeGreaterThan(scan) | |
| 43 | expect(steps(release.jobs.npm)).not.toContain('pantry publish --npm') | |
| 44 | expect(release.jobs.npm.steps!.find(step => step.name === 'Publish scanned npm archives')?.env?.NODE_AUTH_TOKEN).toBe('$') | |
| 45 | const native = release.jobs.pantry.steps! | |
| 46 | expect(native.findIndex(step => step.name === 'Scan binaries prepared for publication')).toBeGreaterThan(native.findIndex(step => step.name === 'Notarize macOS binaries')) | |
| 47 | expect(native.findIndex(step => step.name === 'Publish & Release')).toBeGreaterThan(native.findIndex(step => step.name === 'Scan binaries prepared for publication')) | |
| 48 | expect(native.filter(step => step.uses === './.github/actions/setup-release-scanners')).toHaveLength(1) | |
| 49 | expect(release.jobs.npm.steps!.filter(step => step.uses === './.github/actions/setup-release-scanners')).toHaveLength(1) | |
| 40 | 50 | expect(release.jobs['verify-macos-downloads'].strategy?.matrix.platform).toEqual([ |
| 41 | 51 | { os: 'macos-15', name: 'darwin-arm64' }, |
| 42 | 52 | { os: 'macos-15-intel', name: 'darwin-x64' }, |
| 43 | 53 | ]) |
| 44 | 54 | expect(steps(release.jobs['verify-macos-downloads'])).toContain('scripts/verify-macos-release.ts') |
| 45 | 55 | expect(release.jobs['release-sbom'].uses).toBe('./.github/workflows/sbom.yml') |
| 46 | expect(release.jobs['release-sbom'].with?.enforce_high).toBe(true) | |
| 56 | expect(release.jobs['release-sbom'].with?.enforce_high).toBe(false) | |
| 47 | 57 | expect(needs(release.jobs.pantry)).toContain('release-sbom') |
| 48 | 58 | expect(steps(release.jobs.pantry)).toContain('bun install --frozen-lockfile') |
| 49 | 59 | expect(steps(release.jobs.npm)).toContain('bun install --frozen-lockfile') |
| @@ -58,7 +58,7 @@ if (import.meta.main) { | ||
| 58 | 58 | const setting = process.env.CRAFT_FAIL_HIGH ?? 'false' |
| 59 | 59 | if (!['true', 'false'].includes(setting)) throw new Error('CRAFT_FAIL_HIGH must be true or false') |
| 60 | 60 | const { counts } = scanSbom(process.argv[2] || 'sbom/full-sbom.cyclonedx.json', process.argv[3] || 'scan-results') |
| 61 | const summary = ['## Vulnerability scan results', '', '| Severity | Count |', '| --- | --- |', ...severities.map(severity => `| ${severity} | ${counts[severity]} |`), '', 'Policy: Critical findings fail every scan. High findings fail release scans; they remain review items on main (#279).', ''].join('\n') | |
| 61 | const summary = ['## Vulnerability scan results', '', '| Severity | Count |', '| --- | --- |', ...severities.map(severity => `| ${severity} | ${counts[severity]} |`), '', 'Policy: Critical findings fail every scan. High findings in this full-source scan remain review items; release artifacts and production dependencies have a separate High gate (#279).', ''].join('\n') | |
| 62 | 62 | console.log(summary) |
| 63 | 63 | if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary) |
| 64 | 64 | if (counts.High > 0 && setting === 'false') console.warn('::warning::High vulnerability findings require review under #279') |