From a23ae894a5a267c3917b6f5201cf2aea5245fe80 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 11 Sep 2026 13:15:06 -0700 Subject: [PATCH] ci: cache Rust dependencies used by post-merge typecheck (#13259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Cloud images become deployable only after source verification passes. > - The typecheck job builds the native Runner binary through the server package. > - Fresh runners repeatedly compile Rust dependencies for that binary. > - This PR caches those dependencies for exact-source master verification. > - Workspace code and every typecheck still rebuild or run as before. ## Linked Issues or Issue Description Refs #13243 and #13257. **What existing behavior does this improve?** The typecheck portion of post-merge cloud source verification. **Current behavior** The typecheck job has no Rust dependency cache. An observed release build in this job took 4m 13s, including dependency compilation. **Proposed behavior** Restore dependency build outputs for canonical master pushes with the exact source SHA. Use a separate cache key from the Runner verification job, which builds other profiles. **Reason and benefit** A warm cache should remove roughly 2–3 minutes of dependency compilation from this job. Overall deployment gains depend on the remaining critical path. The first cache population still compiles from scratch. **Breaking changes** None. All checks remain enabled. Non-master callers compile without restoring or saving this cache. ## What Changed - Select the pinned Rust toolchain before the typecheck cache lookup. - Reuse the existing pinned Rust cache action with a typecheck-specific key. - Exclude workspace crates and installed cargo executables. - Test restore/save trust boundaries and document cache behavior. ## Verification - All workflow script tests pass locally, including nine new cache trust/contract cases. - actionlint passes for release-verify.yml. - Full local typecheck and build pass on the same source base (167s and 206s); `pnpm test:run` is still running and is recorded with #13257. This PR changes only the workflow, cache guard tests, and documentation. - All 32 current-head checks are successful or intentionally skipped, including the complete Linux test matrix, build, and Greptile 5/5 with no unresolved findings. Verify cache population and subsequent restore on actual master runs. ## Risks - The first run and any toolchain/dependency invalidation compile from scratch. - Cache restore/save overhead reduces the benefit for small dependency graphs. - Disable the cache step to roll back; the existing uncached build remains valid. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, and code execution. Exact serving model ID and context window are not exposed by this environment. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass — focused change tests pass; full-suite local permission failures are disclosed above - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- .../tests/typecheck-rust-cache.test.mjs | 38 +++++++++++++++++++ .github/workflows/release-verify.yml | 24 ++++++++++++ doc/cloud-build-readiness.md | 11 ++++++ 3 files changed, 73 insertions(+) create mode 100644 .github/scripts/tests/typecheck-rust-cache.test.mjs 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.