mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
d08abcba15233de375a655d21d743022d7842e94
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd51f157e9 |
fix(ci): make the Runner Rust cache key independent of the image toolchain (#13500)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Every change goes through the pull request CI workflow, and its `Verify Paperclip Runner` lane builds and tests the native Rust runner > - https://github.com/paperclipai/paperclip/pull/13457 made that lane restore master's prebuilt Rust dependency cache instead of recompiling 313 crates > - Measured over 26 runs since it went live, the cache hits on the RunsOn fleet and misses on every GitHub-hosted runner > - The cause is the cache key: `rust-cache` hashes every installed toolchain, and each runner image ships a different stable Rust next to the pinned one > - This pull request removes the extra toolchains before the key is computed, in the reader and the writer > - The benefit is that the saving the cache already delivers, 4.8 minutes per run, reaches the 73% of runs that currently miss it ## Linked Issues or Issue Description No public GitHub issue exists for this. The problem follows the enhancement issue template below. - Refs https://github.com/paperclipai/paperclip/pull/13457 — added the cache this pull request repairs - Refs https://github.com/paperclipai/paperclip/pull/13459 — activated it - Refs https://github.com/paperclipai/paperclip/pull/13194 — created the `release-runner-v1` entry on master I searched this repository for other pull requests touching this cache and found no duplicate and nothing in flight. **What existing behavior does this improve?** The Rust dependency cache added in #13457 misses on GitHub-hosted runners, so most pull requests still recompile the whole dependency tree. **Subsystem affected** Cross-cutting (multiple of the above). The change touches CI workflow configuration only. It does not change product code. **Current behavior** The cache works, but only on one runner class. Across 26 successful `Verify Paperclip Runner` jobs since #13459 merged: | Runner | Runs | Before | After | Change | Cache | |---|---|---|---|---|---| | RunsOn fleet | 7 | 16.4m | 11.6m | −4.8m | 4 of 4 full hit | | ubuntu-latest | 19 | 14.9m | 14.7m | −0.2m | 0 of 6 hit | | All | 26 | 15.0m | 13.9m | −1.1m | | Only 27% of runs reach the fleet, so the fleet-wide saving is 1.1 minutes rather than the 4.8 minutes the cache delivers where it lands. The two runners compute different keys: ``` fleet: v0-rust-release-runner-v1-Linux-x64-4bb3b8ea-a95b0328 ubuntu-latest: v0-rust-release-runner-v1-Linux-x64-9fdc73e3-a95b0328 ``` The lockfile half agrees. The environment half does not. `rust-cache` logs why, under `Environment considered`: | Runner | Toolchains it found | |---|---| | fleet | 1.97.1 and **1.98.0** | | ubuntu-latest | 1.97.1 and **1.98.1** | `rust-cache` hashes every installed toolchain, not only the active one. Both images carry the pinned 1.97.1. Each also ships its own stable Rust, and those differ by a patch version. The post-merge writer runs on a RunsOn image, so the fleet agrees with it and GitHub-hosted runners cannot. Pinning `RUSTUP_TOOLCHAIN` in #13457 was necessary but not sufficient. It fixes which toolchain builds the code. It does not change which toolchains exist on the image. **Proposed behavior** Remove every toolchain except the pin, before the cache step, in both the reader and the `release-runner-v1` writer. The key then depends on the pinned compiler and the lockfile alone, not on what the image happens to carry. **Reason and benefit** The cache already proves its value where it lands: release compile drops from 5m07s to 1m30s, debug from 1m48s to 13s, and the job from 16.4m to 11.6m. This change extends that to the other 73% of runs. Expected fleet-wide mean: about 11.5m, against 13.9m today and 15.0m before #13457. It also removes a standing fragility. The fleet hits today only because two RunsOn images happen to agree. If either image updates its stable Rust on its own, the hit rate drops to zero with no code change. **Breaking changes** None. The change only affects cache key computation. A miss reproduces the current behavior. **Additional context** The typecheck writer in `release-verify.yml` keeps its current step on purpose. It restores and saves on the same post-merge image, so its key never disagrees with itself. ## What Changed - Added a toolchain normalization block to `Select the pinned Runner Rust toolchain` in `.github/workflows/pr-trusted.yml`, before the cache restore. It keeps the pinned toolchain and uninstalls the rest. - Added the identical block to the same step in the `verify_paperclip_runner` job of `.github/workflows/release-verify.yml`, which writes `release-runner-v1`. The reader and the writer must agree, or the key matches nothing. - Made the block tolerant. If a toolchain cannot be removed it prints a notice and continues, so a pull request loses the cache rather than the run. - Extended `.github/scripts/tests/pr-runner-rust-cache.test.mjs` with two tests: the block is byte-identical in both workflows, and it runs before the cache step in each. ## Verification Run the workflow shape tests: ```bash node --test '.github/scripts/tests/*.test.mjs' ./scripts/__tests__/e2e-shard.test.mjs ./scripts/__tests__/release-verify-workflow.test.mjs ./scripts/__tests__/run-vitest-stable-shard.test.mjs ./scripts/cloud-source-verification.test.mjs ``` Result: 471 pass, 0 fail. I ran the step body against a stub `rustup` to confirm the logic, rather than only checking syntax. Three paths, all exit 0: | Case | Result | |---|---| | Pin plus an extra stable toolchain | Uninstalls only the extra, exports `RUSTUP_TOOLCHAIN=1.97.1-x86_64-unknown-linux-gnu` | | Pin only, already normalized | No uninstall calls, no error | | No `rustup` on `PATH` | Prints the notice and continues | I also mutation-tested the new parity assertion. Each mutation edits only the writer, then the reader and writer disagree: | Mutation to `release-verify.yml` | Result | |---|---| | One word changed in the shared comment | Caught | | `uninstall` changed to `remove` | Caught | | Trailing `rustup toolchain list` deleted | Caught | After this merges, confirm the repair in CI. Take a `Verify Paperclip Runner` job that ran on `ubuntu-latest` and check the restore step for `full match: true`. Under `Environment considered`, `Rust Versions` must list only 1.97.1. The job should finish near 11.5m rather than 14.7m. ## Risks - **Master must republish the cache once.** This changes the key, so the existing `release-runner-v1` entry no longer matches. `cloud-readiness.yml` runs `release-verify.yml` on every push to master, so the first push after this lands writes the new entry. Pull requests merged before that miss the cache, which is exactly what most of them do today. No run breaks. - **The reader and the writer must stay in step.** If they diverge, no run hits the cache. The new parity test fails on any difference in the block, including a comment. - **Uninstalling the image toolchain is deliberate.** Everything in this job builds through the pinned 1.97.1, resolved from `packages/paperclip-runner/rust-toolchain.toml` and `RUSTUP_TOOLCHAIN`. Nothing in the job uses the image default. - **Low blast radius.** The change only affects cache key inputs. A miss compiles from scratch, as today. - **Image drift is now handled.** The key no longer depends on the image's own Rust version, so a future image update cannot silently disable the cache. ## Model Used Claude Opus 5, provider Anthropic, exact model ID `claude-opus-5`, 1M context window. Adaptive thinking was on. I used tool use throughout: the `gh` CLI and the GitHub API to pull 26 post-merge job records and 10 full job logs, log parsing in Python to isolate the per-phase timings and the two cache keys, a stub `rustup` on `PATH` to exercise the new step, and local `node --test` runs to verify and mutation-test the guards. Run through Claude Code. ## 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 - [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 Note on the unchecked boxes. The CI and Greptile boxes stay unchecked until those checks finish. On documentation: no document describes the Runner cache keys, so there is nothing to update. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f97a3f886e |
perf(ci): restore master's Rust dependency cache on the PR runner lane (#13457)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Every change to Paperclip goes through the pull request CI workflow before it merges > - One lane of that workflow, `Verify Paperclip Runner`, builds and tests the native Rust runner > - That lane compiles all 313 third-party crates from scratch on every pull request, in two profiles > - Master already builds and stores exactly those compiled crates, but the pull request lane never reads them > - This pull request restores that existing cache on the pull request lane, read-only > - The benefit is about 3.9 minutes less compute per run, or about 4.7 compute-hours each day, with no change to what CI checks ## Linked Issues or Issue Description No public GitHub issue exists for this. The problem follows the enhancement issue template below. Related merged pull requests, found by searching this repository: - Refs https://github.com/paperclipai/paperclip/pull/13194 — created the `release-runner-v1` cache that this pull request reads - Refs https://github.com/paperclipai/paperclip/pull/13300 — established the restore-only dependency cache pattern that this pull request follows - Refs https://github.com/paperclipai/paperclip/pull/13326 — split the post-merge runner job into the `protocol` and `rust` lanes used as the warm-cache baseline below - Refs https://github.com/paperclipai/paperclip/pull/13259 — applied the same caching idea to the post-merge typecheck job I found no open pull request that duplicates this work. **What existing behavior does this improve?** The `Verify Paperclip Runner` job in `.github/workflows/pr-trusted.yml` recompiles the full Rust dependency tree on every pull request. **Subsystem affected** Cross-cutting (multiple of the above). The change touches CI workflow configuration only. It does not change product code. **Current behavior** The job has no Rust cache. Its only cache step restores the pnpm store. Each run therefore downloads about 280 crates and compiles all 313 third-party crates twice, once in the `dev` profile and once in the `release` profile. Measured over 12 successful runs on 2026-09-15, the job takes 15.0 minutes on average. The range is 12.1 to 17.4 minutes. | Phase | Mean | Share | |---|---|---| | `check:eval-kernel` | 0.0m | — | | `typecheck:typescript` and protocol manifest | 0.1m | 1% | | `build:rust` — cargo `dev` profile | 1.8m | 12% | | TypeScript tests (`node --test` and vitest, 143 files) | 5.8m | 39% | | `check:replay-goldens` | 0.1m | 1% | | `typecheck:rust` — `cargo fmt` and `cargo check` | 0.9m | 6% | | `test:rust` compile — cargo `release` profile | 4.9m | 33% | | `test:rust` run | 0.8m | 5% | | Parity checks | 0.0m | — | | `check:api-authority` | 0.5m | 3% | Cargo reports the two compiles directly. The `dev` profile takes 1m24s to 1m53s. The `release` profile takes 3m54s to 5m36s. **Proposed behavior** The job restores master's existing Rust dependency cache before it runs the checks. Master already writes this cache. The post-merge `rust` lane in `.github/workflows/release-verify.yml` writes `release-runner-v1`. It also warms both profiles into that entry. The entry is 677MB and lives on `refs/heads/master`. Pull request branches are allowed to read caches from the default branch. The pull request lane restores that entry read-only. Master stays the only writer. A pull request never saves a branch-scoped copy. A pull request never evicts the shared entry. This matches the rule the pnpm store in the same file already follows. **Reason and benefit** Master runs these same checks with a warm cache. That gives a direct measurement of the saving. | Check set | Cold (pull request today) | Warm (master) | Change | |---|---|---|---| | `check:runner` and `check:api-authority` | 7.1m | 3.4m | −3.7m | | `check:eval-kernel` and `check:protocol` | 7.8m | 7.3m | −0.5m | | Cache restore step | — | 20–21s | +0.35m | The net saving is about 3.9 minutes per run, or about 26%. About 73 runs execute this job each day. The daily saving is therefore about 4.7 compute-hours. The protocol side changes very little. Vitest dominates that side, not compilation. The cache should almost always hit. `packages/paperclip-runner/runner/Cargo.lock` changed in 7 of the last 1697 commits on master. A miss costs nothing more than today's behavior. **Breaking changes** None. The change adds two steps to one CI job. It does not change any check, any test, or any product code. **Additional context** No documentation covers pull request CI caching, so this pull request updates no documents. ## What Changed - Added a `Select the pinned Runner Rust toolchain` step to the `verify_paperclip_runner` job in `.github/workflows/pr-trusted.yml`. The step exports `RUSTUP_TOOLCHAIN`. `rust-cache` hashes `rustc -vV` into the cache key, so the key needs the pinned compiler. This lane had no rustup step before, so `rustc` resolved to each runner image's default instead of the pinned 1.97.1. - Made that step tolerate a missing `rustup`. `release-verify.yml` runs on one post-merge fleet image. The gate in this workflow routes to either `ubuntu-latest` or the public pull request fleet. A missing `rustup` now costs the cache. It does not fail the pull request. - Added a `Restore Runner Rust dependencies (read only)` step that uses `Swatinem/rust-cache` with `save-if: false`. - Mirrored every cache key input from the master writer in `release-verify.yml`: the same action SHA, `workspaces`, `shared-key`, `cache-workspace-crates`, and `cache-bin`. Neither side sets `prefix-key`. Any drift causes a silent miss and a full recompile. - Added `.github/scripts/tests/pr-runner-rust-cache.test.mjs`. It asserts key-input parity across the two workflow files, the step order, and the restore-only contract. ## Verification Run the workflow shape tests: ```bash node --test '.github/scripts/tests/*.test.mjs' ``` Result: 413 pass, 0 fail. I also mutation-tested the new assertions. I applied each mutation to the workflow, ran the new test file, then restored the file. All 7 mutations fail the suite: | Mutation | Result | |---|---| | `shared-key` changed to `pr-runner-v1` | Caught | | `save-if` changed to `true` | Caught | | `cache-bin` changed to `true` | Caught | | `rustup show active-toolchain` line deleted | Caught | | `RUSTUP_TOOLCHAIN` export line deleted | Caught | | `working-directory` line deleted | Caught | | `command -v rustup` guard deleted | Caught | To confirm the cache key inputs match the master writer, parse both workflows and compare: ```bash ruby -ryaml -e 'pr=YAML.safe_load(File.read(".github/workflows/pr-trusted.yml"), aliases: true); rv=YAML.safe_load(File.read(".github/workflows/release-verify.yml"), aliases: true); a=pr["jobs"]["verify_paperclip_runner"]["steps"].find{|s| s["uses"].to_s.include?("rust-cache")}; b=rv["jobs"]["verify_paperclip_runner"]["steps"].find{|s| s["uses"].to_s.include?("rust-cache")}; puts a["uses"]==b["uses"]; %w[workspaces shared-key cache-workspace-crates cache-bin].each{|k| puts "#{k}: #{a["with"][k]==b["with"][k]}"}' ``` Every line prints `true`. After the pin advances (see Risks), confirm the cache works in CI. The restore step log must show `Cache restored from key: v0-rust-release-runner-v1-Linux-x64-...`. Cargo must stop printing `Compiling` lines for third-party crates. The job should drop from about 15.0 minutes to about 11.0 minutes. ## Risks Low risk overall. A cache miss produces exactly today's behavior, so the worst case is no improvement. - **This change does nothing until a second pull request lands.** `.github/workflows/pr.yml` pins this reusable workflow by SHA. The file header describes this two-step rollout. A follow-up pull request must bump that pin. That follow-up validates itself, because GitHub uses the pull request's own `pr.yml` for `pull_request` events. - **A key mismatch would silently remove the benefit.** The runner images here may ship a different default `rustc` than the post-merge fleet. The toolchain step pins the compiler to prevent this. The new test guards the remaining key inputs. If the first runs still miss, compare `rustup show` output against a master run. - **A missing `rustup` degrades quietly.** The step prints a GitHub notice and continues. CI stays green and the run is simply uncached. - **No cache poisoning path.** Pull requests only read. `save-if: false` stops any write. GitHub also isolates pull request cache writes from the default branch. - **The cache entry can expire.** GitHub evicts unused entries after 7 days and enforces a repository size limit. Master pushes are frequent, so the entry should stay warm. Eviction only causes a miss. ## Model Used Claude Opus 5, provider Anthropic, exact model ID `claude-opus-5`, 1M context window. Adaptive thinking was on. I used tool use throughout: the `gh` CLI to read 12 job logs and step timings from recent CI runs, the GitHub Actions cache API to read cache entry keys and sizes, `git log` to measure `Cargo.lock` churn, and local `node --test` runs to verify and mutation-test the change. Run through Claude Code. ## 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) - [ ] 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 - [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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Note on the unchecked boxes. The branch name `claude/paperclip-runner-conditional-81a300` carries a tool-generated suffix, so it does not meet the branch naming rule. I can rename it if you want. The CI and Greptile boxes stay unchecked until those checks finish on this pull request. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |