diff --git a/.github/scripts/tests/typecheck-rust-cache.test.mjs b/.github/scripts/tests/typecheck-rust-cache.test.mjs new file mode 100644 index 0000000000..c8ff43b880 --- /dev/null +++ b/.github/scripts/tests/typecheck-rust-cache.test.mjs @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { runInNewContext } from "node:vm"; + +const workflow = readFileSync(new URL("../../workflows/release-verify.yml", import.meta.url), "utf8"); +const typecheck = workflow.split(" typecheck:\n")[1].split(" general_tests:\n")[0]; +const cache = typecheck.split(" - name: Cache typecheck Rust dependencies\n")[1].split(" - name: Validate release package manifest")[0]; +const sha = "a".repeat(40); +const github = { repository: "paperclipai/paperclip", event_name: "push", ref: "refs/heads/master", sha }; +for (const [name, overrides, ref, allowed] of [ + ["exact master push", {}, sha, true], + ["PR", { event_name: "pull_request", ref: "refs/pull/1/merge" }, sha, false], + ["privileged PR", { event_name: "pull_request_target" }, sha, false], + ["fork", { repository: "someone/paperclip" }, sha, false], + ["branch", { ref: "refs/heads/feature" }, sha, false], + ["manual source", { event_name: "workflow_dispatch" }, sha, false], + ["unmerged source", {}, "b".repeat(40), false], + ["moving ref", {}, "master", false], +]) { + test(`typecheck cache restore and save: ${name}`, () => { + for (const field of ["if", "save-if"]) { + const expr = cache.match(new RegExp(`^ +${field}: \\$\\{\\{ (.+) \\}\\}$`, "m"))?.[1]; + assert.ok(expr); + assert.equal(runInNewContext(expr, { github: { ...github, ...overrides }, inputs: { ref } }), allowed); + } + }); +} +test("cache excludes workspace code and executable installs, and preserves full checks", () => { + assert.match(cache, /uses: Swatinem\/rust-cache@[a-f0-9]{40}/); + assert.match(cache, /workspaces: packages\/paperclip-runner\/runner -> target/); + assert.match(cache, /shared-key: release-typecheck-v1/); + assert.match(cache, /cache-workspace-crates: false/); + assert.match(cache, /cache-bin: false/); + assert.ok(typecheck.indexOf('echo "RUSTUP_TOOLCHAIN=$toolchain"') < typecheck.indexOf("uses: Swatinem/rust-cache")); + assert.match(typecheck, /run: pnpm -r typecheck/); + assert.match(workflow, /shared-key: release-runner-v1/); +}); diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index cf6dba8319..72f91a3f8a 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -39,6 +39,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 typecheck 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-typecheck-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: Validate release package manifest run: node ./scripts/release-package-map.mjs check diff --git a/doc/cloud-build-readiness.md b/doc/cloud-build-readiness.md index 9465a0e5cf..7af553e247 100644 --- a/doc/cloud-build-readiness.md +++ b/doc/cloud-build-readiness.md @@ -145,3 +145,14 @@ workflow. Changing the variable does not migrate an already assigned job. Check the Actions job's runner name and runner group to verify placement. Record queue time, image verification completion, and `Cloud deployable v1` separately; source verification and the migrator still run on GitHub-hosted runners. + + +### Typecheck Rust dependency cache + +Source verification's typecheck job builds the native Runner binary through the +server's `prepare:runner-vendor` command. It restores and saves compiled Rust +dependencies only for canonical master pushes that verify the event's exact SHA. +The `release-typecheck-v1` cache is separate from Runner verification because +those jobs compile different profiles. The pinned toolchain is selected before +cache lookup. Workspace crates and installed cargo binaries are excluded, and +all typechecks still execute. A missing or invalidated cache triggers compilation.