Skip to content

release: snapshot one-click rc cut — a cut-rc workflow that bumps, versions, gates and publishes from ONE SHA, ending the two-treadmill freeze dance #7447

Description

@os-zhuang

Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): approved for immediate implementation and dispatch. Background: cutting rc.6 today required chasing objectui main across four pin-bump laps (8aad9fd9b9fa49cfeb3781b6188dbcd3e02, every lap overtaken) and would have needed ~40 min of coordinated freezes across two repos. The root causes are design, not discipline: (1) the release path checks pin freshness against a moving objectui HEAD; (2) the standing Version Packages PR (#6208) is force-refreshed on every main push, so its CI can never finish on a busy day, and the version→publish gap invites content/record skew.

The design (adopted)

New workflow .github/workflows/cut-rc.ymlrc prereleases only. One human dispatch, one snapshot, zero freezes:

  1. Trigger: workflow_dispatch ONLY, input version = the expected resulting version (e.g. 17.0.0-rc.6), environment: release, concurrency group cut-rc (no parallel cuts). This preserves the 2026-08-07 ruling verbatim: no push, bot, queue or schedule can synthesize the event; the human types the version.
  2. Snapshot: checkout objectstack main, record SNAPSHOT_SHA. Resolve objectui main HEAD once (git ls-remote), record OBJECTUI_SHA. Everything downstream uses these two values; both repos may keep moving freely.
  3. Pin bump: clone objectui (full enough for the digest walk — see traps below), run scripts/bump-objectui.sh $OBJECTUI_SHA with OBJECTUI_ROOT set, then pnpm objectui:build, playwright install chromium-headless-shell (allowed in CI — this is not a dispatch container), pnpm sdui:manifest (the ADR-0082 D4 ratchet; mandatory second half of every pin move, sdui.manifest.json 的来源未定:声明一致性 ratchet 目前只在手工 pnpm sdui:manifest 时跑,CI 里从来不跑(#4690 的遗留决定) #5960), pnpm check:console-sha.
  4. Pin self-consistency, NOT liveness: inside the run, assert .objectui-sha == $OBJECTUI_SHA (inline compare). ⛔ Do NOT call check:objectui-pin-fresh here and do NOT change that script's semantics — objectui moving during the run must not fail the cut. The Release process: prevent frontend (objectui) changesets being missed when the console pin lags #3340 invariant this preserves is "everything shipped is covered by the changeset record" (true by construction: the bump changeset covers OLD_PIN..$OBJECTUI_SHA); commits past the snapshot are simply next release's record.
  5. Pre-version gates (they read .changeset/*, so they run BEFORE versioning): check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-nul-bytes.
  6. Version: pnpm run version (⚠️ the repo script — changeset version && sync-protocol-version.mjs && sync-template-versions.mjs — never raw changeset version). Pre mode (.changeset/pre.json, tag rc) is committed state and applies automatically. Then assert computed version (packages/cli/package.json) == the dispatch input; mismatch fails before anything irreversible.
  7. Land the version commit on main FIRST, publish it SECOND — preserving release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170's invariant ("the publish lane may only ever ship a commit that is ALREADY on main") to the letter:
    • Primary route: push the single squashed version commit (bump + version output; file surface is package.jsons / CHANGELOGs / .changeset/* / .objectui-sha — disjoint from lane PRs by construction) directly to main using a bypass credential, with a bounded fetch-rebase-retry loop for races. Admin prerequisite (maintainer, one-time): add a bypass actor for this push — either the Actions app on the ruleset bypass list scoped to this workflow, or a fine-grained PAT stored as RELEASE_PUSH_TOKEN for an account on the bypass list.
    • If bypass turns out impossible in this org: STOP and report needs_decision with the measured constraint — the fallback (publish from a release ref + catch-up PR) is the rc.3/rc.4 accident shape (release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170) and is NOT to be adopted silently.
  8. Publish: changeset publish + tag push from the just-landed main commit, NPM_TOKEN via the release environment — mirror release.yml's publish job for secrets/guard patterns (read its publish: job before writing yours; its dispatch guards are the house style).
  9. Docs: docs/releases-maintenance.md gains a "cutting an rc (snapshot flow)" section; the existing Version-PR flow is re-scoped as GA only (board-clearing, No producer files the final console bump card at the release-cut moment — the #6162 chore is keyed to a settled window, and the card it does file never reaches the release board #7275-A cut precondition, human changelog review all stay there). One cross-reference line, no edits to release.yml's lanes.

Measured traps the implementer inherits (all hit today, all with receipts in session)

  • bump-objectui.sh with no sha argument pins the LOCAL objectui checkout's HEAD — stale by construction (chore: pin the console to objectui main 8aad9fd50b16 (approvals inbox + bell-badge breakdown) #7308 note 1). Always pass the resolved sha.
  • The changeset digest exits 2 on an unwalkable shallow range; deepen the objectui clone until OLD_PIN is reachable (--depth=250 sufficed at ~70 commits; fetch-depth: 0 is simplest in CI).
  • A fresh workspace must build local dists before pnpm objectui:build: the console's vite alias table (objectui#4103) resolves @objectstack/client → local packages/client/dist, whose DTS needs @objectstack/core built. pnpm --filter "@objectstack/client..." build (dependency closure) is the fix; @objectstack/spec build is also required for generated-artifact reads.
  • pnpm sdui:manifest needs a Playwright chromium; in CI install it normally. (The scratchpad symlink workaround in docs/releases-maintenance.md is for agent dispatch containers only.)
  • Expected ratchet state today: 62 spec-only divergences == accepted baseline, 0 missing blocks — four consecutive laps confirmed no declaration drift from objectui 8aad9fd..bcd3e02.

Boundaries (⛔)

  • No second publish path reachable without workflow_dispatch + environment: release. The 2026-08-07 ruling and release.yml's lane-split comments are binding context — read them first.
  • release.yml's three existing lanes and check-objectui-pin-fresh.mjs semantics are OUT of scope — GA keeps them unchanged.
  • content/docs/releases/** untouched (standing rule).
  • Releases nothing → skip-changeset label on the PR, per the Check Changeset gate's route 2.

Acceptance

  • Workflow YAML passes actionlint/self-review; every guard failure path exits before changeset publish.
  • A dry-run mode (inputs.dry_run) that stops after step 6 and uploads the would-be version diff as an artifact — so the first real dispatch is not the first execution.
  • Docs section written; PR body walks the sequence and names the admin prerequisite for the maintainer.

Refs: #6170 / #3340 (invariants), #5960 (ratchet on-demand ruling), #7308 (previous bump lap + traps), #7275 (GA cut precondition, stays), #6162 (steward bump cards — unaffected; they keep feeding the advisory pin lag), #4898 (empty-changeset stall), 2026-08-07 manual-publish ruling (release.yml header).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions