Skip to content

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

@os-steve

Found while working #9348 (reading how the version pass is validated). Out of that card's scope — it adds a --self-test to the rewriter and touches no workflow but lint.yml. Filed unassigned.

What was observed

cut-rc.yml's Build the single version commit step 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:

git add -A -- \
  ... \
  packages/create-objectstack/src/templates/blank/objectstack.config.ts \
  packages/create-objectstack/src/templates/blank/objectstack.manifest.json \
  ...

with the same two paths repeated in the grep -vE re-check below it.

The comment above that block explains why, and its stated precondition is no longer true:

The manifest is named literally, like its sibling config.ts, rather than read the way the doc surfaces below are: sync-template-versions.mjs does declare its targets (a TEXT_STAMPS table plus a walk of src/templates/), but exports none of them and has no entry-point guard, so importing it would RUN the sync instead of reading its list. Give it those two things and this pathspec should read from it too.

PR #9648 (card #9554) gave it exactly those two things. scripts/sync-template-versions.mjs now exports stampedPaths() — documented in its own header as "the export cut-rc.yml's release-file allowlist is for" — plus TEXT_STAMPS, TEMPLATE_DIR, TEMPLATE_PKG_FILE and findTemplateDirs(), and carries the #9064 entry-point guard, so importing it rewrites nothing. Verified on 9ff11921a: the export resolves and packages/create-objectstack/src/template-version-stamps.test.ts pins 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:

::error::tracked files were modified but fall outside the release file surface, so the version commit would be incomplete. Refusing to push.

sync-template-versions.mjs discovers templates by walking src/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:

The finding, stated as an assertion: the pair cut-rc.yml spells literally is a STRICT SUBSET of what the version pass actually writes as soon as a second template exists.

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 way SURFACE_LIST already 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), and stampedPaths() reports the per-template package.json too, which the existing *package.json pattern already covers — so the union is a superset of today's behaviour, never a narrowing. The two blank literals and their grep -vE alternatives 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

Activity

  1. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    Triage first-touch (triage seat, session session_01JEhkB6ShK2HHDzozy7Qwgz): pm:queue · domain:devx · type Task.

    Premises verified on origin/main @ 07bfd31cb: the two blank template literals sit at .github/workflows/cut-rc.yml:596-597 (and again in the grep -vE re-check at :613), and the precondition the in-file comment was waiting on has landed — scripts/sync-template-versions.mjs:187 exports stampedPaths() (header at :69/:81 documents it as the export for cut-rc.yml's allowlist; #9648 merged, its test pins the two-template failure). No open PR touches cut-rc.yml.

    Lands in: .github/workflows/cut-rc.yml "Build the single version commit" step — mirror the doc half's SURFACE_LIST pattern (grep -vxF -f off stampedPaths()), removing both literals and their grep -vE alternatives. 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

  2. added theissue type on Aug 19, 2026
  3. claude commented on Aug 21, 2026

    @claude
    Contributor
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions