From 61c97ac08acbd4ff91d959ccde7a3d931d7a3137 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 13:02:05 -0600 Subject: [PATCH 1/6] fix(ci): split bun cache restore from save in setup-bun composite The composite previously used actions/cache@v5 (full) which auto-saves at job end, allowing PR-controlled code to write into the cache before save. Use actions/cache/restore@v5 for reads and gate actions/cache/save@v5 on github.event_name != 'pull_request' so only trusted runs (push to main, workflow_call from release.yml) populate the cache. --- .github/actions/setup-bun/action.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index ea06fe35..838836cb 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -13,17 +13,26 @@ runs: using: composite steps: - uses: oven-sh/setup-bun@v2 - - uses: actions/cache@v5 - if: inputs.cache == 'read-write' + + # Restore on both 'restore' and 'read-write'. Save is the only difference. + - name: Restore Bun cache + if: inputs.cache != 'none' + uses: actions/cache/restore@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} restore-keys: bun-${{ runner.os }}- - - uses: actions/cache/restore@v5 - if: inputs.cache == 'restore' + + - run: bun install --frozen-lockfile + shell: bash + + # Save only from trusted contexts. PR-triggered runs (github.event_name + # == 'pull_request') skip the save so PR-controlled code cannot poison + # the cache for future runs. Push to main and workflow_call invocations + # from release.yml (which inherit 'push' or 'issue_comment') do save. + - name: Save Bun cache + if: inputs.cache == 'read-write' && github.event_name != 'pull_request' + uses: actions/cache/save@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - restore-keys: bun-${{ runner.os }}- - - run: bun install --frozen-lockfile - shell: bash From 574192cd9568e22c03ed4ba28c458250e03ea0ae Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 13:05:14 -0600 Subject: [PATCH 2/6] fix(ci): tighten setup-bun cache save gate - Gate the save step on the restore step's cache-hit output so we do not warn-spam when the key already exists. - Tighten the trust gate from github.event_name != 'pull_request' to github.event_name == 'push' so issue_comment (!snapshot) runs do not save the cache. Snapshot CI checks out PR-author-controlled code; only push-to-main is a structurally trusted save context. --- .github/actions/setup-bun/action.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 838836cb..4635948e 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -16,6 +16,7 @@ runs: # Restore on both 'restore' and 'read-write'. Save is the only difference. - name: Restore Bun cache + id: bun-cache if: inputs.cache != 'none' uses: actions/cache/restore@v5 with: @@ -26,12 +27,17 @@ runs: - run: bun install --frozen-lockfile shell: bash - # Save only from trusted contexts. PR-triggered runs (github.event_name - # == 'pull_request') skip the save so PR-controlled code cannot poison - # the cache for future runs. Push to main and workflow_call invocations - # from release.yml (which inherit 'push' or 'issue_comment') do save. + # Save only on push to main. PR (pull_request) and snapshot (issue_comment) + # runs both check out PR-author-controlled code, so they must not write the + # shared cache. Push-to-main reaches this composite via workflow_call from + # release.yml; the called workflow inherits github.event_name == 'push'. + # Skip the save when the restore was a full hit — actions/cache/save warns + # noisily when the key already exists. - name: Save Bun cache - if: inputs.cache == 'read-write' && github.event_name != 'pull_request' + if: >- + inputs.cache == 'read-write' && + github.event_name == 'push' && + steps.bun-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: path: ~/.bun/install/cache From 2ae91525012163746b7414c4f17142ff67992e43 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 13:07:19 -0600 Subject: [PATCH 3/6] fix(ci): use setup-bun composite in build/lint/test jobs Replaces inline oven-sh/setup-bun@v2 + actions/cache(/restore)@v5 + bun install steps with a single ./.github/actions/setup-bun reference. Combined with the composite's split restore/save and the github.event_name == 'push' trust gate, this resolves the 9 CodeQL alerts (actions/cache-poisoning/poisonable-step #29-#37) in ci.yml. The test-e2e job is unchanged. --- .github/workflows/ci.yml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de8dee34..63f90dc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,13 +30,9 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: oven-sh/setup-bun@v2 - - uses: actions/cache@v5 + - uses: ./.github/actions/setup-bun with: - path: ~/.bun/install/cache - key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - restore-keys: bun-${{ runner.os }}- - - run: bun install --frozen-lockfile + cache: read-write - run: bun run build lint: @@ -48,13 +44,9 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: oven-sh/setup-bun@v2 - - uses: actions/cache/restore@v5 + - uses: ./.github/actions/setup-bun with: - path: ~/.bun/install/cache - key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - restore-keys: bun-${{ runner.os }}- - - run: bun install --frozen-lockfile + cache: restore - run: bun run format:check - run: bun run lint - run: bun run typecheck @@ -68,13 +60,9 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: oven-sh/setup-bun@v2 - - uses: actions/cache/restore@v5 + - uses: ./.github/actions/setup-bun with: - path: ~/.bun/install/cache - key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - restore-keys: bun-${{ runner.os }}- - - run: bun install --frozen-lockfile + cache: restore - run: bun run check:patches - run: bun run test From 35230d67507ef44d94c5e9ea0049c42783eb45a5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 14:44:56 -0600 Subject: [PATCH 4/6] refactor(ci): move bun cache save into push-only workflow The setup-bun composite previously contained a conditional cache save step. Even gated to push contexts, the save action's structural presence in a job reachable from a workflow_call that ultimately traces back to issue_comment (the !snapshot path) is enough for CodeQL's cache-poisoning rule to flag every step in the job. Move the cache save into a dedicated warm-bun-cache.yml workflow triggered only by push to main. The composite becomes restore-only and its read-write mode is removed (no callers used it after this refactor's build job switched to restore). PR runs read main's warmed cache via restore-keys fallback; the warmup workflow rebuilds the cache whenever bun.lock changes on main. --- .github/actions/setup-bun/action.yml | 25 ++++----------------- .github/workflows/ci.yml | 2 +- .github/workflows/warm-bun-cache.yml | 33 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/warm-bun-cache.yml diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 4635948e..88da5a1e 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -5,8 +5,9 @@ inputs: cache: description: > Cache mode for Bun dependencies. - "none" skips caching, "restore" uses read-only cache restore, - "read-write" populates the cache after install. + "none" skips caching, "restore" uses read-only cache restore. + The cache is populated by the warm-bun-cache.yml workflow on push to + main; PR and workflow_call runs only restore. default: "none" runs: @@ -14,10 +15,8 @@ runs: steps: - uses: oven-sh/setup-bun@v2 - # Restore on both 'restore' and 'read-write'. Save is the only difference. - name: Restore Bun cache - id: bun-cache - if: inputs.cache != 'none' + if: inputs.cache == 'restore' uses: actions/cache/restore@v5 with: path: ~/.bun/install/cache @@ -26,19 +25,3 @@ runs: - run: bun install --frozen-lockfile shell: bash - - # Save only on push to main. PR (pull_request) and snapshot (issue_comment) - # runs both check out PR-author-controlled code, so they must not write the - # shared cache. Push-to-main reaches this composite via workflow_call from - # release.yml; the called workflow inherits github.event_name == 'push'. - # Skip the save when the restore was a full hit — actions/cache/save warns - # noisily when the key already exists. - - name: Save Bun cache - if: >- - inputs.cache == 'read-write' && - github.event_name == 'push' && - steps.bun-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 - with: - path: ~/.bun/install/cache - key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63f90dc2..09294eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: ref: ${{ inputs.ref }} - uses: ./.github/actions/setup-bun with: - cache: read-write + cache: restore - run: bun run build lint: diff --git a/.github/workflows/warm-bun-cache.yml b/.github/workflows/warm-bun-cache.yml new file mode 100644 index 00000000..8da14f9b --- /dev/null +++ b/.github/workflows/warm-bun-cache.yml @@ -0,0 +1,33 @@ +name: Warm Bun cache + +# Populates ~/.bun/install/cache for PRs to read via the setup-bun composite's +# restore step. Runs only on push to main so untrusted (PR/snapshot) code can +# never reach a cache save action. + +on: + push: + branches: [main] + paths: + - bun.lock + - .github/workflows/warm-bun-cache.yml + +permissions: + contents: read + +concurrency: + group: warm-bun-cache-${{ github.ref }} + cancel-in-progress: true + +jobs: + warm: + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile From b9bb16573da0334fae5fca688cf273f66684f2e1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 17:10:32 -0600 Subject: [PATCH 5/6] refactor(ci): inline setup-bun composite, gate cache restore on snapshot Removes the .github/actions/setup-bun composite action and inlines its operations (oven-sh/setup-bun@v2, optional actions/cache/restore@v5, bun install --frozen-lockfile) at all eight call sites. In ci.yml's build, lint, test, and test-e2e jobs, the cache restore is gated on github.event_name != 'issue_comment' so the snapshot path (which checks out PR-author-controlled code in the default branch's privileged context via release.yml's workflow_call) cannot read the cache. Other workflows (release.yml versioning/canary-version, notify-failure, notify-release) inline directly without a gate because they only run on trusted triggers. --- .github/actions/setup-bun/action.yml | 27 ---------------------- .github/workflows/ci.yml | 34 ++++++++++++++++++++++------ .github/workflows/notify-failure.yml | 3 ++- .github/workflows/notify-release.yml | 3 ++- .github/workflows/release.yml | 16 +++++++++---- .github/workflows/warm-bun-cache.yml | 6 ++--- 6 files changed, 46 insertions(+), 43 deletions(-) delete mode 100644 .github/actions/setup-bun/action.yml diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml deleted file mode 100644 index 88da5a1e..00000000 --- a/.github/actions/setup-bun/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Setup Bun -description: Install Bun, optionally restore dependency cache, and run bun install - -inputs: - cache: - description: > - Cache mode for Bun dependencies. - "none" skips caching, "restore" uses read-only cache restore. - The cache is populated by the warm-bun-cache.yml workflow on push to - main; PR and workflow_call runs only restore. - default: "none" - -runs: - using: composite - steps: - - uses: oven-sh/setup-bun@v2 - - - name: Restore Bun cache - if: inputs.cache == 'restore' - uses: actions/cache/restore@v5 - with: - path: ~/.bun/install/cache - key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - restore-keys: bun-${{ runner.os }}- - - - run: bun install --frozen-lockfile - shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09294eeb..99bd3999 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,18 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + # Skip cache restore when invoked via workflow_call from release.yml's + # snapshot path (issue_comment trigger). That path checks out + # PR-author-controlled code in the default branch's privileged context; + # consuming a cached install in that context is a poisoning surface. + - if: github.event_name != 'issue_comment' + uses: actions/cache/restore@v5 with: - cache: restore + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile - run: bun run build lint: @@ -44,9 +53,14 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - if: github.event_name != 'issue_comment' + uses: actions/cache/restore@v5 with: - cache: restore + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile - run: bun run format:check - run: bun run lint - run: bun run typecheck @@ -60,9 +74,14 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - if: github.event_name != 'issue_comment' + uses: actions/cache/restore@v5 with: - cache: restore + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile - run: bun run check:patches - run: bun run test @@ -102,7 +121,8 @@ jobs: - name: Mark workspace as safe for git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - uses: oven-sh/setup-bun@v2 - - uses: actions/cache/restore@v5 + - if: github.event_name != 'issue_comment' + uses: actions/cache/restore@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} diff --git a/.github/workflows/notify-failure.yml b/.github/workflows/notify-failure.yml index a765e5e3..d9889104 100644 --- a/.github/workflows/notify-failure.yml +++ b/.github/workflows/notify-failure.yml @@ -20,7 +20,8 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile - name: Notify Slack run: bun scripts/slack.ts --status failure env: diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml index d79cf6ce..31ef5b24 100644 --- a/.github/workflows/notify-release.yml +++ b/.github/workflows/notify-release.yml @@ -20,7 +20,8 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile - name: Notify Slack run: bun scripts/slack-release.ts --version "$VERSION" env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa751b69..733814e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,9 +36,13 @@ jobs: version: ${{ steps.check.outputs.version }} steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache/restore@v5 with: - cache: restore + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile - name: Check if release needed id: check @@ -194,9 +198,13 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-bun + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache/restore@v5 with: - cache: restore + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + - run: bun install --frozen-lockfile - name: Version packages for canary id: version diff --git a/.github/workflows/warm-bun-cache.yml b/.github/workflows/warm-bun-cache.yml index 8da14f9b..d74fd492 100644 --- a/.github/workflows/warm-bun-cache.yml +++ b/.github/workflows/warm-bun-cache.yml @@ -1,8 +1,8 @@ name: Warm Bun cache -# Populates ~/.bun/install/cache for PRs to read via the setup-bun composite's -# restore step. Runs only on push to main so untrusted (PR/snapshot) code can -# never reach a cache save action. +# Populates ~/.bun/install/cache for PR and release runs to read. +# Runs only on push to main so untrusted (PR/snapshot) code never reaches +# the cache save action. on: push: From f56e9a0c1026477b5c1168bd732db3e083388a03 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 12 May 2026 17:13:23 -0600 Subject: [PATCH 6/6] refactor(ci): restore bun cache in notify workflows Both notify-failure and notify-release run on push-to-main triggered release workflows where the cache is already warm; adding the restore step avoids a cold install on every notification job. --- .github/workflows/notify-failure.yml | 5 +++++ .github/workflows/notify-release.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/notify-failure.yml b/.github/workflows/notify-failure.yml index d9889104..1ae06966 100644 --- a/.github/workflows/notify-failure.yml +++ b/.github/workflows/notify-failure.yml @@ -21,6 +21,11 @@ jobs: steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 + - uses: actions/cache/restore@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- - run: bun install --frozen-lockfile - name: Notify Slack run: bun scripts/slack.ts --status failure diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml index 31ef5b24..bd8d94f8 100644 --- a/.github/workflows/notify-release.yml +++ b/.github/workflows/notify-release.yml @@ -21,6 +21,11 @@ jobs: steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 + - uses: actions/cache/restore@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- - run: bun install --frozen-lockfile - name: Notify Slack run: bun scripts/slack-release.ts --version "$VERSION"