From c04ad4b43e040bcda9ba35b79fd87d46ec130e82 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Mon, 3 Aug 2026 16:03:10 -0700 Subject: [PATCH 1/3] ci: move dependency audits off unrelated PRs, add daily sweep New advisories against deps already on main were redding every open PR that touched a package's source, even when the PR changed no deps. - Per-package CI workflows no longer run the audit; it only ever ran on PRs there, and dep-changing PRs are already gated by the audit in ci-catalog-validation.yml (every dep change updates the root lockfile, which triggers it). - audit-deps.js gains --recursive: plain `pnpm list` at the workspace root only covers the root importer (~49 pkgs), so the no-filter audit in ci-catalog-validation.yml was silently auditing almost nothing. With --recursive it covers all workspace projects (~800 pkgs). - New deps-audit.yml runs the workspace-wide audit daily on main and opens/updates a tracking issue on failure, so advisories that drop with no dep PR in flight are still caught within a day. Templates and examples keep their own audits: they are standalone projects outside the workspace, tightly path-scoped already. --- .github/scripts/audit-deps.js | 10 +++- .github/workflows/ci-catalog-validation.yml | 8 ++- .../workflows/ci-lib-changelog-emitter.yml | 13 ---- .github/workflows/ci-lib-cli.yml | 13 ---- .github/workflows/ci-lib-core.yml | 13 ---- .github/workflows/ci-lib-ts-sdk.yml | 13 ---- .github/workflows/ci-website-preview.yml | 9 +-- .github/workflows/deps-audit.yml | 60 +++++++++++++++++++ 8 files changed, 77 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/deps-audit.yml diff --git a/.github/scripts/audit-deps.js b/.github/scripts/audit-deps.js index 529345d0d..e9968e03c 100644 --- a/.github/scripts/audit-deps.js +++ b/.github/scripts/audit-deps.js @@ -13,6 +13,7 @@ // Options: // --level Minimum severity to fail on (low|moderate|high|critical). Default: low // --filter pnpm workspace filter (e.g. @common-grants/cli) +// --recursive Audit every workspace project, not just the one in cwd // --ignore-ghsa GHSA ID to ignore (can be repeated) // // Requires pnpm on PATH. Remove this script once pnpm ships @@ -36,6 +37,7 @@ const REQUEST_TIMEOUT_MS = 30_000; const args = process.argv.slice(2); let level = 'low'; let filter = ''; +let recursive = false; const ignoreGhsas = new Set(); for (let i = 0; i < args.length; i++) { @@ -46,6 +48,9 @@ for (let i = 0; i < args.length; i++) { case '--filter': filter = args[++i]; break; + case '--recursive': + recursive = true; + break; case '--ignore-ghsa': ignoreGhsas.add(args[++i]); break; @@ -68,9 +73,12 @@ const severityGate = new Set(SEVERITY_ORDER.slice(threshold)); // 1. Collect all transitive dependencies via pnpm // --------------------------------------------------------------------------- +// Plain `pnpm list` only covers the project in cwd — at the workspace root +// that is just the root importer, which has almost no deps. --recursive is +// required for a genuinely workspace-wide audit. const listCmd = filter ? `pnpm list --filter ${filter} --json --depth=Infinity` - : 'pnpm list --json --depth=Infinity'; + : `pnpm list ${recursive ? '-r ' : ''}--json --depth=Infinity`; let raw; try { diff --git a/.github/workflows/ci-catalog-validation.yml b/.github/workflows/ci-catalog-validation.yml index cd4e76a14..c634115df 100644 --- a/.github/workflows/ci-catalog-validation.yml +++ b/.github/workflows/ci-catalog-validation.yml @@ -7,6 +7,7 @@ on: - pnpm-lock.yaml - .github/dependabot.yml - .github/workflows/ci-catalog-validation.yml + - .github/scripts/audit-deps.js workflow_dispatch: jobs: @@ -32,10 +33,15 @@ jobs: run: pnpm install --frozen-lockfile - name: Audit dependencies + # This is the dependency gate for PRs: any dep change in any workspace + # package updates the root lockfile, which triggers this workflow. + # --recursive audits every workspace project (plain `pnpm list` at the + # root only covers the root importer). + # # Calls the npm bulk advisory endpoint directly as a workaround for # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and # revert to `pnpm audit` once pnpm ships native support. - run: node .github/scripts/audit-deps.js --level moderate + run: node .github/scripts/audit-deps.js --recursive --level moderate - name: CI — core run: pnpm run ci:core diff --git a/.github/workflows/ci-lib-changelog-emitter.yml b/.github/workflows/ci-lib-changelog-emitter.yml index 60a4326ee..7be67e333 100644 --- a/.github/workflows/ci-lib-changelog-emitter.yml +++ b/.github/workflows/ci-lib-changelog-emitter.yml @@ -36,16 +36,3 @@ jobs: - name: Run tests run: pnpm --filter typespec-versioning-changelog run test:coverage - - - name: Audit dependencies - # Gated to PRs into `main` (and main-context workflow_call invocations) so - # advisories that live on `main` and can only be fixed there don't block - # PRs into HOLD-* batching branches or other non-main bases. The HOLD → main - # checkpoint PR re-runs this audit, so anything live at merge time is gated - # at the actionable boundary. - # - # Calls the npm bulk advisory endpoint directly as a workaround for - # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and - # revert to `pnpm --filter typespec-versioning-changelog run audit` once pnpm ships native support. - if: github.base_ref == 'main' || github.ref == 'refs/heads/main' - run: node ${GITHUB_WORKSPACE}/.github/scripts/audit-deps.js --filter typespec-versioning-changelog diff --git a/.github/workflows/ci-lib-cli.yml b/.github/workflows/ci-lib-cli.yml index 1345d83ac..bbdd89fbf 100644 --- a/.github/workflows/ci-lib-cli.yml +++ b/.github/workflows/ci-lib-cli.yml @@ -41,16 +41,3 @@ jobs: - name: Build library run: pnpm --filter @common-grants/cli run build - - - name: Audit dependencies - # Gated to PRs into `main` (and main-context workflow_call invocations) so - # advisories that live on `main` and can only be fixed there don't block - # PRs into HOLD-* batching branches or other non-main bases. The HOLD → main - # checkpoint PR re-runs this audit, so anything live at merge time is gated - # at the actionable boundary. - # - # Calls the npm bulk advisory endpoint directly as a workaround for - # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and - # revert to `pnpm --filter @common-grants/cli run audit` once pnpm ships native support. - if: github.base_ref == 'main' || github.ref == 'refs/heads/main' - run: node ${GITHUB_WORKSPACE}/.github/scripts/audit-deps.js --filter @common-grants/cli diff --git a/.github/workflows/ci-lib-core.yml b/.github/workflows/ci-lib-core.yml index ec3b01cc7..2415c6df1 100644 --- a/.github/workflows/ci-lib-core.yml +++ b/.github/workflows/ci-lib-core.yml @@ -38,16 +38,3 @@ jobs: - name: Emit TypeSpec outputs run: pnpm --filter @common-grants/core run typespec - - - name: Audit dependencies - # Gated to PRs into `main` (and main-context workflow_call invocations) so - # advisories that live on `main` and can only be fixed there don't block - # PRs into HOLD-* batching branches or other non-main bases. The HOLD → main - # checkpoint PR re-runs this audit, so anything live at merge time is gated - # at the actionable boundary. - # - # Calls the npm bulk advisory endpoint directly as a workaround for - # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and - # revert to `pnpm --filter @common-grants/core run audit` once pnpm ships native support. - if: github.base_ref == 'main' || github.ref == 'refs/heads/main' - run: node ${GITHUB_WORKSPACE}/.github/scripts/audit-deps.js --filter @common-grants/core diff --git a/.github/workflows/ci-lib-ts-sdk.yml b/.github/workflows/ci-lib-ts-sdk.yml index b634c81d7..056dc2cea 100644 --- a/.github/workflows/ci-lib-ts-sdk.yml +++ b/.github/workflows/ci-lib-ts-sdk.yml @@ -38,16 +38,3 @@ jobs: - name: Run tests run: pnpm --filter @common-grants/sdk run test:coverage - - - name: Audit dependencies - # Gated to PRs into `main` (and main-context workflow_call invocations) so - # advisories that live on `main` and can only be fixed there don't block - # PRs into HOLD-* batching branches or other non-main bases. The HOLD → main - # checkpoint PR re-runs this audit, so anything live at merge time is gated - # at the actionable boundary. - # - # Calls the npm bulk advisory endpoint directly as a workaround for - # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and - # revert to `pnpm --filter @common-grants/sdk run audit` once pnpm ships native support. - if: github.base_ref == 'main' || github.ref == 'refs/heads/main' - run: node ${GITHUB_WORKSPACE}/.github/scripts/audit-deps.js --filter @common-grants/sdk diff --git a/.github/workflows/ci-website-preview.yml b/.github/workflows/ci-website-preview.yml index ead917f46..e960465b8 100644 --- a/.github/workflows/ci-website-preview.yml +++ b/.github/workflows/ci-website-preview.yml @@ -77,15 +77,8 @@ jobs: if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: pnpm run test - - name: Audit dependencies - # Calls the npm bulk advisory endpoint directly as a workaround for - # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and - # revert to `pnpm run audit:high` once pnpm ships native support. - if: ${{ !cancelled() && steps.build.outcome == 'success' }} - run: node ${GITHUB_WORKSPACE}/.github/scripts/audit-deps.js --level high - # Deploy the built artifact to a per-PR Cloudflare Worker. Decoupled from - # the test/check/audit outcome (via artifact_ready) so previews stay + # the test/check outcome (via artifact_ready) so previews stay # available for reviewing UI changes on PRs that have unrelated failures. deploy-preview: runs-on: ubuntu-latest diff --git a/.github/workflows/deps-audit.yml b/.github/workflows/deps-audit.yml new file mode 100644 index 000000000..dfae81627 --- /dev/null +++ b/.github/workflows/deps-audit.yml @@ -0,0 +1,60 @@ +name: "Deps: Scheduled Dependency Audit" + +# Daily workspace-wide advisory sweep on main. New GHSAs published against +# deps already on main surface here as a tracking issue within a day, instead +# of redding every open PR the moment they drop. PRs that actually change +# deps are still gated by the audit in ci-catalog-validation.yml (any dep +# change updates the root lockfile, which triggers it). +on: + schedule: + # Daily at ~7-8am PT (15:00 UTC = 8am PDT / 7am PST) + - cron: "0 15 * * *" + workflow_dispatch: + # Smoke-test on changes here (audit script changes are covered by + # ci-catalog-validation.yml). + pull_request: + paths: + - .github/workflows/deps-audit.yml + +permissions: + contents: read + issues: write + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install pnpm + uses: pnpm/action-setup@v6 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Audit all workspace packages + # Calls the npm bulk advisory endpoint directly as a workaround for + # pnpm audit 410 errors (pnpm/pnpm#11265). Remove this script and + # revert to `pnpm audit` once pnpm ships native support. + run: node .github/scripts/audit-deps.js --recursive + + - name: Open tracking issue on scheduled failure + if: failure() && github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + TITLE="Scheduled dependency audit is failing" + EXISTING=$(gh issue list --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number') + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body "Still failing: $RUN_URL" + else + gh issue create --title "$TITLE" --label dependencies \ + --body "The daily workspace-wide dependency audit found advisories against deps on \`main\`. See the run log for details: $RUN_URL" + fi From 1ce18cca8ebc4e0938cadd48197e907e02bd5f6b Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Tue, 4 Aug 2026 09:09:52 -0700 Subject: [PATCH 2/3] fix(deps): floor-override postcss and body-parser advisories - postcss@<=8.5.22 -> >=8.5.23 (GHSA-fxqj-rqcc-2cmp, moderate) via website>@astrojs/react>vite>postcss; resolves to 8.5.25. - body-parser@<1.20.6 -> >=1.20.6 <2 (GHSA-v422-hmwv-36x6, low) via lib/cli>express; bounded below 2 so express 4 keeps the 1.x line. Verified: pnpm audit and audit-deps.js --recursive both clean; cli and website test suites green against the new resolutions. --- pnpm-lock.yaml | 30 ++++++++++++++++-------------- pnpm-workspace.yaml | 2 ++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8408f15b6..cb30c5c47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,12 +76,14 @@ catalogs: overrides: axios@<1.15.0: '>=1.15.0' + body-parser@<1.20.6: '>=1.20.6 <2' brace-expansion@<5.0.9: '>=5.0.9' defu@<=6.1.4: '>=6.1.5' esbuild@>=0.17.0 <0.28.1: '>=0.28.1' fast-uri@>=3.0.0 <3.1.5: '>=3.1.5 <4' form-data@>=4.0.0 <4.0.6: '>=4.0.6' lodash@>=4.0.0 <=4.17.23: '>=4.18.0' + postcss@<=8.5.22: '>=8.5.23' qs@<6.15.2: '>=6.15.2' sharp@<0.35.0: '>=0.35.0' undici@>=7.0.0 <7.29.0: 7.29.0 @@ -3129,8 +3131,8 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} boolbase@1.0.0: @@ -5071,7 +5073,7 @@ packages: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: - postcss: ^8.2.14 + postcss: '>=8.5.23' postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} @@ -5081,8 +5083,8 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss@8.5.22: - resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -7397,8 +7399,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.22 - postcss-nested: 6.2.0(postcss@8.5.22) + postcss: 8.5.25 + postcss-nested: 6.2.0(postcss@8.5.25) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -9413,7 +9415,7 @@ snapshots: blake3-wasm@2.1.5: {} - body-parser@1.20.5: + body-parser@1.20.6: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -9983,7 +9985,7 @@ snapshots: eslint: 10.5.0 eslint-compat-utils: 0.6.5(eslint@10.5.0) globals: 16.5.0 - postcss: 8.5.22 + postcss: 8.5.25 postcss-selector-parser: 7.1.1 transitivePeerDependencies: - supports-color @@ -10121,7 +10123,7 @@ snapshots: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -11732,9 +11734,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@6.2.0(postcss@8.5.22): + postcss-nested@6.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.22 + postcss: 8.5.25 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -11747,7 +11749,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.22: + postcss@8.5.25: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -12958,7 +12960,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 - postcss: 8.5.22 + postcss: 8.5.25 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 44401fa66..c319f18d6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,12 +35,14 @@ catalogs: overrides: axios@<1.15.0: '>=1.15.0' + body-parser@<1.20.6: '>=1.20.6 <2' brace-expansion@<5.0.9: '>=5.0.9' defu@<=6.1.4: '>=6.1.5' esbuild@>=0.17.0 <0.28.1: '>=0.28.1' fast-uri@>=3.0.0 <3.1.5: '>=3.1.5 <4' form-data@>=4.0.0 <4.0.6: '>=4.0.6' lodash@>=4.0.0 <=4.17.23: '>=4.18.0' + postcss@<=8.5.22: '>=8.5.23' qs@<6.15.2: '>=6.15.2' sharp@<0.35.0: '>=0.35.0' undici@>=7.0.0 <7.29.0: '7.29.0' From da6e3ea95accb713527d71ebbe48272f8f271d27 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Wed, 5 Aug 2026 09:47:12 -0700 Subject: [PATCH 3/3] ci: drop audit from the website preview job comment The validate job no longer runs an audit step, so the comment describing checks/tests/audit was stale. --- .github/workflows/ci-website-preview.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-website-preview.yml b/.github/workflows/ci-website-preview.yml index 5ab97554f..2b4f089a1 100644 --- a/.github/workflows/ci-website-preview.yml +++ b/.github/workflows/ci-website-preview.yml @@ -17,9 +17,9 @@ env: CLOUDFLARE_WORKERS_DOMAIN: billy-daly.workers.dev jobs: - # Install, build, and run checks/tests/audit. All post-build steps are - # sequential but each runs regardless of the previous step's pass/fail so - # lint, test, and audit failures all surface in a single CI run. + # Install, build, and run checks/tests. All post-build steps are sequential + # but each runs regardless of the previous step's pass/fail so lint and test + # failures all surface in a single CI run. validate: runs-on: ubuntu-latest outputs: