Uh oh!
There was an error while loading. Please reload this page.
ci(release): refresh the Version Packages PR on a schedule, not on every push - #11238
Merged
Conversation
…ery push The `version-pr` job ran on every main push, so changesets/action recomputed and force-pushed `changeset-release/main` on each of main's ~18 daily landings. The standing Version Packages PR therefore never held still long enough for its own branch CI to converge, and it was ejected from the merge queue on entry. This is (action algorithm x trigger frequency), not a CLI version: the repo is already on @changesets/cli ^3.0.0. Move `version-pr` onto a 6-hourly `schedule` plus an on-demand `workflow_dispatch` input, which is the mode the changesets project documents for exactly this. Between refreshes the branch is static, its CI converges, and it merges through the ordinary queue. `workflow_dispatch` was already the publish repair lane, so the event is split by a new `refresh_version_pr` boolean rather than by a second workflow file: with it, only `version-pr` runs; without it, the repair lane behaves exactly as before. `release-integrity` gains the `if:` that keeps bookkeeping events from queuing a deployment at the `release` environment for a maintainer to dismiss. ADR-0125's invariants are untouched: `publish` is still the only job that publishes, still unreachable from `schedule` and from a refresh dispatch, still held whole at `environment: release`; `version-pr` still carries no `publish:` script, so it cannot publish by construction. The 2026-08-07 ruling narrative is unchanged. Prose that described the old mechanism is updated in the same change: release.yml's own header and the premise line, route (a)/(b) passage and GA cut flow in docs/releases-maintenance.md. Fixes#11233 Co-Authored-By: Claude <noreply@anthropic.com>
os-zhuang
commented
Aug 23, 2026
ContributorAuthor
ACCEPT — session 全量 diff 复核(2 文件 +200/−45,head
落地:flip ready + auto-merge。合并后维护者的发版流即为:(要最新就 dispatch 勾 Generated by Claude Code |
os-zhuang
marked this pull request as ready for review
August 23, 2026 05:09
os-zhuang
enabled auto-merge
August 23, 2026 05:09
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 23, 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 freeto 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#11233
The defect
release.yml'sversion-prjob ran on every main push. changesets/action's version path isgit reset --hard $GITHUB_SHA→ re-version →git push --force origin HEAD:changeset-release/main, so every one of main's ~18 daily landings recomputed and force-pushed the standing Version Packages PR (#10183 / #4935). The PR therefore never held still long enough for its own branch CI to finish — each run was superseded by the next force-push — so it could not converge and was ejected from the merge queue on entry.This is a property of (action algorithm × trigger frequency), not of the CLI version: this repo is already on
@changesets/cli ^3.0.0and the churn is unchanged. The remedy is the mode the changesets project documents for exactly this case: refresh on a schedule instead of on every push. Between refresheschangeset-release/mainis static, its CI converges, and it merges through the ordinary queue like any other PR.Before / after — the full trigger matrix
version-prrelease-integritypublishdockerreleaseworkflow_dispatchforce), held atreleaseschedule(0 */6 * * *)refresh_version_prrefresh_version_prforce), unchangedEvery cell for the publish path is unchanged. The two new rows are the bookkeeping lane, and neither can reach a job that publishes.
The dispatch collision, and why the shape is this one
workflow_dispatchwas already taken: it is the publish REPAIR lane (ADR-0125 D4, with theforceinput). Making an on-demand refresh use the same event naively would mean one event name starting two lanes that must never start each other:releaseenvironment — an approval prompt a maintainer must open and dismiss on routine bookkeeping. Approval noise is how an approval stops being read, and that approval is this file's entire barrier;Chosen shape — a new
refresh_version_prboolean input splits the event inside the one file. Each job carries its own half of the split; no job infers its lane from another job's presence:Three points that were decisions, not defaults:
release-integritygains anif:it did not have. It previously carried none, which meant "every event this file has" — correct while the file had exactly the two release events, and wrong the moment a third arrived. Without this the 6-hourly tick would run a full release audit and queue deployments. The audit mints nothing, so this is a noise guard, not a safety guard, and it is labelled as such in the file.publish's!inputs.refresh_version_pris written although it is currently redundant. A refresh dispatch skipsrelease-integrity, and a skippedneedsjob makessuccess()false — the belt already holds (thedockerjob documents that same GitHub behaviour from the other direction). But that would make the force branch's safety a fact about a different job'sif:, discoverable only by reading it. This is the one job in the repo that publishes; its guard states its own preconditions.refresh_version_pr+forceis reachable and is refused, not resolved. The two inputs are independent, so the combination can be submitted;publish's guard excludes it, so that dispatch publishes nothing. A dispatch that both refreshes bookkeeping and force-publishes is not a thing anyone means — the harmless reading is the one that runs.No second workflow file, per the card: a second lane would duplicate the publish invariants somewhere they can drift apart from these.
Invariants deliberately untouched
publishis still the only job that runschangeset publishor pushes a tag; it is still held whole atenvironment: release; its steps are unmodified.publishis unreachable fromscheduleand from a refresh dispatch. Ascheduletrigger reachingversion-pris not a loosening: that job has no publish capability by construction — the changesets step still takes nopublish:script, so the action's publish branch is unreachable from any input state it can observe. That structural guarantee is what this PR does not touch.release-integrity's push behaviour, its predicate, and itsgithub.shatripwire are unchanged.cut-rc.yml, environment config andcontent/docs/releases/are not touched.version-pr's per-jobconcurrency(cancel-in-progress: false) is kept, and its comment is re-justified rather than left standing: the race it covers is now a scheduled tick overlapping a slow previous run, or an on-demand dispatch fired by someone impatient with a tick already in flight.One note added to the
on:block because it argues the split rather than decorating it: scheduled runs are queued, may be delayed or dropped under load, and are disabled after 60 days of repository inactivity. All acceptable for bookkeeping — a late refresh leaves the PR visibly stale and one dispatch fixes it — and a second independent reason the publish lane must never be reachable fromschedule.Prose updated in the same PR (the mechanism changed, so the descriptions must)
.github/workflows/release.ymlheader:push → version-prbecomes the schedule/dispatch rows);dockerjob's "exactly one of the two upstream jobs runs on any given event" becomes "at most one", since bookkeeping events run neither. No condition change: unset outputs already compare false;version-prleft the push trigger, and the dispatch collision.docs/releases-maintenance.md:cut-rcremains the rc route;version-prruns, because it does not run on push under either route. The passage now states the stale-until-next-refresh outcome once, and keeps (a)/(b) for what they still decide: whether the cut's push firesrelease-integrityat all;main" becomes the next scheduled refresh;refresh_version_prif it is behind, before merging. This is the operational payoff of the card and the one new thing a releaser must know.Gate verdicts
Families derived with
node scripts/pm/dispatch-gates.mjs(no hand-typed path list — the script takes the change set from the merge base itself): 11 matched. All re-run after the final commit, atd3b9dc67f3. Exit codes captured before any pipe; lines below are each gate's own verdict, not$?:check:doc-authoring✓ doc authoring guard: 389 files clean — no bare metadata literals.check:doc-formula-expressions✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 416 files / 1447 TS blocks judged cleancheck:node-versioncheck-node-version: OK (32 setup-node step(s) across 26 workflow(s), all on Node 22).check:pnpm-filter-targets✓ check:pnpm-filter-targets: 121/149 --filter occurrence(s) across 25 file(s) resolve against 78 workspace package(s)check:required-contexts✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s); 5 instruction surface(s) scanned against 2 retired name(s)check:shard-attestation✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check:workflow-status-functionscheck-workflow-status-functions: OK (scanned 26 workflow file(s), 49 job(s), 25 job-level if: expression(s); 10 read needs.*.outputs.*, all naming a status function).check-aggregator-roster.mjs✓ check-aggregator-roster: 3 aggregator(s) across 2 workflow(s); roster == needs: in both directionscheck-required-contexts.mjs(patrol)required-set-patrol.ymlcheck-shard-attestation.mjs✓ check-shard-attestation --self-test: 103 assertionscheck-step-collectors.mjs✓ check-step-collectors: 354 run: steps across 26 workflow(s)check-ci-filter-parity.mjs†OK: all 88 declared cross-package glob(s) (76 unique) are covered by core or crosspkgcheck:nul-bytescheck-nul-bytes: OK (scanned 6519 text file(s) ... no raw ASCII control bytes).†
check-ci-filter-paritywas named in the dispatch prompt but was not derived for these paths; run anyway, green.YAML validated with in-repo means, no new dependency — parsed with the workspace's own
yamlpackage and the trigger block and all four job guards dumped; both key spellings (on/ YAML-1.1true) handled ascheck-required-contextsdoes. Parse exit 0; guards read back exactly as designed.Residue examined, not assumed.
dispatch-gatesflagged 2 silent families whose roster sits under.github/workflows— the shape it warns "reads as a clearance and is not". Both arecheck:single-claim-paths; itsSINGLE_CLAIM_PATHSdeclares exactly one entry,.objectui-sha. Neither changed path is in it, so the silence is correct.Repo-wide
pnpm lintnarrowed, and the narrowing measured.npx eslint --format jsonover both changed paths reports 2 files, 0 errors, each withFile ignored because no matching configuration was supplied— i.e. the population is read from eslint's own config resolution, not from my guess about which files count, and both changed files are outside it entirely. Config invariance: this diff changes no eslint config and no TypeScript, so no untouched file's verdict can move.Landing class
Verified on the final file list, not assumed. Opened as DRAFT anyway:
release.ymlis release-critical, and the PM reviews before flipping ready.Changeset
None — workflow + docs only, so this PR releases no package.
skip-changesetapplied as a union with the labels the bots had already set, then read back, which is the exemptionpr-automation.yml'schangeset-checkreads live.Related / out of scope
docs/releases-maintenance.mdthat ADR-0125 already made stale, independent of this card ("Both lanes areworkflow_dispatch…", and the GA row/step still telling a releaser to dispatch a typed version). Not touched here: they predate this change and one sits inside the 2026-08-07 ruling narrative.Generated by Claude Code