ci: cache Rust dependencies in release Runner verification (#13194)

Cache external Rust dependencies in trusted master release verification after selecting the package-owned toolchain. Keep source compilation and all validation unconditional; restrict both restore and save to the matching master push.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-09-10 20:10:36 -07:00 committed by GitHub
parent 3fd556b8f6
commit 9e970df4c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 0 deletions

View File

@ -0,0 +1,37 @@
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 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/);
});
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/);
});

View File

@ -227,6 +227,30 @@ 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
# 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
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
# 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
run: pnpm install --no-frozen-lockfile

View File

@ -335,3 +335,27 @@ 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 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).