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.