From 80a7ffeae0e87a0fbab3faf52c9f948daf67c702 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 1 Jul 2026 22:07:52 -0400 Subject: [PATCH 1/2] ci: add scheduled pnpm audit-fix workflow Sweeps the standing dependency tree daily for newly disclosed advisories (independent of PR-triggered audits), applies `pnpm audit --fix=override`, and opens/refreshes a single review PR via a scoped GitHub App token. The token is minted just before the PR step so it is absent while dependency build scripts run. Fails the run if advisories remain unresolved. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/audit-fix.yaml | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/audit-fix.yaml diff --git a/.github/workflows/audit-fix.yaml b/.github/workflows/audit-fix.yaml new file mode 100644 index 0000000..43d1b37 --- /dev/null +++ b/.github/workflows/audit-fix.yaml @@ -0,0 +1,107 @@ +name: Audit fix + +# Sweeps the standing dependency tree for newly disclosed advisories on a +# schedule, independent of any PR. This catches advisories published against +# deps we already ship -- a per-PR audit only runs when a PR changes the +# dependency graph. On a finding, pnpm audit --fix writes security-floor +# overrides into pnpm-workspace.yaml and this opens (or refreshes) a single PR +# for a human to review. --fix output is often sloppy (overly broad ranges, +# caret targets instead of pins, minimumReleaseAgeExclude entries), so the PR +# is never auto-merged. +# +# The PR is opened with a GitHub App token (ACTIONS_APP_ID / +# ACTIONS_APP_PRIVATE_KEY) rather than the default GITHUB_TOKEN so that (a) the +# PR triggers the normal check workflows -- GITHUB_TOKEN-authored PRs do not -- +# and (b) it sidesteps the "Allow GitHub Actions to create and approve pull +# requests" org toggle, which only governs GITHUB_TOKEN. The app must be +# installed with contents + pull-requests write. + +on: + schedule: + - cron: "17 7 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: audit-fix + cancel-in-progress: false + +jobs: + audit-fix: + if: github.repository == 'agentcommercekit/ack' + runs-on: ubuntu-latest + # GITHUB_TOKEN only needs read for checkout; the App token (minted just + # before the PR step) carries the contents + pull-requests write scopes used + # to open the PR. Inherits the workflow-level contents: read. + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: ./.github/actions/setup + # pnpm audit exits non-zero when it finds advisories; --fix=override still + # applies the overrides, so swallow the status here and let the diff drive + # the PR. The mode must be named explicitly: despite `pnpm audit --help` + # claiming --fix defaults to "override", bare `pnpm audit --fix` fails with + # ERR_PNPM_INVALID_FIX_OPTION ("Invalid value for --fix: true. Should be + # one of override or update") -- which `|| true` would otherwise mask, + # leaving the fixer a silent no-op. + - name: Apply pnpm audit fixes + run: pnpm audit --fix=override || true + - name: Refresh lockfile + run: pnpm install --lockfile-only + # Decide pass/fail against the post-fix tree HERE, before + # create-pull-request touches the working directory -- so the verdict can't + # depend on whatever state that action leaves behind. clean=false means + # --fix could not resolve everything (or audit errored); the final step + # turns that into a red, notifying run. auditConfig.ignoreGhsas still + # applies, so accepted advisories don't trip it. + - name: Re-audit the post-fix tree + id: audit + run: | + pnpm audit && echo "clean=true" >> "$GITHUB_OUTPUT" \ + || echo "clean=false" >> "$GITHUB_OUTPUT" + # Mint the App token here, AFTER dependency install has run, so the + # contents/pull-requests-write token is never present in the runner + # environment while third-party build scripts (allowBuilds) execute. + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + app-id: ${{ vars.ACTIONS_APP_ID }} + private-key: ${{ secrets.ACTIONS_APP_PRIVATE_KEY }} + # Scope the minted token to exactly what opening the PR needs, rather + # than the app installation's full permission set. + permission-contents: write + permission-pull-requests: write + - name: Open or update fix PR + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.app-token.outputs.token }} + branch: chore/audit-fix + base: main + delete-branch: true + title: "chore(deps): apply pnpm audit fixes" + commit-message: "chore(deps): apply pnpm audit fixes" + labels: dependencies + body: | + Automated `pnpm audit --fix` from the scheduled audit sweep + (`.github/workflows/audit-fix.yaml`). + + **AI usage:** none. This PR is generated mechanically by `pnpm audit + --fix=override`; no AI tools authored these changes (per + [AI_POLICY.md](AI_POLICY.md)). + + Before merging, tidy the generated overrides by hand — `--fix` tends + to write overly broad ranges, caret targets instead of pinned + versions, and `minimumReleaseAgeExclude` entries that should be + pruned once the patch ages past `minimumReleaseAge`. Confirm each + override is scoped to the affected major and carries a comment + naming the advisory, matching the existing block in + `pnpm-workspace.yaml`. + # A standing advisory that --fix can't auto-resolve must not pass as a + # silent green sweep. The post-fix re-audit above already decided this; a + # PR (if any) was opened with whatever --fix could resolve first. + - name: Fail if advisories remain unresolved + if: steps.audit.outputs.clean != 'true' + run: exit 1 From bf4cce063659464b94aa5e1b777042f2d71e9fa9 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 1 Jul 2026 22:42:52 -0400 Subject: [PATCH 2/2] fix(ci): harden audit-fix workflow scope, timeout, and PR body - Pin actions/checkout to ref: main so a workflow_dispatch from a feature branch can't leak its tree into the chore/audit-fix PR (which targets main). - Add timeout-minutes: 15 so a hung audit/install can't hold the cancel-in-progress: false concurrency group for GitHub's 360-min default. - Add a conditional "Partial fix" banner to the PR body when the post-fix re-audit is still dirty, since that red status lives on the audit-fix run rather than the PR's own checks. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/audit-fix.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/audit-fix.yaml b/.github/workflows/audit-fix.yaml index 43d1b37..e0fa4dd 100644 --- a/.github/workflows/audit-fix.yaml +++ b/.github/workflows/audit-fix.yaml @@ -32,12 +32,22 @@ jobs: audit-fix: if: github.repository == 'agentcommercekit/ack' runs-on: ubuntu-latest + # Bound the run so a hung pnpm audit/install can't hold the audit-fix + # concurrency group (cancel-in-progress: false) for GitHub's 360-min default + # and stall the next daily sweep. + timeout-minutes: 15 # GITHUB_TOKEN only needs read for checkout; the App token (minted just # before the PR step) carries the contents + pull-requests write scopes used # to open the PR. Inherits the workflow-level contents: read. steps: - uses: actions/checkout@v4 with: + # Always audit main's dependency tree. Without an explicit ref, a + # manual workflow_dispatch from a feature branch would check out that + # branch and leak its changes into the chore/audit-fix PR (which + # targets main). Scheduled runs already run on main; this pins dispatch + # to the same safe scope. + ref: main persist-credentials: false - uses: ./.github/actions/setup # pnpm audit exits non-zero when it finds advisories; --fix=override still @@ -85,6 +95,8 @@ jobs: commit-message: "chore(deps): apply pnpm audit fixes" labels: dependencies body: | + ${{ steps.audit.outputs.clean != 'true' && '**Partial fix:** some advisories could not be auto-resolved. The scheduled audit-fix run for this change is red -- review its log and resolve the remainder by hand before merging.' || '' }} + Automated `pnpm audit --fix` from the scheduled audit sweep (`.github/workflows/audit-fix.yaml`).