Uh oh!
There was an error while loading. Please reload this page.
ci(release): validate the post-version tree before the version PR is opened - #11964
Conversation
…opened The "chore: version packages" PR runs no CI: changesets/action opens and force-pushes it with the default GITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. So the largest machine-generated diff of the release cycle is the only commit that reaches main without passing the gates. Two files in this repository already state that mechanism -- lint.yml's docs-image-tag block and scripts/sync-protocol-version.mjs's header -- and each states it while explaining a rewriter built to work around it. Render the post-version tree in the version-pr job, validate it, and put it back before changesets/action reads it. The gate list is derived from the change set by scripts/pm/dispatch-gates.mjs rather than written down, so a fifth version-time rewriter pulls in its surface's gates on the day it lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…gates that read it Replaces the first draft's derive-and-run selector. Feeding the change set to scripts/pm/dispatch-gates.mjs named 49 families and ran 44 of them green in 113 s, but running the tool in CI needs check-self-test-wired.mjs to grow a new ledger shape, and check-dispatch-gates.mjs already records the reviewed position that the live derivation has no verdict for CI to hold. That is a maintainer's call, not a rider on this card. What ships instead is two halves: a shape assertion that resolves the doc and template surfaces at run time from the declarations the rewriters themselves read (cut-rc.yml's idiom, same predicate), so a new version-time output reddens this lane on the day it lands; and the eight gates whose corpus is what the version step actually writes, measured at 7 s. Also corrects this job's comment, which called `pnpm run version` three rewriters after #9064 made it four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Reverse verification caught the routing: with protocol-version.ts missing from the shape filter's pattern, a major train's output fell into the "wrote outside the reviewed post-version surface" branch -- red for the right reason and wrong about why, on a lane whose refusals are supposed to name the remedy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 25, 2026
ACCEPT, and option A on the open question. Flipping out of draft; arming once green. Verified against the branch, not read back
That last one is what I asked for in Zone 2 — the card said ⭐ The restore is load-bearing, and you found out why rather than assuming
That reframes The price measurement, with the control that makes it mean somethingPost-version farm 117 s, 44 green / 6 red; the identical sweep on the pre-version tree: 117 s, 44 green / the SAME 6 red. So the version step changes no gate's verdict on this train, and the 6 are environmental (5 asking for And the scope conclusion is genuinely this repo's: 313 paths, four classes, ⭐ Limb (b) failing its first run is the best evidence in the report
A reverse-verification limb that goes red can be banked without looking. You looked, found the red was arriving through the wrong branch, and fixed the classification in Extracting the three step scripts out of the YAML with a parser rather than retyping them, then running them with the Actions context variables, is the correct substitute for a CI run — and the proof that the gates read the rendered tree is exactly right:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11945
The
chore: version packagesPR runs no CI and cannot be made to:changesets/action@v1opens and force-pushes it with the defaultGITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. So the largest machine-generated diff of the release cycle is the only commit that reachesmainwithout passing the gates.This renders the post-version tree inside the
version-prjob, validates it, and puts it back beforechangesets/actionreads it — maintainer ruling objectui#5397 (2026-08-22, 「接受所有」), option A. No PAT / GitHub App token, no per-artifact guarantee.Premise re-verified on current
origin/mainConfirmed, not inherited.
release.ymlline 313 isuses: changesets/action@v1, and line 342 hands itGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}. Two files in this repository already state the mechanism in their own words:.github/workflows/lint.yml:1244-1252— "changeset versionbumps packages/cli on a release PR that (persync-template-versions.mjs's own header) gets NO CI becausechangesets/actionopens it with the default GITHUB_TOKEN — so the bump merged green and the gate reddened on the NEXT ordinary PR, naming files that author never touched."scripts/sync-protocol-version.mjs, header — "release PRs opened by changesets/action with the default GITHUB_TOKEN do not trigger CI (GitHub's anti-recursion rule), so the guard only fired AFTER the merge. Fixing the value at version time is the only spot that cannot be skipped."One citation in the card has rotted. The card cites
lint.yml:988-991; that range is now the doc-frontmatter gate's prose and says nothing about release PRs. Triage'slint.yml:1249is the live line. Quoted above from the current file.The surface, measured here (2026-08-25, at
3689991d2)The sibling's scope conclusion does not transfer, and the reason is concrete:
pnpm run versionhere is four rewriters, and one of them writes TypeScript. Rendered against the real tree (158 pending changesets,@objectstack/cli17.2.0 → 17.3.0), it moved 313 paths, in four classes and no others:.changeset/*.md*/package.json"version":— not one dependency range moved*/CHANGELOG.mdcontent/docs/deployment/self-hosting.mdx,content/docs/upgrading.mdx,docker/README.mdTwo further classes are major-boundary only and did not move:
packages/spec/src/kernel/protocol-version.ts(a runtime source file — which is why "the version step cannot move source" is false here) and, per bundled template,objectstack.config.ts/objectstack.manifest.json. A major cannot reach this lane whilecheck-changeset-no-major.mjsholds, so they are dormant rather than covered, and the workflow says so out loud rather than validating a surface with no gate behind it.cut-rc.yml's allowlist block carries the same measurement for the RC lane and agrees path-for-path.All four sync scripts still run — checked, not assumed. This PR also corrects the
version-prjob's own comment, which still calledpnpm run versionthree rewriters after #9064 made it four.What ships: two halves
Shape — every moved path must fall inside the reviewed surface, with the doc and template halves resolved at run time from the declarations the rewriters themselves read (
SURFACESincheck-docs-image-tag.mjs,stampedPaths()insync-template-versions.mjs), using the same predicatecut-rc.ymlapplies to the same surface, on its stated principle that "a fourth literal is a fourth contract". This is the half that ends the treadmill: a fifth rewriter, or an existing one growing an output, reddens this lane on the day it lands.Content — the eight gates whose corpus is what the version step actually writes:
check:docs-image-tag,check:docs-image-tag-sync,check:template-version-sync,check:nul-bytes,check:release-notes,check:release-page-status,check-release-section-coverage.mjs,check:published-readme-links. Measured at 7 s on the rendered tree, in this job's own state (install, no build).Plus a restore step (
if: always()) that asserts the restoration, becausechangesets/actionpicks its branch from the working tree: with.changeset/consumed and nopublish:input on this lane, v1 takescase !hasChangesets && !hasPublishScript, logs "No changesets present or were removed by merging release PR" and returns — a permanent silent no-op with nothing red anywhere.Which per-artifact rewriters this makes redundant
None are removed here, and none should be — they still produce the correct value, which is worth more than detecting a wrong one. What this retires is the obligation each one carries to be its own gate:
sync-protocol-version.mjs(#2769),sync-template-versions.mjs, andsync-docs-image-tags.mjs(#9064) were each built because the gate that judges their output structurally could not fire on the release PR.check:docs-image-tagnow runs on the post-version tree, which is exactly the run #9064 says was impossible. The next version-time output needs a rewriter; it no longer needs to be trusted without one.Deliberately not run, and why it is not an omission
Fed this 313-path change set,
scripts/pm/dispatch-gates.mjsnames 49 families; 44 run without a build in 113 s, and every one is green on the post-version tree. The control that makes that number mean something: the identical sweep on the pre-version tree returns the identical verdicts — 44 green, the same 6 red — so the version step changes no gate's mind on this train, and those 6 are environment (5 need a build, 1 needs full git history), not findings.It is mostly restatement: a family is derived because a package's
package.jsonmoved, and what moved inside it was the"version"key, while the gate reads that package's source, whichci.ymljudged on this very commit minutes earlier.Running that selector in CI was drafted and withdrawn — see the open question below.
The five build-requiring families (
check:i18n,check:i18n-coverage,check:dev-prereqs,check:doc-formula-expressions,check:doc-security-posture) would in any case re-add to this lane the ~9 minutes of build that this job's own comment records as deliberately moved to thepublishjob, to gate a manifest's"version"key. What they read is not what the version step wrote: the two doc gates judge formula fences and security-posture prose, and the onlycontent/**lines this step can move are the 8 concrete pins above.Verification
Because
release.ymlruns only onschedule/workflow_dispatch, CI cannot exercise these steps on this PR. So they were run locally as shipped — the three step scripts were extracted fromrelease.ymlitself with a YAML parser and executed with the Actions context variables they read, against the real tree.Green path, on the final commit
7a29541a9:That the gates really judge the rendered tree rather than
main's is not asserted but observed:check-release-section-coverage.mjsreported@objectstack/spec 17.3.0 is published (it has a ## 17.3.0 section in packages/spec/CHANGELOG.md)— a section that exists only after the render.Reverse verification — each limb mutates, proves the mutation on disk, runs the shipped validate step, and restores under a trap:
unexpected: packages/core/src/invented-version-output.tsprotocol-version.tspresent in the moved list (count 1)unvalidated: … crossed a MAJOR boundary … which this lane has no gate forobjectstack:17.3.0occurrences indocker/README.md2 → 1fails 1 gate(s) …: pnpm check:docs-image-tagThe second limb is why there is a third commit: on the first pass it fell into the "wrote outside the reviewed surface" branch — red for the right reason and wrong about why, on a lane whose refusals are supposed to name the remedy.
Gates for this diff, derived live with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat7a29541a9and all green (16/16),check:workflow-status-functions,check:required-contexts,check:step-collectors,check:aggregator-roster,check:shard-attestation,check:nul-bytesandcheck-self-test-wired.mjsamong them.Open question for the maintainer
Should the release lane select its gates by derivation instead of by list? Feeding the change set to
scripts/pm/dispatch-gates.mjsgeneralises perfectly — a new version-time output pulls in its surface's gates with nothing here to update — and it is affordable (113 s + 18 s for the tool's own self-test, four times a day). It was withdrawn because shipping it needs two things this card should not decide alone:scripts/check-self-test-wired.mjsmust grow a new ledger shape (the tool's--self-testcannot be wired directly — its own self-test refuses the direct spelling as a fresh escapable literal, measured), andcheck-dispatch-gates.mjs's header already records the reviewed position that "there is no verdict in [the live derivation] for CI to hold". Both forms of the derivation name the same 49 families on this change set, so the measurement is in hand whenever you want it.Siblings
cloudandobjectoscould not be audited: both are outside this session's repository allowlist (objectstack-ai/objectstack,objectstack-ai/objectuionly), and attachingobjectstack-ai/cloudwas refused — "you don't have access". No card filed for either, per the dispatch order. The one grep each still needs doing by a session that can read them.skip-changeset: workflow-only change, nothing published moves.Generated by Claude Code