Skip to content

Commit 8ba478d

Browse files
claude[bot]claude
andauthored
fix(ci): derive cut-rc's template release-file allowlist from stampedPaths() (#10569)
The release-file allowlist in `cut-rc.yml`'s "Build the single version commit" step read its doc half from a declaration and RESTATED its template half: two literals hard-coding the template name `blank`, once in the `git add -A --` pathspec and again in the `grep -vE` re-check. The block's own comment recorded why -- `sync-template-versions.mjs` declared its targets but exported none of them and ran the sync at module scope, so importing it would have rewritten the templates instead of answering -- and named the precondition for fixing it. #9648 landed exactly that: `stampedPaths()` plus the entry-point guard. The two lists are one list, not two contracts of different width. Measured against the shipped bytes extracted from the committed YAML and run over a throwaway tree whose template files are written by the real `sync-template-versions.mjs`: on a one-template tree the old and new forms produce a byte-identical exit code, staged path set and error output; on a two-template tree the old form refuses the cut, naming the second template's objectstack.config.ts and objectstack.manifest.json as unstaged, and the new form accepts it. That is the failure #9648's fixture test already predicts, reproduced against the workflow half. `stampedPaths()` also reports each template's package.json, which the existing '*package.json' pathspec already permits, so the resolved list is a superset of the two literals and never a narrowing. It is not a wildcard over `templates/**` either: a tracked file in a template directory that the declaration does not name is still refused. Nothing here touches the changeset steps, `release.yml`, `pr-automation.yml` or root package.json -- the `.changeset` pathspec and the `^\.changeset/` alternation are byte-identical. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 9616ffb commit 8ba478d

1 file changed

Lines changed: 60 additions & 16 deletions

File tree

.github/workflows/cut-rc.yml

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -560,14 +560,10 @@ jobs:
560560
# Three more paths can appear at a major boundary and are allowed for that
561561
# reason, all written by `sync-protocol-version.mjs` /
562562
# `sync-template-versions.mjs`: packages/spec/src/kernel/protocol-version.ts
563-
# and the blank template's objectstack.config.ts (engines.protocol) and
564-
# objectstack.manifest.json (specVersion). The manifest is named literally,
565-
# like its sibling config.ts, rather than read the way the doc surfaces
566-
# below are: `sync-template-versions.mjs` does declare its targets (a
567-
# `TEXT_STAMPS` table plus a walk of `src/templates/`), but exports none of
568-
# them and has no entry-point guard, so importing it would RUN the sync
569-
# instead of reading its list. Give it those two things and this pathspec
570-
# should read from it too.
563+
# and, per bundled template, objectstack.config.ts (engines.protocol) and
564+
# objectstack.manifest.json (specVersion). protocol-version.ts is named
565+
# literally because that one file IS the whole of its surface. The template
566+
# paths are not, and the reason is the one the doc half states below.
571567
#
572568
# THE DOC SURFACES ARE READ, NOT RESTATED. `pnpm run version` ends with
573569
# `sync-docs-image-tags.mjs`, which rewrites the doc surfaces that pin a
@@ -587,6 +583,29 @@ jobs:
587583
# point of the assertion. Onboarding a surface stays a one-line edit to
588584
# `SURFACES` that a reviewer sees in the diff.
589585
#
586+
# THE TEMPLATE SURFACES ARE READ ON THE SAME TERMS. They used to be two
587+
# literals, both hard-coding the template name `blank`, and this block used
588+
# to record why: `sync-template-versions.mjs` declared its targets (a
589+
# `TEXT_STAMPS` table plus a walk of `src/templates/`) but exported none of
590+
# them and ran the sync at module scope, so importing it to ask "which files
591+
# does the version pass stamp?" would have rewritten the templates instead of
592+
# answering. #9648 gave it the export and the entry-point guard this block
593+
# asked for, so the restatement is retired here rather than re-seeded:
594+
# `stampedPaths()` derives its answer from the same walk and the same table
595+
# the stamper's own `main()` uses.
596+
#
597+
# This is a COLLAPSE OF TWO COPIES OF ONE LIST, not a widening. The literals
598+
# were never a deliberately narrower allowlist — they were the same set,
599+
# spelled by hand, and they equal it only while `blank` is the only bundled
600+
# template. `findTemplateDirs()` exists BECAUSE the template set is not
601+
# curated ("a template added tomorrow is covered on the day it lands"), so
602+
# the day a second template ships, the walk stamps it, the literal pair does
603+
# not cover it, and the unstaged-files assertion below refuses the cut — with
604+
# nothing red until someone attempts a release. `stampedPaths()` also reports
605+
# each template's own package.json, which `'*package.json'` already permits,
606+
# so the resolved list is a superset of the two literals and never a
607+
# narrowing.
608+
#
590609
# The allowlist is ENFORCED rather than described. Only allowlisted paths are
591610
# staged, and then every staged path is re-checked against the same rule and
592611
# the worktree is asserted clean. If `pnpm run version` ever grows a new
@@ -633,14 +652,37 @@ jobs:
633652
echo "doc surfaces declared by SURFACES (${#DOCS_SURFACES[@]}):"
634653
sed 's/^/ /' "$SURFACE_LIST"
635654
655+
# The template surfaces `sync-template-versions.mjs` stamps, read from the
656+
# same walk and the same table the stamper itself uses. Import-safe by
657+
# construction: that module carries the entry-point guard and the exports
658+
# added for exactly this consumer.
659+
TEMPLATE_LIST="${RUNNER_TEMP:-/tmp}/cut-rc-template-version-surfaces.txt"
660+
if ! node --input-type=module \
661+
-e 'import { stampedPaths } from "./scripts/sync-template-versions.mjs"; for (const p of stampedPaths()) console.log(p);' \
662+
> "$TEMPLATE_LIST"; then
663+
echo "::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."
664+
exit 1
665+
fi
666+
# Same rule as the doc half: unknown is a failure, never an empty
667+
# allowlist. `stampedPaths()` THROWS rather than returning [] on a moved
668+
# or empty template directory, so an empty file here means a resolution
669+
# that reported nothing while still exiting 0 — which would stage no
670+
# template path at all and then blame the files it left unstaged.
671+
if [ ! -s "$TEMPLATE_LIST" ]; then
672+
echo "::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."
673+
exit 1
674+
fi
675+
mapfile -t TEMPLATE_SURFACES < "$TEMPLATE_LIST"
676+
echo "template surfaces declared by stampedPaths() (${#TEMPLATE_SURFACES[@]}):"
677+
sed 's/^/ /' "$TEMPLATE_LIST"
678+
636679
git add -A -- \
637680
'*package.json' \
638681
'*CHANGELOG.md' \
639682
.changeset \
640683
.objectui-sha \
641684
packages/spec/src/kernel/protocol-version.ts \
642-
packages/create-objectstack/src/templates/blank/objectstack.config.ts \
643-
packages/create-objectstack/src/templates/blank/objectstack.manifest.json \
685+
"${TEMPLATE_SURFACES[@]}" \
644686
"${DOCS_SURFACES[@]}"
645687
646688
STAGED="$(git diff --cached --name-only)"
@@ -650,13 +692,15 @@ jobs:
650692
fi
651693
652694
# Re-check every staged path against the allowlist. The pathspec above is
653-
# convenience; THIS is the guarantee. Two filters, same allowlist the
654-
# pathspec used: the fixed release paths by pattern, then the declared doc
655-
# surfaces by WHOLE-LINE EXACT match (`-xF`) against the very list that was
656-
# staged — so the second filter cannot accept a path `SURFACES` does not
657-
# name, and needs no regex-escaping of the paths to stay exact.
695+
# convenience; THIS is the guarantee. Three filters, same allowlist the
696+
# pathspec used: the fixed release paths by pattern, then the declared
697+
# template and doc surfaces by WHOLE-LINE EXACT match (`-xF`) against the
698+
# very lists that were staged — so neither derived filter can accept a path
699+
# its declaration does not name, and neither needs regex-escaping of the
700+
# paths to stay exact.
658701
BAD="$(printf '%s\n' "$STAGED" \
659-
| grep -vE '(^|/)package\.json$|(^|/)CHANGELOG\.md$|^\.changeset/|^\.objectui-sha$|^packages/spec/src/kernel/protocol-version\.ts$|^packages/create-objectstack/src/templates/blank/objectstack\.config\.ts$|^packages/create-objectstack/src/templates/blank/objectstack\.manifest\.json$' \
702+
| grep -vE '(^|/)package\.json$|(^|/)CHANGELOG\.md$|^\.changeset/|^\.objectui-sha$|^packages/spec/src/kernel/protocol-version\.ts$' \
703+
| grep -vxF -f "$TEMPLATE_LIST" \
660704
| grep -vxF -f "$SURFACE_LIST" || true)"
661705
if [ -n "$BAD" ]; then
662706
echo "::error::the version commit would carry paths outside the release file surface. Refusing to push. Offending paths follow; if the version pass legitimately grew a new output, widen the allowlist in this workflow deliberately."

0 commit comments

Comments
 (0)