Uh oh!
There was an error while loading. Please reload this page.
ci(release): validate the post-version tree before the version PR is refreshed - #6217
Conversation
…refreshed The release PR runs no CI and cannot be given any. Re-measured 2026-08-25: `ci.yml` has 849 runs on `changeset-release/main`, every recent one `action_required` with `created_at == run_started_at == updated_at` — created and immediately parked, nothing executed. GitHub does not start workflow runs from events raised by `GITHUB_TOKEN`, and the refresh force-pushes that branch, so there is no stable head to re-run against either. On the 17.6.0 release PR the check-runs endpoint returned `total_count: 1`: one job, 7 seconds after the merge. The release commit is the only commit reaching `main` without passing the merge queue. Maintainer ruling 2026-08-22 (option A): validate the post-version tree inside this workflow, before `changesets/action` opens or updates the PR. The ruling attached a pre-condition — price it first — and the price chose the shape. The version step cannot move a source byte. Rendered against the real tree (328 pending changesets, 17.6.0 -> 17.7.0) it touches 411 paths: 330 `.changeset/*.md` deleted, 40 `package.json` (the `"version"` key and nothing else — 80 changed lines, all of them `"version":`), 40 generated `CHANGELOG.md`, and `QUICK_REFERENCE.md`. The source is byte-identical to the `main` commit whose push lane `ci.yml` just ran under coverage across four shards, so a `pnpm test` here would re-test tested bytes at ~40 min a go (ci.yml's own instrumented figure: 39m51s of tests), four times a day on the 6-hourly cron — ~2.7 h of daily runner time for a PR nobody reads until release day, i.e. the cost objectstack#10850 was closed to remove, walking back in. So the refresh lane renders the version itself, validates the surfaces the diff can move, and restores the tree for the action: pnpm quick-reference:check QUICK_REFERENCE.md ~1 s pnpm check:control-bytes the 40 generated CHANGELOGs ~4 s pnpm test scripts/__tests__ 73 files / 1996 tests ~50 s The suite is named as a directory, not a file list: every test that reads a manifest version, `QUICK_REFERENCE.md` or a `CHANGELOG.md` lives in it (7 of 7 matches), `doc-version-claims.test.ts` among them. `check:spec-floors` and `check:published-dist` are deliberately absent — they read dependency ranges and built `dist/`, neither of which moved, and `changeset:publish` runs both first on the publish lane. No "only when the PR content changed" condition, for a measured reason: across the seven consecutive 6-hourly windows from 2026-08-23T06:08Z to 2026-08-25T00:10Z, six carried new changeset files (median 18). Such a predicate would skip about one refresh in seven while adding the local prediction of another project's state that #6081 just deleted from this file. Failure semantics: a red validation fails the job and the refresh never runs, so the PR keeps its last validated content. Nothing can touch the publish lane — all three steps are scoped to `schedule` / `workflow_dispatch`. The restore is load-bearing and asserted, not assumed: with `.changeset/` left consumed the action finds nothing pending, takes its no-op branch and returns, and the PR fossilises with nothing failing anywhere. Part of #5397 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 25, 2026
PM review — ACCEPT. Open question ruled A (as shipped). My order's cost figure was stale and the fix's shape is better than the one I specified.Reviewed by the Both shape-changing findings verified here, not accepted on the report1. My "~12 h/day" figure had expired. Confirmed on schedule:
- cron: '0 */6 * * *'objectstack#10850 moved the refresh off every-push onto a 6-hourly tick, and your Actions-API dating of the changeover (many-per-hour through 2. The scope finding is corroborated structurally. Exactly two steps, and neither transforms source:
⭐ And this is what makes the whole PR right. If the post-version tree is source-identical to the 3. The conditional layer the ruling asked about is refused with a measurement, not an opinion. Seven consecutive windows, six carrying new changesets (219, 46, 23, 18, 13, 9, one zero; median 18) → a validate-only-when-content-changed predicate skips about one refresh in seven, saving ~14% of 68 s. Bought with exactly the cross-project state prediction that #6081 deleted from this file last night. ⛔ Correctly not built. A conditional that re-adds a deleted class of coupling to save ten seconds a day is a bad trade, and answering the ruling's question with numbers instead of deferring it back to me is the right move. The open question — ruled A, blocked, as shippedA stale PR is recoverable by one dispatch with
The restore is asserted, and the failure it guards is nastier than "the tree is dirty"
⭐ That is the right reason to assert the restore rather than trust it. A consumed ⭐ And the ScopePublish-lane steps byte-for-byte untouched; the three new steps scoped to The declared narrowing is measured rather than asserted: The stated limit is the most useful sentence in the report
Naming the unobserved leg, and naming the cheap event that will observe it, is what makes this landable without pretending to proof nobody can get. Also correctly stated: the Cross-repo findingobjectstack#11945 — filed on the strength of reading that repository, not inference: its ⛔ Not arming yet
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Part of #5397
Maintainer ruling 2026-08-22 (「接受所有」): option A — validate the post-version tree inside
changeset-release.yml, after the version step and beforechangesets/actionopens or updates the release PR. The ruling attached a binding pre-condition — price it first — and the price is what chose the implementation shape below.The measurement came first, and it changed the answer
Re-measured today (the card asked for this explicitly). The defect still holds and is worse than filed:
ci.ymlnow has 849 runs onchangeset-release/main(736 when the card was filed), and every recent one isconclusion: action_requiredwithcreated_at == run_started_at == updated_at— created and immediately parked, nothing executed. Most recent:2026-08-25T00:12:42Z, run 32792689033.But the cost premise in the card expired two days ago. The card (2026-08-20) and the dispatch order both price naive option A against a refresh that runs on every push to
main— ~18/day × 40 min ≈ 12 h of daily runner time. objectstack#10850 moved the refresh onto a 6-hourly cron, and the Actions API shows the changeover precisely: refresh runs were many-per-hour through2026-08-23T05:39Z, and from2026-08-23T06:10Zthey are exactly 6 hours apart. Measured: 8 schedule runs in 42 h = 4.0/day, not 18.0 */6 * * *)lane6 s +release100 s; install 13 s, build 37 s, the changesets step incl.pnpm changeset:version13 s)pnpm testpost-version)ci.yml's own instrumented figure for the unsharded suite is 39 min 51 s of tests / 40 min 19 s of job, recorded in its header.Why naive option A is the wrong shape — the version step cannot move a source byte
Rendered against this repository's real tree (328 pending changesets, 17.6.0 → 17.7.0),
pnpm changeset:versiontouches 411 paths, and all of them are one of four kinds:.changeset/*.md*/package.json"version"key and nothing else — 80 changed lines, every one of them"version":*/CHANGELOG.mdQUICK_REFERENCE.mdchangeset:version'ssync-quick-reference-release.mjsZero
.ts, zero.tsx, zero config, zero dependency ranges. The source in the post-version tree is byte-identical to themaincommit the job checked out — the commitci.yml's push lane already ran the whole suite over, under coverage, across four shards (~13.8 min wall, median of 30 recent runs). Apnpm testhere would re-test bytes tested minutes earlier and report nothing new, at 40 minutes a go, four times a day. That is the cost objectstack#10850 was closed to remove, walking back in through a different door — exactly what the dispatch order forbade shipping unmeasured.So the validation is scoped to what the diff can actually move, and the union —
ci.ymlon the source, these steps on the version outputs — is the whole post-version tree.What the refresh lane now does
Three steps, all scoped to
schedule/workflow_dispatch, inserted immediately beforeRefresh the version PR:Render the post-version tree —
pnpm changeset:versioninto the runner's working tree.Validate the post-version tree:
pnpm quick-reference:checkQUICK_REFERENCE.md— the drift main is red: QUICK_REFERENCE.md "Current Release" still says 17.5.0 after the 17.6.0 release bump — and this is the third instance of one recurring mechanism #5394 cashed three timespnpm check:control-bytesCHANGELOG.md, whose prose comes from changeset bodiespnpm test scripts/__tests__Restore the pre-version tree, and assert the restoration.
The tooling suite is named as a directory, not a file list, deliberately: every test in this repository that reads a manifest version,
QUICK_REFERENCE.mdor aCHANGELOG.mdlives in it (measured — 7 of 7 grep matches),doc-version-claims.test.tsamong them, and a hand-list would go stale the first time someone adds an eighth.Deliberately not run, and it is not an omission.
check:spec-floorsreads dependency ranges andcheck:published-distreads builtdist/— the measurement above rules both out (no range moved, no source moved), so on this tree they can only restate the verdict they gave onmain.check:published-distalso costs a full build (8 m 39 s cold, measured here), andpnpm changeset:publishruns both of them first on the publish lane anyway, where the tree they judge is the one being shipped.No "only when the PR content changed" conditional, and that is the other half of the ruling's condition, retired by measurement rather than by preference: across the seven consecutive 6-hourly windows from
2026-08-23T06:08Zto2026-08-25T00:10Z, six carried new changeset files — 219, 46, 23, 18, 13, 9, and one zero (median 18). This repo lands ~18 merges a working day and most carry a changeset, so a content-change predicate would skip about one refresh in seven: ~14 % of 68 seconds, bought with exactly the thing #6081 deleted from this file last night — a local prediction of another project's state that is free to be wrong silently. The cost was solved by scope, so frequency did not need solving.Failure semantics, and the restore
A red validation fails the job and the refresh step never runs, so the standing version PR keeps its last validated content instead of being force-pushed to a broken one. If the post-version tree stays broken the PR goes stale — but loudly, on a red run every six hours, which is the opposite of this card's defect.
changesets/action@v1picks its branch from the tree: with.changeset/left consumed it findshasChangesetsfalse, and the refresh step passes nopublish:input sohasPublishScriptis false too — v1 then takescase !hasChangesets && !hasPublishScript(src/index.tsline 68), logs "No changesets present or were removed by merging release PR", and returns. The refresh becomes a permanent no-op and the PR fossilises with nothing failing anywhere. So the tree is put back and the restoration is asserted (pending count restored and no tracked path still modified), in the same shape the existing clear step asserts its own work. The action then runspnpm changeset:versionitself and — as today — owns the commit and the push.The publish lane is untouched: steps 10–12 (
Clear pending changesets/Publish to npm/Verify the release reached npm) keep their conditions and their order byte-for-byte, and all three new steps skip onpush.Verification — and its honest limit
Union re-run at
44f70009c(the head this PR points at), exit codes captured by redirect before any pipe:The two tests that read this workflow were also run alone —
changeset-release-action-ref-pin.test.tsandci-cd-pipeline-doc.test.ts, 2 files / 39 tests passed. The ref-pin test asserts the exact set['release / Publish to npm', 'release / Refresh the version PR'], so it would have caught a fourthchangesets/actionusage or a renamed lane; this PR adds neither.The render → validate → restore sequence was executed for real, locally, against the actual 328-changeset tree:
pnpm changeset:version(exit 0), thenquick-reference:check(1 s),check:control-bytes(4 s, scanned the regenerated CHANGELOGs),pnpm test scripts/__tests__(50 s, 1996 passed) — all green on the rendered tree — then the restore, verified on disk: 328 pending changesets back, working tree clean.git statusreported 0 untracked files created by the version step, so the tracked-files scan covers the whole moved surface on today's tree.⛔ No release was executed, and this cannot be proven end-to-end without one. What remains unobserved: the runner-side behaviour of the sequence — that the action's own
changeset versionafter the restore produces the same tree, and that the PR refreshes normally. That is exercised by the first scheduled tick after this merges (within 6 hours, no release required); what genuinely waits for the next real release is only the merge of a validated PR. Theaction_requiredreading itself will not change — this PR does not makeci.ymlrun onchangeset-release/main, and nothing can while the bot authors that branch withGITHUB_TOKEN.Docs
content/docs/guide/ci-cd-pipeline.mdgains a subsection under Changeset Release carrying the measurements, the scope argument, and the failure semantics — the page is the repository's single workflow inventory andci-cd-pipeline-doc.test.tsenforces that.Generated by Claude Code