Repository navigation
ci: bound the seven remaining cache saves so bookkeeping cannot void a verdict - #7269
Merged
Merged
Conversation
Two of the seven combined `actions/cache@v6` uses objectui#7048 measured live in this file, split per PR #7047's route: `actions/cache/restore@v6` in place and unbounded, `actions/cache/save@v6` last, bounded and `continue-on-error`. The save phase of the combined action is a step the runner generates at job end and no workflow key reaches it, so an upload stall runs the job into its ceiling and reports `cancelled` — which the merge queue cannot tell from `failure`. Each bound is derived from that site's own measurement, never inherited: - `e2e` / Playwright browsers (~269 MB), ceiling 30 min -> 8 minutes. A save is only observable after a version bump, so the bound is sized off measured restores of the same archive (3s here on jobs 100096569775 and 100094597323, 5s in live-e2e.yml job 100097466897) with a deliberately pessimistic 20x upload penalty: ~1m40s honest save, bound 4.8x that, demanding 0.56 MB/s sustained. 8 x 2 = 16 <= 30. - `docs` / `.turbo/cache`, ceiling 15 min -> 5 minutes. This job's own save measured 5s on job 100094597253 (a merge_group build, where these steps actually run). 5 min is 60x that; 5 x 2 = 10 <= 15. No job's `timeout-minutes` moves and no command-bearing step changes: all 7 jobs and 50 command steps of this file compare byte-equal against adbda1b, with a control proving the comparator non-vacuous. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Same split as ci.yml's: restore in place and unbounded, save last with `timeout-minutes: 5` and `continue-on-error: true`. Bound derived here, not inherited from PR #7047 — landing on the same number is a result: this job's own save of `.turbo/cache` measured 6s on 2026-09-02 (`Post Turbo Cache`, job 100097466846) against a 5m14s job, so 5 minutes is 50x the measured save. `Lint` is a required context on `pull_request` and `merge_group`, and this job declares NO `timeout-minutes` — its ceiling is GitHub's 360-minute default, so an unbounded stalled save could hold the shared serial queue for six hours before reporting `cancelled`. Adding a ceiling is deliberately NOT done here: it would change what this gate rejects. The bound is on the bookkeeping step alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
This job has two combined `actions/cache@v6` uses; both are split. The saves go
last, ordered Playwright-then-fixture so the observed post-phase order is
preserved (post steps run in reverse registration order — job 100097466897).
Bounds derived per cache:
- Playwright browsers (~269 MB) -> 8 minutes, from measured restores of the
same archive (5s here, 3s in ci.yml) plus a pessimistic 20x upload penalty.
- live backend fixture -> 5 minutes. This site's save measured 8s on
2026-09-02 (job 100097466897) — a real save, not a skip: the restore missed,
which is why `Start ObjectStack backend` did the full clone and install.
Both bounds share one 40-minute ceiling, so what matters is their sum:
5 + 8 = 13 minutes against a verdict path measured at 2m25s end to end, so even
both bookkeeping halves stalling at once cannot reach the ceiling.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Same split; `timeout-minutes: 5`, `continue-on-error: true`, save placed last — after the budget verdict and after the comment that reports it, which is where the generated post phase already ran. Bound derived from this site: this job's own save of `.turbo/cache` measured 1s on 2026-09-02 (`Post Turbo Cache`, job 100096569559) against a 4m07s job, so 5 minutes is 300x it. That is the same RATIO PR #7047 sized its bound at, arrived at from this job's own number rather than copied from that one. `Bundle Analysis` is a required context (objectui#6245) and this job declares no `timeout-minutes`, so its ceiling is the 360-minute default — the bound is the only thing that stops a stalled save holding a required check open for hours. Adding a ceiling would change what the gate rejects and is deliberately not done. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
The release lane, treated with extra care: the diff is strictly the cache step and its comment, the save is appended LAST — after publish, after the post-version tree validation and after the refresh — and no step above it is touched. Publishing cannot be affected by a step that runs after all of it and carries `continue-on-error: true`. The derivation here is weaker than at the other six sites and the comment says so rather than dressing it up. A same-SITE save was not observable in the sampled runs: this job runs on `push` to `main` only when the version is not yet on npm, and on the 6-hourly `schedule` the primary key is an exact hit — the sampled scheduled run's `Post Turbo Cache` measured 0s with its restore at 4s (job 99021319583, 2026-08-29). The bound is therefore derived from the SAME CACHE at four sibling sites, whose saves measured 1s, 2s, 5s and 6s on 2026-09-02 (jobs 100096569559, 100096569872, 100094597253, 100097466846); 5 minutes is 50x the slowest of those. This job declares no `timeout-minutes` either, so its ceiling is the 360-minute default. Adding one is NOT attempted: the release act must never be truncated by a clock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Reverting any of the seven splits is invisible — the cache still works, every run still passes, and the next transient stall ejects the next green pull request. PR #7047 shipped a pin for exactly that reason; this one generalises it from the one incident site to the population, because the population is what this card found: seven more combined uses across five workflows, the repository's default spelling for caching. Enumerates every workflow file and requires of every `actions/cache/save`: `timeout-minutes` present, `continue-on-error: true`, placed after its job's last command step, same path and key as a matching restore with an `id`, and guarded on that restore's `cache-hit`. Where a job declares a ceiling, the SUM of its save bounds x 2 must fit inside it — the generalisation of #7047's invariant to a job with more than one cache. Non-vacuity is asserted on the axis that actually failed when this card was filed: the POPULATION, not the probe. A control on a probe cannot catch a scope inherited from someone else's narrative, which is how the first count came in 3.5x low. Deliberately not asserted: that restore halves are bounded (a restore stall fails before any verdict exists, which is honest), and any specific bound value — each site's number is derived from that site's own measurement and lives in the comment beside it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Sep 2, 2026
yinlianghui
marked this pull request as ready for review
September 2, 2026 02:52
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.
Fixes #7048
Every remaining combined
actions/cache@v6use in this repository is split intoactions/cache/restore@v6(in place, unbounded) plus a trailingactions/cache/save@v6carryingtimeout-minutesandcontinue-on-error: true. Seven sites across five workflows — which, as the card's correction comment recorded, was the repository's default spelling for caching, not an edge case on one job.The mechanism is #6577's, pinned by PR #7047 and not re-argued here: the combined action declares
main: dist/restore/index.jspluspost: dist/save/index.js, so its save is a step the runner generates at job end and no workflow syntax attachestimeout-minutesorcontinue-on-errorto it. An upload stall therefore runs the job into its ceiling, the job goescancelled, and the merge queue cannot tellcancelledfromfailure. The ordering sentence is carried verbatim into all seven comments: the verdict is recorded by the checking steps; everything after them is bookkeeping, and bookkeeping must never discard an answer the gate already produced.⛔ No job's
timeout-minutesis raised, and none is added. ⛔ No verdict-bearing step is touched.Population — re-derived, not inherited
Enumeration and content read from the same source (
git ls-tree -r, thengit show) atadbda1bed:actions/cache@v6sitesrestore/savelinesuses: actions/lines across those filesThe seven sites, each with its own bound
⭐ The card fences copying PR #7047's
timeout-minutes: 5across. Every number below is derived from that site's own measurement; the four.turbo/cachesites landing on 5 is a result of their own arithmetic, not a copy — each comment carries its own working.adbda1bed)ci.yml:1146—e2elive-e2e.ymlci.yml:1311—docs.turbo/cachePost Turbo Cache, job 100094597253)apps/site//content/changes; the job's real run is themerge_groupbuildlint.yml:244.turbo/cachelive-e2e.yml:98live-e2e.yml:163ci.yml:1146; restore 5s hereperformance-budget.yml:138.turbo/cachepaths:)changeset-release.yml:576.turbo/cachepushtomain/scheduleonly)Bound arithmetic, stated so it is checkable
.turbo/cache(5 min). Same cache, four independent same-day measurements: 1s, 2s, 5s, 6s. 5 minutes is 50x the slowest and 300x the fastest. Ceiling check where one exists: 5 × 2 = 10 ≤ 15.live-e2e.ymlhas two caches under one ceiling, so what matters is the sum: 5 + 8 = 13 min against a verdict path measured at 2m25s end to end — both bookkeeping halves stalling at once still cannot reach 40.changeset-release.ymlis the only site with no same-site save measurement. Itsreleasejob runs onpushtomainonly when the version is not yet on npm, and on the 6-hourlyschedulethe primary key is an exact hit — the sampled scheduled run'sPost Turbo Cachemeasured 0s with its restore at 4s (job 99021319583, 2026-08-29). Its bound is therefore derived from the same cache at four sibling sites. It is included rather than left out because the diff there is strictly the cache step plus its comment, the save is appended after publish, after the post-version tree validation and after the refresh, and it carriescontinue-on-error: true— so nothing the release lane does can be affected by it. The workflow still parses (below). If a directly measured save of that site ever lands outside the 1–6s range, the comment says to re-derive rather than defend the number.No verdict-bearing step changed — comparison parser output
PR #7047's approach, re-implemented over all five files: parse both revisions with a real YAML parser and compare, per job, every command-bearing (
run:) step plus each job'stimeout-minutes.⭐ The control earned its keep, and the way it did is worth recording. Its first spelling appended
# CONTROL-MUTATIONto arun:line — YAML reads that as a trailing comment, the parsed scalar never changed, and the control came back clean while the comparator was working perfectly. Exactly the class of trap PR #7047 recorded for its own ablation anchor, caught here only because the control was asserted rather than assumed. The mutation now changes the scalar itself.Ablation — 21 of them, each proven on disk and each restore proven by blob hash
Three mutations × seven sites, run against the pin: fold the split back, drop
continue-on-error, hoist the save above the checking steps. 21 red, 0 green. Every mutation is confirmed on disk before the reading is trusted, anchored on the YAML key at its exact indent — never the phrase, becausecontinue-on-error: truealso appears in the comments this change adds (the trap #7047 recorded, inherited deliberately). Every restore is proven by comparinggit hash-objectagainst the file's HEAD blob, with an empty hash read as failure.uses: actions/cache@v6+1,restore@v6−1,save@v6−1continue-on-errorrun:193 before (after: true) vs save 153 → lastrun:204 after (after: false)Sample restore proof:
restored; blob fed6ab4ad671edbce5d5d271c35f61474cc09c68 == HEAD fed6ab4ad671edbce5d5d271c35f61474cc09c68. Final line:all 5 touched workflow files match their HEAD blobs.Gates — all run at the final commit
4c67aeddb, verdict lines quoted✅ No source or published contract of a released package changed in this range, so no changeset is owed.(6 files changed, 0 published source) — none owedcheck:control-bytes✅ check-control-bytes: OK (scanned 6002 tracked text file(s); skipped 85 binary).check:shell-escape-residue✅ check-shell-escape-residue: OK (4/4 root(s) resolved … 205 file(s) and 1318 fenced block(s) examined)— its roots are AGENTS/CLAUDE/skills/content, so it does not scan workflowscheck:governed-queue-guardOK check-governed-queue-guard self-test: 132 cases pass; and for this diff's paths:✅ NOT GOVERNED — 6 path(s) checked against 5 governed surface(s); none matched.check:lint-coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).parsed 31 workflow file(s), 39 job(s) total, 0 problem(s)— this is the "changeset-release.ymlstill parses" evidence;actionlintis neither installed in this container nor referenced anywhere in the repo (git grep actionlintexits 1)scripts/vitest suite (every test that reads these workflow files)Test Files 94 passed (94)/Tests 2625 passed (2625)turbo-cache-save-bound+workflow-cache-save-boundTest Files 2 passed (2)/Tests 14 passed (14)type-check:scripts--listFilesconfirms the new test file is in the program (1 occurrence; #7047's pin as positive control, also 1) — so this is a measurement, not a vacuous passDeclared narrowing:
pnpm lint(turbo run lint, 46 packages) was not run locally. Evidence the narrowing excludes nothing: (1) population read from eslint's own resolution, not a guess — 226 files in the root scope, 46/46 packages reported bycheck-lint-coverage.mjs; (2) file count read from--format json; (3)eslint.config.jsdeclares noproject/projectService/parserOptions(grepexits 1), so it is not type-aware and this diff cannot move any untouched file's verdict — and the diff contains zero files underpackages/,apps/orexamples/, so the package farm has no changed input at all. CI runs the farm regardless.Deviation from the declared file surface — one file, declared not hidden
The card fenced the file surface to the five workflow files. This PR adds one more:
scripts/__tests__/workflow-cache-save-bound.test.ts.The reason is the card's own requirement. "Prove each pin has teeth by ablation" is unanswerable without something that can go red — and reverting any of these seven splits is invisible: the cache still works, every run still passes, and the next transient stall ejects the next green pull request. PR #7047 shipped its split with a pin beside it for exactly this reason and said so in its own body. Without this file the ablation section above would read "21 mutations applied, nothing noticed."
It is a second file rather than an edit to
turbo-cache-save-bound.test.tsbecause the two have different subjects: that one is about one incident and is sized againsttype-check's numbers; this one is about the population, and asserts its non-vacuity on the population axis — which is precisely the axis that failed when this card was first filed 3.5x low, and which a probe-side control structurally cannot catch.Live-run coverage on this PR
ci.yml,lint.ymlandlive-e2e.ymlarepull_request-triggered andperformance-budget.ymllists its own file in itspaths:, so six of the seven sites get a live run on this PR. Two caveats stated rather than glossed:ci.yml'sdocsjob skips its steps on a PR with noapps/site//content/change, so that site's live proof arrives at themerge_groupbuild; andchangeset-release.ymlruns only onpushtomainand on its schedule, so that site gets no live run before merge — the YAML parse and the pin are its whole local evidence.Not claimed
Post Turbo Cachesave runs theType Checkjob into its 20-minute ceiling, turning an all-green merge-queue run into a CANCELLED check that ejects the PR #6577 stays n=1. This is exposure, not a defect, and nothing here upgrades it.successandmainhas not moved in 90 minutes #7010 is not cited as corroboration — different fault shape, and the card says so.🤖 Generated with Claude Code
https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Generated by Claude Code