feat(release): cut-rc — snapshot one-click rc release (#7447) - #7453
Conversation
A `workflow_dispatch`-only lane that bumps the objectui pin, versions, gates and publishes an rc from ONE snapshot, ending the two-repo pin-chasing freeze dance (rc.6 needed four overtaken pin-bump laps and ~40 min of coordinated freezes). The snapshot is the whole idea: objectstack `main` and objectui `main` HEAD are each resolved exactly once, and every downstream step reads those frozen values, so both repositories may keep moving for the entire run. Preserves every invariant of the existing lanes: - 2026-08-07 manual-publish ruling: dispatch-only trigger, `environment: release`. - #6170: the version commit lands on `main` FIRST and is published SECOND, so nothing is ever published from a ref that only existed inside the run. - #3340: `check-objectui-pin-fresh.mjs` is unchanged and still enforces on the GA path. This lane asserts pin SELF-CONSISTENCY instead of liveness, because liveness is exactly what a snapshot gives up; the changeset record still covers OLD_PIN..SNAPSHOT by construction. - #5960: the ADR-0082 D4 declaration-parity ratchet runs here, since this lane is now a route that moves the pin. `release.yml`'s three lanes, `scripts/check-objectui-pin-fresh.mjs` and `content/docs/releases/**` are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPWqbmEFU8gJepBJTHESXd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
PM review (dispatching seat, session
Two corrections to the dispatch I want on the record, because the dev measured rather than assumed and was right both times: (1) in pre mode One gap to close before this leaves draftWhat happens to the standing Please add: a short "What happens to the standing Version Packages PR" paragraph in Everything else is ready. CI is green or running with no failures on Reminder for the maintainer reading this thread: the admin prerequisite is one of the two routes, not both, and route (a) needs no new secret at all. Generated by Claude Code |
… a cut-rc run (#7447) A cut leaves #6208 (`chore: version packages (rc)`) in one of two resting states depending on which push credential the maintainer configured, and the answer is counter-intuitive enough to be worth writing down rather than rediscovering mid-incident. Verified rather than assumed, three ways: - GitHub documents that events triggered by the automatic GITHUB_TOKEN do not create workflow runs, with `workflow_dispatch` / `repository_dispatch` the only exceptions. So on route (a) the version-commit push does not fire release.yml's push lane and `version-pr` never runs. - A PAT is not the GITHUB_TOKEN, so route (b) triggers normally and the PR self-corrects. - This repo already depends on the same guard and says so: docker-publish.yml's header explains a `push: tags:` trigger "would never fire" because the release workflow pushes its tags with GITHUB_TOKEN. States plainly that a stale #6208 after an rc cut is expected and harmless — its changesets are consumed and recorded in pre.json, the PR carries no publish capability by construction — and that the cutter must not fix it by hand. Also sharpens the runtime-image note while adjacent: the image comes from a LATER push under both routes, not the cut's own, because on route (b) `release-integrity` audits long before the cut's publish step reaches npm. Adds the same two expectations to the workflow's run summary, so the operator reads them at the moment they would otherwise be surprised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPWqbmEFU8gJepBJTHESXd
|
Gap closed in I checked the recursion-guard claim rather than encoding it, as you asked. It survives, and it got stronger. Three independent confirmations:
So your version is correct and I encoded it. Two refinements I added because the checking turned them up:
What landed: a "What happens to the standing Version Packages PR" subsection in Re-ran after the edit: Leaving it in draft for you / the maintainer to flip, per your note. Generated by Claude Code Generated by Claude Code |
Fixes #7447
A
workflow_dispatch-only lane that bumps the objectui pin, versions, gates and publishes an rc from one snapshot. Cutting rc.6 today needed four pin-bump laps (8aad9fd→9b9fa49→cfeb378→1b6188d→bcd3e02, every lap overtaken) and would have needed ~40 min of coordinated freezes across two repos. The snapshot is the whole idea: objectstackmainand objectuimainHEAD are each resolved exactly once, and every downstream step reads those frozen values — so both repositories may keep moving for the entire run.File surface:
.github/workflows/cut-rc.yml(new) anddocs/releases-maintenance.md.release.yml's three lanes,scripts/check-objectui-pin-fresh.mjsandcontent/docs/releases/**are untouched.The sequence
main,X.Y.Z-rc.Nonlymainat full depth, recordSNAPSHOT_SHAfetch-depth: 0pre.jsonismode: pre/tag: rc, and the requested version is not the one main already carriesmainHEAD once viagit ls-remote, full-clone itbump-objectui.sh $OBJECTUI_SHA(always explicit), assert.objectui-sha == $OBJECTUI_SHApnpm objectui:build,check:console-sha, playwright install,pnpm sdui:manifestcheck-changeset-fixed,check:nul-bytes, and the three.changesetgatespnpm run version+ assert computed version == dispatch inputchangeset versionmain, confirm ancestry, publish from the landed commitInvariants preserved
environment: release. A guard step also refuses any non-workflow_dispatchevent, so a future edit that adds apush:trigger fails the run instead of publishing.mainfirst and is published second. Nothing is ever published from a ref that existed only inside the run.check-objectui-pin-fresh.mjsis unchanged and still enforces on the GA publish path. This lane deliberately does not call it: liveness is precisely what a snapshot gives up, and re-checking it would restore the race the lane exists to remove. The real invariant ("everything shipped is covered by the changeset record") holds by construction — the bump changeset coversOLD_PIN..SNAPSHOT, and objectui commits past the snapshot are next release's record.pnpm sdui:manifest时跑,CI 里从来不跑(#4690 的遗留决定) #5960 / ADR-0082 D4 — the ratchet runs here, because this lane is now a route that moves the pin. Installing a Playwright browser is fine on a lane that runs a few times a month; the sdui.manifest.json 的来源未定:声明一致性 ratchet 目前只在手工pnpm sdui:manifest时跑,CI 里从来不跑(#4690 的遗留决定) #5960 ruling is about not putting it on every matching PR.8aad9fd50b16(approvals inbox + bell-badge breakdown) #7308 — the objectui clone is deliberately full-depth. A shallow clone makes the digest degrade to a tip-subject-only changeset and still exit 0, which would cost the cut its frontend history silently.Two things I measured rather than assumed
1. The version pass's real file surface. I ran a full
pnpm run versionlocally on this train (1704 pending changesets) and captured the result: 76 modifiedpackage.json, 76 modifiedCHANGELOG.md, and.changeset/pre.json. Zero deletions, zero untracked files. Computed version:17.0.0-rc.6, matching the issue's prediction.Worth recording because it corrects the issue's stated surface: in pre mode
changeset versiondoes not delete the consumed changesets — it records them inpre.jsonand they are removed atchangeset pre exit. So a cut never races a lane PR over a.changeset/*.mdfile it wants to keep, which is a stronger disjointness guarantee than the issue claimed.That measurement is what the commit-building step's allowlist is derived from, and the allowlist is enforced, not described: only allowlisted paths are staged, every staged path is then re-checked against the same rule, and the worktree is asserted clean. If
pnpm run versionever grows a new output the run fails loudly there instead of pushing a surprise tomain. Verified in both directions — all 162 real surface paths accepted;packages/objectql/src/engine.ts,.github/workflows/ci.yml,scripts/*.mjs,content/docs/releases/v17.mdx,pnpm-lock.yamlandpackages/console/dist/**all blocked.2. The three
.changesetgates are forward-only DIFF gates, so they needed a base. Their--listmodes are audits that always exit 0 (--audit-stocksays in its own header that a non-zero exit there would make it a gate, which it is not), so wiring one of those in would have been a gate that cannot fail. I hand them--base "$SNAPSHOT_SHA"instead, which judges this run's own contribution — the@objectstack/consolechangeset the pin bump just emitted. That is a real gate: the digest resolves its bump level from objectui's declarations, so an objectuimajorwould otherwise promote all ~70 packages, and a digest bug emitting empty frontmatter would stall the release silently and greenly (#4898).$SNAPSHOT_SHAis a pinned sha, which #6129 warns against — but that warning is aboutpr-automation.yml's frozenbase.sha, a value that goes stale as main moves under a PR. Here the snapshot is by construction the parent of the commit being judged, so it is the merge base and cannot go stale. I also confirmed the three gates' self-tests scope their "exactly one call site" wiring assertions topr-automation.yml/lint.yml, so the new call sites do not trip them — all 377 assertions pass with this branch applied.Admin prerequisite (one-time, maintainer-only)
The lane pushes the version commit straight to
main, so the pushing identity must be on main's ruleset bypass list —contents: writealone does not put it there. Either:contents: writeon this repository for an account already on that bypass list, and store it as the repository secretRELEASE_PUSH_TOKEN.The workflow uses the secret when present and the Actions identity otherwise, so configuring either is enough and neither needs a workflow edit. Until one is configured the push step fails with exactly that message and nothing is published — the failure is before any irreversible step, and it is distinguished from a network fault in the error text. I did not implement the catch-up-PR fallback: that is the rc.3/rc.4 accident shape (#6170) and the issue rules it out.
Direct-to-main push is not structurally impossible here — it is an ordinary ruleset bypass configuration — so this is a prerequisite to satisfy, not a
needs_decision.What I could and could not verify locally
Executed, real output:
node scripts/check-workflow-status-functions.mjs --self-test+ real scan — 34 assertions; scan covered 24 workflow files including this one, 43 jobs, 9 job-levelif:readingneeds.*.outputs.*, all naming a status function.node scripts/check-required-contexts.mjs— pass (this workflow registers no required context; it is dispatch-only).pnpm check:nul-bytes— 6830 files, no raw control bytes; plus a targetedgrep -naPself-scan over both changed files.pnpm check:changeset-gate-self-tests— 377 assertions across all three gates, green with the new call sites present.--base HEAD~5) and the empty-diff case (--base HEAD, i.e. the "objectui had not moved, no bump commit" path). All exit 0.bash scripts/bump-objectui.sh --help— confirms thebump-objectui.sh SHApositional shape the workflow relies on.pnpm run versionend to end — the measurement above; the worktree was reset afterwards.yamlpackage: 1 trigger, 2 inputs, 1 job, 37 steps, no step name containing": "(the nested-mapping trap).run:block extracted andbash -nsyntax-checked — 20 blocks, all clean.pnpm check:fornode-version,adr-anchors,adr-links,docs-audit-scope,release-notes,role-word,org-identifier,merge-driver,quick-reference-counts,objectui-changeset,doc-authoring,empty-changeset— all pass.Not executable locally, and stated plainly:
actionlintandshellcheckare not installed in this container, so the YAML review is the parse above plus manual review againstrelease.yml's patterns, and the shell review isbash -nplus manual review. More importantly, I cannot execute the workflow end to end — no runner, noreleaseenvironment, no npm or push credentials. Nothing here proves the live behaviour of the push-retry loop, the Playwright install against objectui's build tree, or the publish steps.That is exactly what
dry_runis for, and it is why I made it stop later than the issue's acceptance criterion asks. It stops after the local squashed commit rather than after the version step, so the squash, the allowlist enforcement and the version assertion have all really executed, and the uploaded artifact is byte-for-byte the commit a real run would push. Please dispatch withdry_runchecked before the first real cut.Deliberate non-inclusions
release.yml'srelease-integritylane runs on every push tomainand requests the image once the version is on npm — the GitHub Release body 超 125k 上限:spec 的 rc 段已达 343k,RC 窗口内每次发布都会在 createGithubReleases 处失败(npm 已发出、Docker 被连带跳过) #4900 repair path doing its job, within a merge or two on this repo. Duplicating it here would be a second copy of an existing repair path;docker-publish.ymlcan be dispatched for one immediately. Named in the workflow header, the docs and the run summary rather than left to be discovered.if:and an in-shell assert inside each irreversible step. A droppedif:in a future edit would otherwise publish from a run a human asked not to publish.Releases nothing (workflow + docs only) →
skip-changeset.Generated by Claude Code