Repository navigation
cut-rc.yml's release-file allowlist still restates the two literal blank template paths, and the precondition its comment names for reading them instead landed in #9648 #9830
Description
Activity
Triage first-touch (triage seat, session
session_01JEhkB6ShK2HHDzozy7Qwgz):pm:queue·domain:devx· type Task.Premises verified on
origin/main@07bfd31cb: the twoblanktemplate literals sit at.github/workflows/cut-rc.yml:596-597(and again in thegrep -vEre-check at:613), and the precondition the in-file comment was waiting on has landed —scripts/sync-template-versions.mjs:187exportsstampedPaths()(header at:69/:81documents it as the export for cut-rc.yml's allowlist; #9648 merged, its test pins the two-template failure). No open PR touchescut-rc.yml.Lands in:
.github/workflows/cut-rc.yml"Build the single version commit" step — mirror the doc half'sSURFACE_LISTpattern (grep -vxF -foffstampedPaths()), removing both literals and theirgrep -vEalternatives. Task not Bug: with a single template the literals still equal the derivation and every cut passes today; this removes the restatement before a second template turns it into a failed release cut. Same derive-don't-restate family as #9840 (lint.yml comment) and #9829 (ci.yml filter) — three independent fixes, no shared files, mutually reinforcing precedents.Blocked-by: none. Size/model suggestion: S, sonnet–opus.
Generated by Claude Code
{ "issue": 9830, "status": "done", "branch": "claude/issue-9830-cutrc-release-file-allowlist", "pr": "https://github.com/objectstack-ai/objectstack/pull/10569", "premise_still_valid": true, "summary": "Re-derived on origin/main @ d1ba685ec8: the restatement is exactly as the card describes, only relocated -- the two `blank` literals are at cut-rc.yml:642-643 (card/triage said 596-597) and repeated in the grep -vE re-check at :659 (triage said :613); cut-rc.yml moved under the card via #10143 and #10494, both in the objectui-pin steps, neither touching this block. RULING: COLLAPSE. Establishing that the two lists are one list, not two contracts of different width: stampedPaths() and the stamper's own main() read the same findTemplateDirs()/TEMPLATE_PKG_FILE/TEXT_STAMPS (the module header states TEMPLATE_PKG_FILE is named at module scope so the two 'cannot drift'); the allowlist's own comment states its intent is exactness, not width ('Note what this deliberately is NOT: a wildcard'); and the equality holds only while one template exists, which nobody maintains -- findTemplateDirs() exists because the template set is deliberately not curated. TEMPLATE_LIST is now resolved from stampedPaths() mirroring the SURFACE_LIST block above it (both failure contracts carried over: resolution failure and EMPTY are hard failures), the pathspec's two literals become \"${TEMPLATE_SURFACES[@]}\", and the re-check drops the two alternations for a grep -vxF -f whole-line exact filter. FENCE: the #9465 v3 fence is adjacent and deliberately not crossed -- the .changeset pathspec line is byte-identical, the ^\\.changeset/ alternation inside the re-check regex is byte-identical (only the two template alternations were removed), the v3 WHAT-MAY-BE-IN-IT measurement block above the step is untouched, and release.yml / pr-automation.yml / root package.json are untouched. The rewiring landed here is the one #9554's dev handed to the epic verbatim on 2026-08-18 as ready-to-apply lines; it has sat unclaimed since, and the epic's only open sub-issue (#9500) observes a live RC window and writes nothing. Flagged in open_questions for PM confirmation rather than assumed silently.", "tests": "All legs at HEAD 32d210bde3 (the final commit); gate union re-derived from that same commit. CANNOT BE VERIFIED LOCALLY: cut-rc.yml end-to-end -- workflow_dispatch-only, running it performs a release; no leg ran the workflow. WHAT DID RUN: the allowlist's shipped bytes, extracted from the committed YAML by anchor and dedented (never retyped) for both the origin/main form (OLD) and this PR's form (NEW), executed against throwaway git repos shaped like the release tree, whose template files are written by the REAL scripts/sync-template-versions.mjs; the fixture reproduces the v3 cut shape (consumed changeset moved into .changeset/pre/, untracked pre.json, package.json + CHANGELOG.md, the three declared doc surfaces, protocol-version.ts at a major boundary). (1) UNCHANGED BEHAVIOUR, one-template tree: exit code + staged path set + ::error:: lines captured for both forms are byte-identical -- `md5sum` reports 9c8bfbbc6927e17dc49af8b4f79efacd for both behaviour.1T.OLD.txt and behaviour.1T.NEW.txt (17 lines each, non-degenerate); both exit=0, both staging the same 11 paths. The NEW form additionally PRINTS 'template surfaces declared by stampedPaths() (3):' with the three paths, mirroring the doc half's existing echo -- the only output difference, and not a verdict. (2) TWO-TEMPLATE POSITIVE CONTROL: OLD exit=1 with '::error::tracked files were modified but fall outside the release file surface, so the version commit would be incomplete. Refusing to push.' naming '::error:: unstaged: .../templates/starter/objectstack.config.ts' and '.../starter/objectstack.manifest.json'; NEW exit=0, no ::error::, 14 paths staged including both starter text stamps. That is #9648's predicted failure reproduced against the workflow half. (3) NOT A WILDCARD -- three negative controls on NEW, all exit=1: a tracked templates/blank/README.md (inside a template dir, not named by stampedPaths()) -> '::error:: unstaged: packages/create-objectstack/src/templates/blank/README.md'; a tracked write outside every surface -> '::error:: unstaged: packages/spec/src/kernel/other.ts'; a pre-staged unallowlisted path exercising the BAD re-check -> '::error:: unexpected: rogue-preexisting.ts'. (4) ABLATION, both legs REBUILT-EQUIVALENT and confirmed ON DISK BY ANCHOR COUNT, never by an editor's exit code. No build/dist is involved -- the workflow imports the .mjs source directly at run time, so the mutation is live the moment it is on disk; the on-disk confirmation is still done because an editor's exit code proves nothing. Mutations were applied to the FIXTURE's copy of sync-template-versions.mjs (the repo file was never edited) and COMMITTED inside the fixture, so the ablated file is not itself an unstaged path -- an earlier uncommitted run made the OLD control red for the wrong reason (the mutated script showed up as 'unstaged: scripts/sync-template-versions.mjs'), and that run was discarded rather than reported. A1 un-export: '^export function stampedPaths(' 1 -> 0 and '^function stampedPaths(' = 1 on disk; NEW exit=1 '::error::could not resolve stampedPaths() from scripts/sync-template-versions.mjs, so the template half of the release file surface is unknown. Refusing to push.'; OLD exit=0 with no objection. A2 inject 'return []; // ABLATION_EMPTY_RETURN' as the first statement: marker count 0 -> 1 on disk; NEW exit=1 '::error::stampedPaths() in scripts/sync-template-versions.mjs resolved EMPTY, so no template surface would be staged even though the version pass stamps them. Refusing to push.'; OLD exit=0 with no objection. The OLD column is the measurement: origin/main's form has no declaration to break. RESTORE leg for both: file restored from the repo original, `md5sum -c` reports 'scripts/sync-template-versions.mjs: OK', '^export function stampedPaths(' back to 1, ABLATION_EMPTY_RETURN back to 0, fixture tree clean apart from the version pass, NEW back to exit=0 with no ::error::. GATES: node scripts/pm/dispatch-gates.mjs with NO path arguments, run again after the final commit -- 'change set derived from git -- 1 path(s) vs merge base d1ba685ec of origin/main and HEAD (committed 1, working tree 0, untracked 0)', .github/workflows/cut-rc.yml, 6 matched families. All green, quoting each gate's own verdict line, exit codes captured before any pipe: 'check-node-version: OK (32 setup-node step(s) across 26 workflow(s), all on Node 22).' | check-required-contexts run green plus '✓ check-required-contexts --self-test: 124 assertions ...' | '✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).' plus '✓ check-shard-attestation --self-test: 92 assertions ...' | 'check-workflow-status-functions: OK (scanned 26 workflow file(s), 49 job(s), 24 job-level if: expression(s); 10 read needs.*.outputs.*, all naming a status function).' | 'check-nul-bytes: OK (scanned 6181 text file(s) -- 6181 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).' plus a direct grep -naP over the edited file (exit 1, no match). YAML parses via the yaml package: jobs ['cut'] and 35 named steps unchanged, 'Build the single version commit' present. pnpm install ran once through scripts/pm/os-verify-lock.sh (three of the six gates need the yaml package): 'os-verify-lock: VERDICT command-exit 0 · held the lock 5s · waited 69s'. CI convergence is not awaited per the 2026-08-10 ruling; no gate job conclusion is claimed here.", "open_questions": [ { "question": "Does this edit count as crossing #9465's `@changesets/cli` v3 fence? The dispatch fenced \"cut-rc.yml's changeset-related steps\". The edited lines live inside the `Build the single version commit` step, which stages `.changeset` among other things, but the lines themselves are the template half of the release-file allowlist and touch no changeset semantics. I proceeded and made the boundary auditable rather than stopping.", "options": [ "A. Accept as landed: the fence is about changeset SEMANTICS (the v3 migration surface), and the diff leaves every changeset byte identical -- the `.changeset` pathspec line, the `^\\.changeset/` alternation, and the whole v3 measurement prose block. Precedent: PR #10494 merged into this same file today declaring 'Nothing here touches the changeset steps', and #9498 (the atomic migration) is merged and closed, leaving only #9500, an observation card that writes nothing.", "B. Treat the whole `Build the single version commit` step as fenced and hold the PR until the epic PM signs off. Note the epic was handed these exact lines on 2026-08-18 by #9554's dev and has not acted on them in three days; the 2026-08-18 'a fence I have been over-applying' comment on #9465 asked the epic to settle whether its parenthetical or the whole file is the fence, and that question is still unanswered.", "C. Revert and re-file as a sub-issue of #9465 so the epic lands it." ], "recommendation": "A. The fence exists to keep two open PRs from mutating one release machine at once; there is no such PR -- the migration is merged and the only open sub-issue writes nothing. The diff is confined to the template half of the allowlist and every changeset-related byte is provably unchanged, which is checkable from the diff rather than taken on trust. B costs the fix another indefinite hold on a question the epic has already left open for three days; C reopens closed work. If the PM prefers B, the PR is draft and nothing has merged." } ], "out_of_scope_findings": [ "filed as #10570: guard-main-checkout-bash.sh has no shell-comment handling, so a `>` inside a `#` comment is read as a redirect and false-BLOCKS the command (fail-closed, opposite direction to the sibling observation #10409). Reproduced against the committed hook with a control isolating the comment as the cause: same command + same cwd, hook-exit 2 with the comment, hook-exit 0 without it. Unassigned, no labels." ] }
Generated by Claude Code
Found while working #9348 (reading how the version pass is validated). Out of that card's scope — it adds a
--self-testto the rewriter and touches no workflow butlint.yml. Filed unassigned.What was observed
cut-rc.yml'sBuild the single version commitstep enforces a release-file allowlist. Its doc half is read from the declaration:node --input-type=module \ -e 'import { SURFACES } from "./scripts/check-docs-image-tag.mjs"; for (const s of SURFACES) console.log(s.file);'Its template half is restated, two literals, both hard-coding the template name
blank:with the same two paths repeated in the
grep -vEre-check below it.The comment above that block explains why, and its stated precondition is no longer true:
PR #9648 (card #9554) gave it exactly those two things.
scripts/sync-template-versions.mjsnow exportsstampedPaths()— documented in its own header as "the exportcut-rc.yml's release-file allowlist is for" — plusTEXT_STAMPS,TEMPLATE_DIR,TEMPLATE_PKG_FILEandfindTemplateDirs(), and carries the #9064 entry-point guard, so importing it rewrites nothing. Verified on9ff11921a: the export resolves andpackages/create-objectstack/src/template-version-stamps.test.tspins that an import against a stale two-template checkout leaves it byte-identical.Why it matters, and the honest severity
Today it is inert. The repo ships one template, so the two literals equal what
stampedPaths()derives, and every cut passes.The day a second template ships it is a failed release cut, not a soft failure. The allowlist is enforced in two directions: paths outside it are refused, and — the half that bites here — anything the version pass modified but left unstaged fails the step:
sync-template-versions.mjsdiscovers templates by walkingsrc/templates/, deliberately ("a template added tomorrow is covered on the day it lands"), so it will stamp the new template's three surfaces; the pathspec will stage none of them; the cut refuses to push. Nothing is red until someone attempts a release.#9648's test already states this as an assertion, against a two-template fixture:
So the failure is predicted and pinned; only the workflow half is outstanding.
Shape of a fix (not a ruling)
Mirror the doc half, on the terms the comment already sets out:
node --input-type=module \ -e 'import { stampedPaths } from "./scripts/sync-template-versions.mjs"; for (const p of stampedPaths()) console.log(p);'then stage and re-check by whole-line exact match (
grep -vxF -f), the waySURFACE_LISTalready is. Two details worth carrying over from the doc half: an empty resolved list must be a hard failure rather than an empty allowlist (stampedPaths()already throws instead of returning[], so this is a resolution failure only), andstampedPaths()reports the per-templatepackage.jsontoo, which the existing*package.jsonpattern already covers — so the union is a superset of today's behaviour, never a narrowing. The twoblankliterals and theirgrep -vEalternatives come out in the same edit; leaving them in would re-seed the restatement.Refs: #9554 / PR #9648 (landed the export and the guard) · #9348 (found here) · #9518 (the last time this allowlist refused every cut) · #9064 (the doc half's precedent).
Generated by Claude Code