Skip to content

ci: save the Nix store cache on the default branch only - #17

Merged
ryanleecode merged 11 commits into
mainfrom
ci/store-cache-main-only
Oct 12, 2026
Merged

ryanleecode merged 11 commits into
mainfrom
ci/store-cache-main-only

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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 main push, still restores the default branch's newest bundle through the nix-store-<system>- prefix. Pull request and other-branch entries, which no other run can restore, no longer evict main's bundle.

Plan: docs/plans/2026-10-12-1640-ci-store-cache-default-branch-only-plan.md (U1, U2, KTD1). It supersedes docs/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 literal refs/heads/main that the full-path review found against CONST-T12.

Change

.github/workflows/ci.yml, 2 lines: Export the unsigned store paths and Save the Nix store cache gain github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && in front of the existing cache-hit != 'true' test. No new action or tool, no pin change, no key or restore-key change, no new gate, and timeout-minutes stays at the uniform 60.

Docs: the superseding plan, and docs/solutions/performance-issues/pull-request-actions-cache-evicts-main-nix-store-bundle.md.

Commit What
c62cda5f9 U1: the two if: lines, literal refs/heads/main
1369559b0 solution doc
f291e2e3c superseding plan (R8, KTD1 derived branch, AE4/AE5 legacy entries, V5/V6, Q5)
8003e5557 U2: derive the default branch; solution doc follows
d87bd002c plain merge of origin/main a84c2cc03 (#18)
9b7af5ff9, 250a558f1 solution doc: the 2026-10-12 recurrence, and the observed restore/access times (review finding S1)
8fd382b94 plain merge of origin/main c82913fe5 (#19, the P0 fix)

Merge order and how this merges with #15 (PR6)

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 head 8003e5557 against merge base f552830c1. The run is 20261012-163614-b4461cce, persisted at /tmp/compound-engineering-0/ce-code-review/20261012-163614-b4461cce/. It holds review.json, metadata.json with cost.status: complete, stages.jsonl, every reviewer artifact, finish-input.json, synthesized-findings.json, validator-input.json, validator-verdicts.json and validator-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.

Reviewer Served model Result
correctness MiniMax-M3.1-Flash-Preview #4
security MiniMax-M3.1-Flash-Preview 0. The writer set shrinks to the default branch. The payload's default_branch is server-populated and cannot be chosen by a fork. If it is ever absent, the guard fails closed
testing MiniMax-M3.1-Flash-Preview #2
performance MiniMax-M3.1-Flash-Preview + deepseek-v4.1-flash 0 (residual risks only)
project-standards MiniMax-M3.1-Flash-Preview 0. Rule audit below
learnings-researcher deepseek-v4.1-flash pack inventory below
cross-model adversarial peer (adversarial-minimax) MiniMax-M3.1-Flash-Preview, 14/14 turns #1, S1, and a merge-push point folded into #1
validator batch (#1, #2) correctness-reviewer subagent rejected both

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.md or 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, path compound-packs. There are 41 top-level rule files, each pack's README.md excluded: 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.

Rule or law Applies My ruling and evidence
CONST-T12 (decide what applies from what it is, never its name) yes Violated at c62cda5f9, fixed by U2. The save condition named refs/heads/main while the restore fallback follows the default branch by what it is. ci.yml:96,107 now read format('refs/heads/{0}', github.event.repository.default_branch). The push: branches: [main] trigger stays literal because on: accepts no expressions (#4)
CONST-S4 (one source; delete the superseded) yes Complies. f291e2e3c deletes the 2026-10-11 plan instead of leaving it beside its replacement
CONST-S1, CONST-S2, CONST-W1, CONST-N1, CONST-B1 yes Comply, with line evidence in project-standards.json rule_audit
CONST-E7 (evidence before "done"; never edit the grader) yes Complies. Measurements below come from the jobs API, and no gate is edited
CONST-D1, D2, D4, B4, N3, T3, T8, T10 no No type, error, module or test is touched
boundary-testing/pin-dependency-semantics contested Raised to root, see R1 below
the other 40 pack rules no Every applies_when targets Effect/TypeScript cells, stores, schemas, packages or mutation config. This diff is one YAML guard and two Markdown docs

Findings and my rulings

# Finding (reviewer, severity, validator) Ruling
1 V3 ran on the superseded literal, so the head expression has no executed coverage. The merge push will not exercise the save branch either (adversarial-minimax, P1, validator rejected) Facts partly upheld, no code change. I disagree with the validator. The validator cites run 38278546996's skipped export/save. But that run and 38278904588 were exact hits (Cache hit for: …x86_64-linux-03664279…, …aarch64-linux-3f5e33c4…), so cache-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.ref is refs/pull/17/merge on every pull_request run, and no value of default_branch makes refs/heads/<x> equal it. The discriminating false-branch run under the same semantics is 38264981227 (prefix hit, cache-hit false, export/save skipped). The true branch is #2. The merge-push point now reads differently: #19 and #20 evicted every main entry by 17:04Z, so the next main push 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 here
2 The guard's true branch has no verification before or at merge. Suggests an unconditional step echoing GITHUB_REF, default_branch and the guard value (testing, P1, validator rejected) Dismissed with terminal evidence, no change. The operand resolves to main on a push. Org monorepo run 38114462675 (push, main), job 114396520300, log line 368, shows BRANCH: main, and the source at that commit (reusable-mutation.yml:100 @ 02bf9653b) is BRANCH: ${{ github.event.repository.default_branch }}. So format('refs/heads/{0}', 'main') == 'refs/heads/main' == github.ref on a push to main. V5 checks the result after merge: Save the Nix store cache concludes success and the caches API shows a refs/heads/main entry for that run's key. The echo step would be new CI behaviour outside a cache-policy change
4 R8's rename guarantee does not hold, because on: push: branches: [main] stays literal and a rename "silently stops every save with all jobs green" (correctness, P2, not selected) Rejected with evidence, no change. R8 promises only that a rename cannot leave the save condition behind. The finding's failure mode is wrong: a rename that misses the trigger leaves pushes to the new default branch with no CI run at all. That is visible on every push, not a green job. on.push.branches takes no expressions, so a literal is the only form there. Dropping the filter would run CI on every branch push, which is out of scope
S1 "A pull request restore counts as an access" is an unsourced platform claim (adversarial-minimax, P2, confidence 50, suppressed) Accepted, fixed in 9b7af5ff9/250a558f1. The doc now cites the observation: #17's PR jobs restored main's entries at 16:37:36Z and 16:38:12Z, and the caches API then reported last_accessed_at 16:39:05Z and 16:38:59Z
R1 boundary-testing/pin-dependency-semantics: GitHub's cache scoping, search order, prefix match and eviction are pinned in prose, not a differential test (project-standards, P2, earlier full pass on 1369559b0; the pass on 8003e5557 marks it n/a) Brought to root: both positions. Rule applies: applies_when names "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's
P1 Footprint model falsified: 4 GB non-main entries stay resident (adversarial-minimax, P2, earlier pass) Accepted, fixed in the superseding plan. KTD3, AE4, AE5 and Q5 now model the legacy entries, and the solution doc's "Why This Works" names them
P2 Guard's permit branch has no post-change observation (adversarial-minimax, P2, earlier pass) Same as #2. V5 now requires Save = success plus a matching refs/heads/main key
P3 V5 cannot detect the failure it protects against (adversarial-minimax, P2, earlier pass) Accepted, fixed in the plan: V5 and V6 now assert the save step's conclusion, the key-matched refs/heads/main entry and the non-default refs
P4 The release lane's dispatch pins a stale self-ref entry (adversarial-minimax, P3, earlier pass) Accepted, fixed in the plan: AE5 and the design table's workflow_dispatch row. The entry has since been evicted (caches API, 17:04Z)
P5 The cache save gate keys off the literal refs/heads/main (project-standards, P2, earlier pass) Accepted, fixed by U2 (8003e5557)
P6 The guard is pinned in prose only; suggests a workflow-YAML check (project-standards, P2, earlier pass) Rejected for this PR. That is a new CI gate, so it needs operator sign-off (GATE1), and a string match over YAML is a file-content test the repo's test admission bar bans (CHK1/OP12). Today's detection is the review grep and the caches-by-ref query in the solution doc

The first full pass on 1369559b0 (run 20261012-154404-f011e763) also returned 4 adversarial findings. They were ruled in this PR's earlier body: the weekly schedule: 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 in 8003e5557).

Run f011e763 is 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 run 20261012-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-sigs trusts 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 what main'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_at of the Nix · <system> job from GET /repos/systemfsoftware/gritlint/actions/runs/{id}/jobs. Restore and save keys come from the job logs, and cache facts from GET /repos/systemfsoftware/gritlint/actions/caches. No pull request job exceeded 10 minutes.

Per-job wall time

Run Kind Restored Export and save x86_64 aarch64
38149555075 #16 pull request, before, cold nothing ran (x86_64 35 + 9 s) 592 s 282 s
38264981227 this PR at c62cda5f9, warm, first run main's 5e296917c generation by prefix (own key missed, cache-hit false) skipped by the guard 316 s 182 s
38266407567 this PR at 1369559b0 exact key on refs/heads/main skipped 135 s 98 s
38278546996 this PR at 8003e5557 (derived guard) exact key 03664279… / 3f5e33c4… on refs/heads/main skipped (exact hit) 158 s 105 s
38278904588 this PR at d87bd002c exact key, as above skipped (exact hit) 161 s 112 s
38262818822 main push of #15, cold nothing (Cache not found) ran (x86_64 74 + 13 s) 893 s 439 s
38262833748 main push of #16, warm previous main generation by prefix ran (x86_64 68 + 11 s) 404 s 249 s
38288658641 main push of #19 (c82913fe5), cold nothing (Cache not found; #19's pull request had saved these exact keys on refs/pull/19/merge) ran: saved …x86_64-linux-76f3d330… and …aarch64-linux-d2f0959f… 724 s 414 s
38291526750 this PR at 8fd382b94, on repaired main exact key 76f3d330… / d2f0959f… on refs/heads/main skipped 164 s 102 s

The main rows ran under the old rule, which this PR keeps for main. The cold main x86_64 jobs took 893 s (14m53s, #15) and 724 s (12m04s, #19). Those are main jobs, 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, and main could not read them.

#16's 9m52s x86_64 case, before and after

This PR's first run computed the same keys main saved for #16's merge commit, so it is #16's change on a warm start.

x86_64 aarch64
Before, #16's own pull request run (38149555075) 592 s (9m52s) 282 s
After, the same outputs restoring main (38264981227) 316 s (5m16s) 182 s
Change -276 s (-47%) -100 s (-35%)

Cache sizes, hit or miss, and the live recurrence

One main generation is 4.00 GB: 2,356,998,235 B x86_64 plus 1,642,660,769 B aarch64 (03664279… / 3f5e33c4…).

Time (2026-10-12) Entries Total By ref
14:57Z 6 10,545,809,872 B 4 refs/heads/main, 2 legacy refs/pull/16/merge. No refs/pull/17/merge entry
~16:35Z 6 11,978,096,548 B 3 refs/heads/main, 1 refs/heads/changeset-release/main (release dispatch 38266314981), 2 refs/pull/19/merge
~17:05Z 5 ~9.69 GB 0 refs/heads/main: 3 refs/pull/19/merge, 2 refs/pull/20/merge

Across every snapshot, no refs/pull/17/merge entry has ever appeared, although this PR's CI ran 7 times (c62cda5f9 through 8fd382b94); that is V3 for R3 and R4. Meanwhile #19 and #20, still on the old rule, evicted every main entry within about 20 minutes. That is the failure this PR removes, and it happened on main today. When this lands, open pull requests stop saving on their next run, because pull_request runs 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 on refs/pull/19/merge (#19 is merged). There is still no refs/pull/17/merge entry.

Status

The #19 blocker is resolved. On main c82913fe5, CI, Release and Mutants all concluded success. On head 8fd382b94, CI 38291526750 and Changeset Check 38291527129 are green. Base main at c82913fe5; GitHub reports MERGEABLE, with merge state BLOCKED pending the required review.

Earlier red runs on this branch (38282139114 on 9b7af5ff9, 38284073865 on 250a558f1) were main's P0: with main's bundle evicted, cold builds hit #18's stale cargo-deps-vendor-staging fixed-output hash, which #19 fixed.

After merge, V5 and V6 come from the first main push whose key misses. The 4 legacy refs/pull/19/merge entries need gh cache delete, which needs actions: write (plan Q5).

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.
#15 (5e29691) and #16 (f552830) merged first. Update the renamed
standalone plan path, the merged-in base, the merge-order questions,
the ci.yml line references of the two conditioned steps, and the
V1/V2/V4 sources, which main pushes made before this PR now supply.
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.
@systemfsoftware-maker systemfsoftware-maker changed the title ci/store cache main only ci: save the Nix store cache on main only Oct 12, 2026
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.
@systemfsoftware-maker systemfsoftware-maker changed the title ci: save the Nix store cache on main only ci: save the Nix store cache on the default branch only Oct 12, 2026

@ryanleecode ryanleecode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ryanleecode
ryanleecode merged commit 0e2e07b into main Oct 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants