Repository navigation
cut-rc.yml refuses every cut since #9064: sync-docs-image-tags.mjs writes three paths outside the enforced release file surface #9518
Description
Found while rehearsing #9497 (the @changesets/cli v3 epic, #9465). It is not caused by that migration — it reproduces identically on today's @changesets/cli@2.31.1, so it is filed on its own.
What is broken
cut-rc.yml's "Build the single version commit" step stages an allowlisted set of paths and then asserts that nothing tracked is left modified-but-unstaged:
LEFT="$(git diff --name-only)"
if [ -n "$LEFT" ]; then
echo "::error::tracked files were modified but fall outside the release file surface, so the version commit would be incomplete. Refusing to push."
pnpm run version now ends with node scripts/sync-docs-image-tags.mjs, which rewrites three paths that the staging pathspec does not cover:
content/docs/deployment/self-hosting.mdx
content/docs/upgrading.mdx
docker/README.md
So LEFT is non-empty and the step exits 1. The cut refuses to push — correctly, by its own rule; the rule's input surface just went stale.
Measured
Throwaway clone of origin/main @ 3397d7ec9 (165 pending changesets), .changeset/pre.json absent, then changeset pre enter rc + the full pnpm run version pipeline, then cut-rc.yml's staging + assertions replayed verbatim:
@changesets/cli@2.31.1 (today's pin)
STAGED count: 156
allowlist regex: accepts every staged path
unstaged assertion: TRIPS on:
content/docs/deployment/self-hosting.mdx
content/docs/upgrading.mdx
docker/README.md
sync-docs-image-tags.mjs's own output for the same run:
docker/README.md
32:6 image-tag: 17.0.0 -> 18.0.0-rc.0
43:3 image-tag: 17.0.0 -> 18.0.0-rc.0
65:45 build-arg: 17.0.0 -> 18.0.0-rc.0
content/docs/deployment/self-hosting.mdx
76:3 image-tag: 17.0.0 -> 18.0.0-rc.0
94:3 image-tag: 17.0.0 -> 18.0.0-rc.0
145:6 image-tag: 17.0.0 -> 18.0.0-rc.0
163:20 npm-pin: 17.0.0 -> 18.0.0-rc.0
content/docs/upgrading.mdx
41:8 image-tag: 17.0.0 -> 18.0.0-rc.0
8 pin(s) across 3 surface(s)
The same replay under @changesets/cli@3.0.0 trips on exactly the same three paths, so the migration neither causes nor cures it.
Why nobody has seen it
sync-docs-image-tags.mjs was added to the version script by #9064 (2026-08-16). v17.0.0 shipped 2026-08-14. No cut has run since the rewriter joined the pipeline, so the assertion has never been reached with these three files dirty. The workflow's allowlist comment still describes a pnpm run version measured on the 17.0.0-rc.5 -> rc.6 train, before the rewriter existed.
release.yml's version-PR lane is unaffected: changesets/action@v1 runs a plain git add ., with no allowlist.
Fix shape
Per #9465 ruling 3 the allowlist follows the measurement. Widen the staging pathspec and the re-check regex in cut-rc.yml to cover the three docs/docker surfaces the version pass legitimately writes — ideally by naming them as the rewriter's declared output rather than by pattern, so the next surface sync-docs-image-tags.mjs learns about fails loudly here again.
Note for whoever takes this: #9498 rewrites this same allowlist for the v3 pre-mode file set. Whichever lands first should carry the other's change or hand it over explicitly — two PRs editing this block is the shape #9465 exists to prevent.
Activity
os-project-manager commentedon Aug 18, 2026
Claim: epic #9465, round 2 — adopted as sub-issue 5/5 of that epic
Session:
session_01HDA9nN6nXQngoQUAAzRdMbBranch:
claude/issue-9518-cut-rc-docs-image-tagsWorktree:
objectstack-issue-9518Domain:
domain:devxFile surface:
.github/workflows/cut-rc.yml(the staging pathspec, its re-check regex, and the allowlist comment that is now stale) (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus— small diff, but it is the gate that decides whether a release commit may be pushed, so it is not a mechanical card.Serial constraints cleared: #9498 rewrites this same allowlist block for the v3 pre-mode file set, and is currently held on a maintainer decision (release numbering), so there is no race — this lands first and #9498 rebases onto it, which is the handover the filer asked for. #9450 also names
cut-rc.yml(line 285's "range is walkable" preflight) but is an ungradedfindingwith no assignee and no branch, i.e. not in flight; it is a disjoint block and is not carried here.Filed by the #9497 rehearsal and adopted rather than left in the backlog because it is not a migration artifact: it reproduces on today's pinned
@changesets/cli@2.31.1, so the next RC cut is blocked right now, whatever happens to the v3 epic. Gradedpriority:p0and typedBugon that basis — the release lane refuses to push, and nobody would find out until a cut was attempted.Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
{ "issue": 9518, "status": "done", "branch": "claude/issue-9518-cut-rc-docs-image-tags", "pr": "https://github.com/objectstack-ai/objectstack/pull/9553", "premise_still_valid": true, "summary": "The card's premise held exactly and was INCOMPLETE in one way that matters. cut-rc.yml's staging pathspec and re-check regex now cover what pnpm run version actually writes. The three doc surfaces are RESOLVED at run time from SURFACES in scripts/check-docs-image-tag.mjs rather than copied: sync-docs-image-tags.mjs already imports that same declaration ('one list, two consumers'), the module is import-safe thanks to the entry-point guard #9064 added, and this step joins as the third consumer — so ruling 2's preferred route was available and taken. Measurement also found a FOURTH offending path the card does not name, packages/create-objectstack/src/templates/blank/objectstack.manifest.json, written by sync-template-versions.mjs (specVersion), and it is fixed here: allowlisting only three of four would leave the cut still refusing to push, i.e. would not satisfy the card. Per epic ruling 3 the allowlist follows the measurement. That fourth path is named literally beside its sibling objectstack.config.ts because sync-template-versions.mjs declares its targets but exports none of them and has no entry-point guard, so it cannot be read the way the docs gate can — filed as #9554. Deliberately NOT a wildcard: a content/docs page SURFACES does not name is still rejected. Two new guards refuse the cut if the declaration cannot be resolved or resolves empty. cut-rc.yml:285 untouched; no release performed.", "tests": "All at final commit 0bfd6134b (git rev-parse --short HEAD after the last commit). GATES, re-derived for the actual diff with `node scripts/pm/dispatch-gates.mjs .github/workflows/cut-rc.yml` — same five families as dispatch named, no new ones: check:node-version rc=0, check:required-contexts rc=0, check:shard-attestation rc=0, check:workflow-status-functions rc=0, plus check:nul-bytes rc=0 ('scanned 6141 text file(s) ... no raw ASCII control bytes'). UNION_FAIL=0 at 0bfd6134b. REPLAY, per the card: throwaway clone of origin/main @ b9f807734, no remotes, changeset pre enter rc, full `pnpm run version` on the pinned @changesets/cli@2.31.1 -> exit 0, cli 17.0.0 -> 18.0.0-rc.0, rewriter output reproduced the card's byte-for-byte ('8 pin(s) across 3 surface(s)', same line:col). git status --porcelain BEFORE staging = 160 entries (159 M + 1 ?? pre.json): 77 package.json, 76 CHANGELOG.md, protocol-version.ts, blank/objectstack.config.ts, blank/objectstack.manifest.json, docker/README.md, content/docs/upgrading.mdx, content/docs/deployment/self-hosting.mdx, .changeset/pre.json. The step's shell was EXTRACTED FROM THE YAML (not hand-copied) and run under `bash -e`, the runner's default shell for run:, with the dirty tree restored byte-identically before each leg. LEG 1 origin/main: exit 1, LEFT non-empty on FOUR paths (the card's three plus blank/objectstack.manifest.json). LEG 2 this PR: exit 0, STAGED=160, accepted-by-fixed-regex=157, accepted-by-SURFACES-list=3, 157+3=160, BAD=[] and LEFT=[] both EMPTY, commit built. LEG 3 (can it still fail): two synthetic dirty tracked files -> exit 1 naming README.md AND content/docs/ai/actions-as-tools.mdx — a content/docs page outside SURFACES is still rejected, which is the proof this is not a wildcard. LEG 4 SURFACES emptied -> exit 1 'resolved EMPTY ... Refusing to push'. LEG 5 module made unparseable -> exit 1 'could not resolve SURFACES ... Refusing to push'. Legs 3-5 are reverse verification of the new guards; expected direction was 'turns red' and that is what was observed in all three. No ablation-on-dist applies (no package build is involved). NO RELEASE: throwaway clone with no remotes, no publish, no tags, no pre-mode entry/exit on origin, no workflow dispatch.", "open_questions": [], "out_of_scope_findings": [ "filed as #9554: sync-template-versions.mjs declares its version-stamp targets (TEXT_STAMPS + a walk of src/templates/) but exports none of them and has no entry-point guard, so cut-rc.yml must restate two template paths literally — both hardcoding the template name 'blank', which re-opens this same defect the day a second template ships", "filed as #9555: any local `pnpm run version` rehearsal hangs FOREVER in this container — the shared checkout is shallow, so every changeset resolves to a parentless boundary commit and @changesets/git's getCommitsThatAddFiles loops on deepenCloneBy with no reachable remote. Cost ~2.5h here before diagnosis; includes the working scaffold (drop + re-add .changeset in two commits, net tree identical) that takes a resolution pass from never-terminating to 0.57s. Not a production defect: cut-rc.yml checks out with fetch-depth: 0", "not filed, reported for PM judgement: the `skip-changeset` label was applied by me and confirmed by READ-BACK — PR #9553 labels are ci/cd, size/s, skip-changeset. Written as the union of the bot's existing set, since issue_write's label write is a whole-set PUT and pr-automation.yml documents that race explicitly" ] }Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
ACCEPT. Verified against the branch and
origin/main, not against the report..github/workflows/cut-rc.yml, +72/−13. The fix for a fourth offending path lives inside that same file, so finding more than the card named did not widen the diff beyond its declared surface.Fixes #9518is the first body line; labelsci/cd, size/s, skip-changeset.SURFACESreally isexport constinscripts/check-docs-image-tag.mjs, andsync-docs-image-tags.mjsreally does import it. This step joining as the third consumer of one declaration is the shape the ruling asked for.grep -vxF -fagainst the very list that was staged — whole-line, fixed-string. The PR body's own reason for that choice is the kind of detail that decides correctness later: an unescaped ERE alternation spellingREADME.mdwould have acceptedREADMEXmd.content/docspageSURFACESdoes not name; legs 4 and 5 show the two new guards refuse the cut when the declaration is unresolvable or empty. A gate that can no longer fail would have been worse than the bug — that is why it was demanded before the work started, and it was answered with output rather than argument.Check Changesetfailure at 09:23 is superseded by its 09:27 re-run,skipped, afterskip-changesetlanded. Everything else issuccessor skipped by path filter, as a workflow-only diff should be.Lint & Repo Gates,TypeScript Type Check,Console Pin Freshness,Part-ofand the same-issue guard all green.skip-changesetis the right call and was justified from the diff rather than applied to clear a red: one workflow file, no package published, nothing shipped to consumers.One correction, and it is against me. I started to flag the fourth path's attribution as wrong — nothing in
scripts/sync-template-versions.mjsnamedobjectstack.manifest.jsonorTEXT_STAMPS. I was reading the shared checkout, whose HEAD sits at this morning's5904b0500. Onorigin/main(b348ac2c2) that script has grown from 3,388 to 10,818 bytes and does exactly what the report says. The attribution is correct; the stale read was mine. Recording it because this repo's own rule — verify againstorigin/main, never the shared working tree — exists precisely for the near-miss I just had, and a reviewer who skips it publishes a confident, wrong correction.Landing: flipping to ready and enabling auto-merge. #9498 rebases onto this once it lands, per the sequencing on #9465.
Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Landed and verified on
origin/mainas955ccf20d— fix(ci): stage what the version pass actually writes in cut-rc.yml (#9553).Checked by content rather than by sha ancestry, since the queue lands changes as a squash and the reviewed sha is therefore not an ancestor of
main. Onorigin/main:.github/workflows/cut-rc.yml:SURFACE_LISTresolved at run time (line 550) with both guards — unresolvable (553) and empty (560) — present;mapfile -t DOCS_SURFACESfeeding the staging pathspec (564);packages/create-objectstack/src/templates/blank/objectstack.manifest.json, in the allowlist (575);grep -vxF -f "$SURFACE_LIST"(592).What this unblocks: the next RC cut can push again. It had been refusing on every attempt since #9064 joined
sync-docs-image-tags.mjsto theversionscript, and nobody would have found out until someone tried to cut — which is why this was graded p0 the moment the rehearsal surfaced it, ahead of the migration that found it.Follow-ups left open deliberately, both filed by the same run: #9554 (
sync-template-versions.mjsdeclares its stamp targets but exports none and has no entry-point guard, so this workflow must still name two template paths literally — and both hardcodeblank, which re-opens this defect the day a second template ships) and #9555 (localpnpm run versionrehearsals hang forever in this container on a shallow checkout; carries the working scaffold). Neither blocks anything today.Closing as completed.
Generated by Claude Code