Repository navigation
ci: save the Nix store cache on the default branch only - #17
Merged
Merged
Conversation
Pull request runs keep restoring by the nix-store-<system>- prefix, which reaches main, and stop exporting and saving. Plan only; U1 is a two-line if: change to .github/workflows/ci.yml. Reviewed report-only (ce-doc-review, 4 reviewers); findings are ruled in the hand-off, not applied.
A pull request's cache is scoped to its merge ref, so no other run could restore it, yet each one took up to 4 GB and evicted main's bundle (10.5 GB in 6 PR-only entries on 2026-10-11). #16 then restored nothing and its x86_64 job took 592 s. `Export the unsigned store paths` and `Save the Nix store cache` now also require `github.ref == 'refs/heads/main'` (plan KTD1). Every run still restores by the `nix-store-<system>-` prefix, which reaches main's newest bundle; pull requests skip the export and the save. Nothing else changes: no action, pin, key or timeout-minutes.
Records why only main saves the store bundle: a pull request's Actions cache lives on refs/pull/N/merge, which no other run can restore, yet it counts against the 10 GB limit and evicted main's bundle. Includes the jobs API wall times before and after (#16 592 s, #17 316 s on x86_64), the alternatives rejected in the plan, and a review gate with the grep that lists every cache save step.
…e rule The full-path review of PR #17 found the shipped `refs/heads/main` literal against CONST-T12, and the release flow's workflow_dispatch on changeset-release/main as a second non-default writer (run 38266314981). U1 had executed, so a new dated plan carries the corrected contract: R8 and KTD1 derive the default branch from the event, KTD3 and AE4/AE5 account for legacy entries on other refs, V5/V6 take the first output-changing main push after merge, and Q5 asks the operator to delete the legacy entries.
`Export the unsigned store paths` and `Save the Nix store cache` compare `github.ref` with `refs/heads/<default_branch>` from the event payload instead of the literal `refs/heads/main`. The restore fallback already follows the default branch by what it is; a second, literal copy of that fact could be left behind by a rename, stopping every save while every job stays green (CONST-T12). Behaviour on main is unchanged. The solution doc states the derived condition, names the release flow's dispatch as a non-pull-request writer, and corrects the claim that the cache holds default-branch generations alone while legacy entries remain.
… refresh Review of PR #17 (cross-model peer) flagged that 'a pull request restore counts as an access' had no source. The caches API shows it: main's x86_64 entry's last_accessed_at moved to the minute #17's pull request job restored it. Symptoms also gain the live recurrence of 2026-10-12, when #19 and #20, still on the old rule, left no refs/heads/main entry.
The previous wording said last_accessed_at moved 'to the minute' a pull request job restored main's entry. The job logs put the restores at 16:37:36Z and 16:38:12Z and the caches API reported access at 16:39:05Z and 16:38:59Z, so the doc now states those times.
ryanleecode
approved these changes
Oct 12, 2026
ryanleecode
left a comment
Contributor
There was a problem hiding this comment.
Root verdict at 8fd382b: full-path receipt (5 packs, 41 rules; CONST-T12 violated at c62cda5 and fixed by U2) with a MiniMax-M3.1 cross-model peer (14/14 turns); the Grok-named run f011e763 excluded as unreviewed. 0 behind, 0 unresolved threads, all checks green, compound entry in docs/solutions. R1 (boundary-testing/pin-dependency-semantics): ruled not binding. The rule's mechanism pins a lockfile-versioned dependency behind an adapter with an upgrade tripwire, and GitHub's cache service is server-side and unversioned, so a change shows as a cold job caught by V5 and the caches-by-ref query. Findings #1, #2, #4 and P6 rulings accepted.
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Only runs on the repository's default branch export and save the Nix store bundle. Every run, whether a pull request, a branch dispatch or a
mainpush, still restores the default branch's newest bundle through thenix-store-<system>-prefix. Pull request and other-branch entries, which no other run can restore, no longer evictmain's bundle.Plan:
docs/plans/2026-10-12-1640-ci-store-cache-default-branch-only-plan.md(U1, U2, KTD1). It supersedesdocs/plans/2026-10-11-2237-ci-store-cache-main-only-plan.md, which is deleted in the same change, because U1 had executed with a literalrefs/heads/mainthat the full-path review found against CONST-T12.Change
.github/workflows/ci.yml, 2 lines:Export the unsigned store pathsandSave the Nix store cachegaingithub.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&in front of the existingcache-hit != 'true'test. No new action or tool, no pin change, no key or restore-key change, no new gate, andtimeout-minutesstays at the uniform 60.Docs: the superseding plan, and
docs/solutions/performance-issues/pull-request-actions-cache-evicts-main-nix-store-bundle.md.c62cda5f9if:lines, literalrefs/heads/main1369559b0f291e2e3c8003e5557d87bd002corigin/maina84c2cc03(#18)9b7af5ff9,250a558f18fd382b94origin/mainc82913fe5(#19, the P0 fix)Merge order and how this merges with #15 (PR6)
5e296917c) and test(core): only the first heading leaves a rule's message #16 (f552830c1) were plain-merged in2f8593903. feat(gritlint): npm-style tarball, release and changeset check #15 addedThe npm tarball rebuilds bit for bit, itsupload-artifactstep, and thetarballs-matchandchangeset-intentjobs. It did not touch the export or save steps; this PR changes only their twoif:lines.tarballs-matchreads therelease-*artifacts, not the cache.a84c2cc03, release versioning) was plain-merged ind87bd002c. It touches none of this PR's files. Before that merge,Changeset Checkon8003e5557failed with "moves 1 publishable package that no intent names: @systemfsoftware/gritlint", which is chore(release): version packages #18's version bump seen against the stale basef552830c1. Ond87bd002cit passed (run 38278905145).c82913fe5, the P0 vendoring fix) was plain-merged in8fd382b94, after itsmainpush was green on CI (38288658641), Release (38288659217) and Mutants (38288658657). The merge had no conflicts and changes none of this PR's files. The PR diff againstmainis byte-identical before and after it (git diff origin/main...HEADbefore,git diff origin/main HEADafter, both sha25618dff7b3…): the same 3 files, +259/−2.8003e5557. Since then the PR's own content changed only in the two solution-doc commits, which I reviewed. The chore(release): version packages #18 and fix(nix): vendor cargo deps with crane, independent of workspace versions #19 merges change nothing in the PR diff, so the receipt still stands.Review (full path)
ce-simplify-code: skipped, because the code diff is 2 lines.ce-code-review mode:agent base:origin/main, report-only, full path (coverage.depth: "full"), on head8003e5557against merge basef552830c1. The run is20261012-163614-b4461cce, persisted at/tmp/compound-engineering-0/ce-code-review/20261012-163614-b4461cce/. It holdsreview.json,metadata.jsonwithcost.status: complete,stages.jsonl, every reviewer artifact,finish-input.json,synthesized-findings.json,validator-input.json,validator-verdicts.jsonandvalidator-outcome.json. Nothing was auto-applied. After the review the branch took the #18 merge, which touches none of these files, and the two solution-doc commits (9b7af5ff9,250a558f1) answering finding S1. I reviewed that delta myself.Verdict as returned: Not ready. The receipt keeps one finding, #4 (P2, owner human), and the validator rejected #1 and #2. My ruling on all of them is below. My call: no finding requires a code change. What blocks the merge is #19, not this review.
Reviewers and served models
The coding and orchestrating model is Claude. Each served model below comes from that subagent's session transcript.
default_branchis server-populated and cannot be chosen by a fork. If it is ever absent, the guard fails closedadversarial-minimax)The cross-model pass ran on a non-Claude family. The peer received only the outcome brief, the trusted constraints, the diff and the evidence file, not
context.mdor any other reviewer's artifact. No reviewer or leaf timed out.Compound Packs and constitution
Resolver: no errors and no warnings. All five packs come from
https://github.com/systemfsoftware/systemfsoftware@02bf9653b1ca2b0a2951baf2743bb3f70d19fc77, pathcompound-packs. There are 41 top-level rule files, each pack'sREADME.mdexcluded: boundary-testing 6, cell-architecture 15, mutation-discipline 3, package-topology 7, schema-laws 10. The learnings researcher's header says 40, but its own per-pack tables give 41. A directory listing also gives 41.c62cda5f9, fixed by U2. The save condition namedrefs/heads/mainwhile the restore fallback follows the default branch by what it is.ci.yml:96,107now readformat('refs/heads/{0}', github.event.repository.default_branch). Thepush: branches: [main]trigger stays literal becauseon:accepts no expressions (#4)f291e2e3cdeletes the 2026-10-11 plan instead of leaving it beside its replacementproject-standards.jsonrule_auditapplies_whentargets Effect/TypeScript cells, stores, schemas, packages or mutation config. This diff is one YAML guard and two Markdown docsFindings and my rulings
skippedexport/save. But that run and 38278904588 were exact hits (Cache hit for: …x86_64-linux-03664279…,…aarch64-linux-3f5e33c4…), socache-hit == 'true'explains the skip whatever the guard says. In 38282139114 the skip follows a build failure. No head run discriminates the guard. On a pull request it needs no run:github.refisrefs/pull/17/mergeon everypull_requestrun, and no value ofdefault_branchmakesrefs/heads/<x>equal it. The discriminating false-branch run under the same semantics is 38264981227 (prefix hit,cache-hitfalse, export/save skipped). The true branch is #2. The merge-push point now reads differently: #19 and #20 evicted everymainentry by 17:04Z, so the nextmainpush misses its key and runs the save. V5 takes the first push after merge whose key misses. The plan is frozen after execution, so this is recorded hereGITHUB_REF,default_branchand the guard value (testing, P1, validator rejected)mainon a push. Org monorepo run 38114462675 (push,main), job 114396520300, log line 368, showsBRANCH: main, and the source at that commit (reusable-mutation.yml:100@02bf9653b) isBRANCH: ${{ github.event.repository.default_branch }}. Soformat('refs/heads/{0}', 'main') == 'refs/heads/main' == github.refon a push tomain. V5 checks the result after merge:Save the Nix store cacheconcludessuccessand the caches API shows arefs/heads/mainentry for that run's key. The echo step would be new CI behaviour outside a cache-policy changeon: push: branches: [main]stays literal and a rename "silently stops every save with all jobs green" (correctness, P2, not selected)on.push.branchestakes no expressions, so a literal is the only form there. Dropping the filter would run CI on every branch push, which is out of scope9b7af5ff9/250a558f1. The doc now cites the observation: #17's PR jobs restoredmain's entries at 16:37:36Z and 16:38:12Z, and the caches API then reportedlast_accessed_at16:39:05Z and 16:38:59Z1369559b0; the pass on8003e5557marks it n/a)applies_whennames "platform protocol" and "subtle vendor behaviors", and the restore fallback depends on search order. Rule does not bind: its mechanism pins a lockfile-versioned library behind a boundary adapter with an upgrade tripwire. GitHub's cache service is server-side and unversioned, so nothing in this repo moves when it changes. A change shows as a cold job, measured on every PR and logged as the restore key, not as a silent wrong result. The only executable pin would be a CI step querying the caches API, which is a new gate needing operator approval (GATE1). My position: it does not bind here, and the plan's Scope Boundaries record the reasoning. I made no change; the call is root'smainentries stay resident (adversarial-minimax, P2, earlier pass)Save=successplus a matchingrefs/heads/mainkeyrefs/heads/mainentry and the non-default refsworkflow_dispatchrow. The entry has since been evicted (caches API, 17:04Z)refs/heads/main(project-standards, P2, earlier pass)8003e5557)The first full pass on
1369559b0(run20261012-154404-f011e763) also returned 4 adversarial findings. They were ruled in this PR's earlier body: the weeklyschedule:refresh (rejected, a separate sweep), the merge push not exercising save (now #1), the post-save assertion gate (rejected, GATE1), and the legacy-entry sentence (rewritten in8003e5557).Run
f011e763is unreviewed under the house rule against Grok/XAI. Its receipt (finish-input.json,synthesized-findings.json) names Grok, listing it among the peer CLIs that were not installed. Its peer and standards artifacts (adversarial-minimax.json,project-standards.json) do not, and no Grok/XAI model served any work: the subagent transcripts show only MiniMax-M3.1-Flash-Preview and deepseek-v4.1-flash for reviewers, and claude-haiku-4-5 for the docs leaves. Its findings above (R1, P1-P6) are kept only as inputs that I ruled on. The review of record is run20261012-163614-b4461cce, which covered the full final code diff, with its cross-model peer on MiniMax-M3.1-Flash-Preview (14/14 turns). Neither that receipt nor its peer artifact names Grok/XAI, so it needs no rerun.Residual risks the receipt keeps, no change: the guard is duplicated on two steps;
nix copy --no-check-sigstrusts the cache, and this PR narrows the writers to the default branch; the plan rejects any automated reseed; and an output-changing pull request now rebuilds whatmain's bundle lacks on every push, bounded by the 486 s cold build. Testing gap: no admissible test exists for a workflow guard. The proof is V3 and V5.Verification
Every time below is
completed_at - started_atof theNix · <system>job fromGET /repos/systemfsoftware/gritlint/actions/runs/{id}/jobs. Restore and save keys come from the job logs, and cache facts fromGET /repos/systemfsoftware/gritlint/actions/caches. No pull request job exceeded 10 minutes.Per-job wall time
c62cda5f9, warm, first runmain's5e296917cgeneration by prefix (own key missed,cache-hitfalse)1369559b0refs/heads/main8003e5557(derived guard)03664279…/3f5e33c4…onrefs/heads/maind87bd002cmainpush of #15, coldCache not found)mainpush of #16, warmmaingeneration by prefixmainpush of #19 (c82913fe5), coldCache not found; #19's pull request had saved these exact keys onrefs/pull/19/merge)…x86_64-linux-76f3d330…and…aarch64-linux-d2f0959f…8fd382b94, on repairedmain76f3d330…/d2f0959f…onrefs/heads/mainThe
mainrows ran under the old rule, which this PR keeps formain. The coldmainx86_64 jobs took 893 s (14m53s, #15) and 724 s (12m04s, #19). Those aremainjobs, not pull request jobs, and nothing was cut. #19's cold push repeats #15's symptom: the pull request had saved the exact keys on its merge ref, andmaincould not read them.#16's 9m52s x86_64 case, before and after
This PR's first run computed the same keys
mainsaved for #16's merge commit, so it is #16's change on a warm start.main(38264981227)Cache sizes, hit or miss, and the live recurrence
One
maingeneration is 4.00 GB: 2,356,998,235 B x86_64 plus 1,642,660,769 B aarch64 (03664279…/3f5e33c4…).refs/heads/main, 2 legacyrefs/pull/16/merge. Norefs/pull/17/mergeentryrefs/heads/main, 1refs/heads/changeset-release/main(release dispatch 38266314981), 2refs/pull/19/mergerefs/heads/main: 3refs/pull/19/merge, 2refs/pull/20/mergeAcross every snapshot, no
refs/pull/17/mergeentry has ever appeared, although this PR's CI ran 7 times (c62cda5f9through8fd382b94); that is V3 for R3 and R4. Meanwhile #19 and #20, still on the old rule, evicted everymainentry within about 20 minutes. That is the failure this PR removes, and it happened onmaintoday. When this lands, open pull requests stop saving on their next run, becausepull_requestruns use the workflow from the merge commit.At about 18:42Z the caches API held 6 entries totalling 11,865,428,877 B:
main's new generation (76f3d330…2,346,134,253 B,d2f0959f…1,609,193,277 B), last accessed by this PR's run at 18:38:27Z and 18:33:01Z, and 4 legacy entries onrefs/pull/19/merge(#19 is merged). There is still norefs/pull/17/mergeentry.Status
The #19 blocker is resolved. On
mainc82913fe5, CI, Release and Mutants all concludedsuccess. On head8fd382b94, CI 38291526750 and Changeset Check 38291527129 are green. Basemainatc82913fe5; GitHub reportsMERGEABLE, with merge stateBLOCKEDpending the required review.Earlier red runs on this branch (38282139114 on
9b7af5ff9, 38284073865 on250a558f1) weremain's P0: withmain's bundle evicted, cold builds hit #18's stalecargo-deps-vendor-stagingfixed-output hash, which #19 fixed.After merge, V5 and V6 come from the first
mainpush whose key misses. The 4 legacyrefs/pull/19/mergeentries needgh cache delete, which needsactions: write(plan Q5).