From b5c4ba554d75b9b19f0a8eb226d371dc80a66a7e Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 10 Sep 2026 19:04:02 -0700 Subject: [PATCH 1/3] ci: cache Rust dependencies in release Runner verification Co-Authored-By: Paperclip --- .../tests/release-runner-cache.test.mjs | 35 +++++++++++++++++++ .github/workflows/release-verify.yml | 21 +++++++++++ doc/RELEASE-AUTOMATION-SETUP.md | 16 +++++++++ 3 files changed, 72 insertions(+) create mode 100644 .github/scripts/tests/release-runner-cache.test.mjs diff --git a/.github/scripts/tests/release-runner-cache.test.mjs b/.github/scripts/tests/release-runner-cache.test.mjs new file mode 100644 index 0000000000..84cbbee0e9 --- /dev/null +++ b/.github/scripts/tests/release-runner-cache.test.mjs @@ -0,0 +1,35 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const workflow = readFileSync(new URL("../../workflows/release-verify.yml", import.meta.url), "utf8"); +const runner = workflow.split(" verify_paperclip_runner:")[1].split(" build:")[0]; + +test("Runner dependency caching selects the package's pinned compiler before computing its key", () => { + const select = runner.indexOf(" - name: Select the pinned Runner Rust toolchain"); + const cache = runner.indexOf(" - name: Cache Runner Rust dependencies"); + assert.ok(select >= 0 && cache > select); + const setup = runner.slice(select, cache); + assert.match(setup, /working-directory: packages\/paperclip-runner/); + assert.match(setup, /rustup show active-toolchain/); + assert.match(setup, /echo "RUSTUP_TOOLCHAIN=\$toolchain" >> "\$GITHUB_ENV"/); + assert.match(runner, /uses: Swatinem\/rust-cache@[0-9a-f]{40} # v[0-9.]+/); + assert.match(runner, /workspaces: packages\/paperclip-runner\/runner -> target/); + assert.match(runner, /shared-key: release-runner-v1/); +}); + +test("the shared cache excludes workspace artifacts and only saves the exact master-push source", () => { + assert.match(runner, /cache-workspace-crates: false/); + assert.match(runner, /cache-bin: false/); + const saveIf = runner.match(/^\s*save-if: (.+)$/m)?.[1]; + assert.equal(saveIf, "${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}"); + assert.doesNotMatch(runner, /cache-on-failure: true|cache-all-crates: true/); +}); + +test("cache hits cannot bypass Runner verification", () => { + const verify = runner.split(" - name: Verify Paperclip Runner")[1]; + assert.match(verify, /run: pnpm --filter @paperclipai\/paperclip-runner check:all/); + assert.doesNotMatch(verify, /if:|continue-on-error:/); + assert.ok(runner.indexOf("Cache Runner Rust dependencies") < runner.indexOf(" - name: Verify Paperclip Runner\n")); + assert.doesNotMatch(runner, /id-token: write|packages: write|secrets: inherit/); +}); diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index ecf71da950..5b25fea478 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -227,6 +227,27 @@ jobs: node-version: 24 cache: pnpm + - name: Select the pinned Runner Rust toolchain + working-directory: packages/paperclip-runner + run: | + set -euo pipefail + rustup show + toolchain="$(rustup show active-toolchain | awk '{print $1}')" + echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" + + - name: Cache Runner Rust dependencies + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: packages/paperclip-runner/runner -> target + shared-key: release-runner-v1 + # Rebuild workspace code and rerun every check. Cache only compiled + # dependencies; never restore installed executables from cargo/bin. + cache-workspace-crates: false + cache-bin: false + # Manual candidate refs may read the cache, but only a successful + # master push verifying its own commit can populate the shared cache. + save-if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }} + - name: Install dependencies run: pnpm install --no-frozen-lockfile diff --git a/doc/RELEASE-AUTOMATION-SETUP.md b/doc/RELEASE-AUTOMATION-SETUP.md index e8131595f8..a7629295f8 100644 --- a/doc/RELEASE-AUTOMATION-SETUP.md +++ b/doc/RELEASE-AUTOMATION-SETUP.md @@ -335,3 +335,19 @@ Check: - [doc/RELEASING.md](RELEASING.md) - [doc/PUBLISHING.md](PUBLISHING.md) - [doc/plans/2026-03-17-release-automation-and-versioning.md](plans/2026-03-17-release-automation-and-versioning.md) + +## Runner verification dependency cache + +`release-verify.yml` caches Cargo dependencies for its `Verify Paperclip Runner` +job using a pinned Rust Cache action. It selects the compiler from the Runner +package's `rust-toolchain.toml` before computing the cache key. Compiler and Cargo +metadata changes select a new cache; the `release-runner-v1` shared key lets +callers of this reusable verification workflow reuse the same dependency cache. + +Workspace crates and installed Cargo binaries are excluded. Every run still +builds the Runner workspace and runs `check:all`, including the Rust and +TypeScript tests. Only a successful master-push run verifying that push's exact +SHA saves the shared cache. Manual candidate verification can restore it without +saving. A miss or eviction costs compilation time but does not change the checks. +To discard old dependency caches, increment the shared-key version and let the +next successful master verification warm it again. From a41b759f47d6f29c6506453309796627c81a2a60 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 10 Sep 2026 19:15:51 -0700 Subject: [PATCH 2/3] ci: restrict Rust cache restores to trusted master pushes Co-Authored-By: Paperclip --- .../scripts/tests/release-runner-cache.test.mjs | 4 +++- .github/workflows/release-verify.yml | 3 +++ doc/RELEASE-AUTOMATION-SETUP.md | 14 +++++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/scripts/tests/release-runner-cache.test.mjs b/.github/scripts/tests/release-runner-cache.test.mjs index 84cbbee0e9..9e4b8fe715 100644 --- a/.github/scripts/tests/release-runner-cache.test.mjs +++ b/.github/scripts/tests/release-runner-cache.test.mjs @@ -18,11 +18,13 @@ test("Runner dependency caching selects the package's pinned compiler before com assert.match(runner, /shared-key: release-runner-v1/); }); -test("the shared cache excludes workspace artifacts and only saves the exact master-push source", () => { +test("the shared cache excludes workspace artifacts and only restores or saves the exact master-push source", () => { assert.match(runner, /cache-workspace-crates: false/); assert.match(runner, /cache-bin: false/); const saveIf = runner.match(/^\s*save-if: (.+)$/m)?.[1]; assert.equal(saveIf, "${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}"); + const cacheStep = runner.split(" - name: Cache Runner Rust dependencies")[1].split(" - name: Install dependencies")[0]; + assert.equal(cacheStep.match(/^\s*if: (.+)$/m)?.[1], saveIf); assert.doesNotMatch(runner, /cache-on-failure: true|cache-all-crates: true/); }); diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index 5b25fea478..4a7b9e2e29 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -236,6 +236,9 @@ jobs: echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" - name: Cache Runner Rust dependencies + # Restore and save only within trusted master-push verification. GitHub + # isolates branch/PR caches from master; other callers compile afresh. + if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }} uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: packages/paperclip-runner/runner -> target diff --git a/doc/RELEASE-AUTOMATION-SETUP.md b/doc/RELEASE-AUTOMATION-SETUP.md index a7629295f8..7896b6cf98 100644 --- a/doc/RELEASE-AUTOMATION-SETUP.md +++ b/doc/RELEASE-AUTOMATION-SETUP.md @@ -346,8 +346,16 @@ callers of this reusable verification workflow reuse the same dependency cache. Workspace crates and installed Cargo binaries are excluded. Every run still builds the Runner workspace and runs `check:all`, including the Rust and -TypeScript tests. Only a successful master-push run verifying that push's exact -SHA saves the shared cache. Manual candidate verification can restore it without -saving. A miss or eviction costs compilation time but does not change the checks. +TypeScript tests. Only an own-repository master-push run verifying that push's exact +SHA can restore the cache, and only a successful run saves it. PR, tag, and +manual candidate verification compile without this cache. A miss or eviction costs compilation time but does not change the checks. To discard old dependency caches, increment the shared-key version and let the next successful master verification warm it again. + +The trust boundary is the protected master branch, not the cache-key text. +GitHub does not let master restore caches created by a child branch, sibling +branch, tag, or PR merge ref. Both permitted restore scopes (current branch and +default branch) are master here. A workflow with authority to execute arbitrary +code on master can affect verification directly and is already trusted. The +cache contains dependency build artifacts, not credentials or workspace output. +See [GitHub cache access restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). From 5b0417c88ca657ba105c8573f0c6e8772ef87496 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 10 Sep 2026 19:19:27 -0700 Subject: [PATCH 3/3] ci: align Rust cache comments with master-only access Co-Authored-By: Paperclip --- .github/workflows/release-verify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index 4a7b9e2e29..438649d30e 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -247,8 +247,8 @@ jobs: # dependencies; never restore installed executables from cargo/bin. cache-workspace-crates: false cache-bin: false - # Manual candidate refs may read the cache, but only a successful - # master push verifying its own commit can populate the shared cache. + # The step guard also restricts restores. Save only after a successful + # master-push verification of that push's exact commit. save-if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }} - name: Install dependencies