Skip to content

fix(ci): stage what the version pass actually writes in cut-rc.yml - #9553

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-9518-cut-rc-docs-image-tags
Aug 18, 2026
Merged

fix(ci): stage what the version pass actually writes in cut-rc.yml#9553
os-project-manager merged 1 commit into
mainfrom
claude/issue-9518-cut-rc-docs-image-tags

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#9518

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. Since
#9064 put node scripts/sync-docs-image-tags.mjs at the end of pnpm run version,
that assertion has tripped on every cut: the rewriter's output falls outside the
pathspec, so LEFT is non-empty and the step exits 1. The rule was right; its input
surface had gone stale.

Measured, not assumed

Full pnpm run version in a throwaway clone (no push remote) of origin/main @
b9f807734, 199 pending changesets, changeset pre enter rc then the whole
pipeline, on today's pinned @changesets/cli@2.31.1. Result: 160 dirty paths
77 package.json, 76 CHANGELOG.md, .changeset/pre.json, and 6 others.

Replaying the step's shell verbatim (extracted from the YAML, not hand-copied)
against origin/main's version reproduces the refusal — and finds four offending
paths, one more than the card recorded:

::error::tracked files were modified but fall outside the release file surface...
::error:: unstaged: content/docs/deployment/self-hosting.mdx
::error:: unstaged: content/docs/upgrading.mdx
::error:: unstaged: docker/README.md
::error:: unstaged: packages/create-objectstack/src/templates/blank/objectstack.manifest.json

The fourth is written by sync-template-versions.mjs (specVersion to ^18.0.0),
not by the docs rewriter, so #9518's body does not name it. It is fixed here anyway
because it is the same defect in the same assertion in the same block: widening the
allowlist for only three of the four would leave the cut refusing to push, which is
the acceptance criterion of the card. Per #9465 ruling 3 the allowlist follows the
measurement.

The doc surfaces are read, not restated

sync-docs-image-tags.mjs does not own its target list — it imports SURFACES from
scripts/check-docs-image-tag.mjs, the gate whose findings it exists to clear, on
that file's stated principle "one list, two consumers". This step now joins as the
third consumer of the same declaration, resolving it at run time:

node --input-type=module \
-e 'import { SURFACES } from "./scripts/check-docs-image-tag.mjs"; for (const s of SURFACES) console.log(s.file);'

That module is import-safe by construction: #9064 added its entry-point guard for
exactly this kind of consumer. Two new guards refuse the cut if the declaration
cannot be resolved, or resolves empty — an unknown surface must never degrade into
an empty allowlist, which would silently re-open this same hole.

The template manifest is instead named literally, beside its sibling
objectstack.config.ts. 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 rather than read its
list. Giving it those two things would let this pathspec read from it too; filed
separately as an observation.

This is deliberately not a wildcard

content/docs/** would have silenced the exact signal the assertion exists for.
Only paths a reviewed declaration names are accepted. Two properties verified by
replay:

  • a content/docs page SURFACES does not name is still rejected;
  • the re-check matches the declared paths with grep -vxF (whole-line, fixed
    string), so it is exact — an unescaped ERE alternation spelling README.md would
    have accepted READMEXmd.

Verification — five replay legs

The workflow cannot run outside a cut, so its step was extracted from the YAML and
run under bash -e (the runner's default shell for run:) against the real dirty
tree, restored byte-identically before each leg.

LegWorkflowResult
1origin/mainexit 1 — 4 unstaged paths (above)
2this PRexit 0 — STAGED 160, BAD empty, LEFT empty, commit built
3this PR + synthetic dirtexit 1 — names README.md and content/docs/ai/actions-as-tools.mdx
4this PR, SURFACES emptiedexit 1 — "resolved EMPTY ... Refusing to push"
5this PR, module unparseableexit 1 — "could not resolve SURFACES ... Refusing to push"

Leg 2, instrumented:

doc surfaces declared by SURFACES (3):
docker/README.md
content/docs/deployment/self-hosting.mdx
content/docs/upgrading.mdx
PROBE staged-count: 160
PROBE accepted-by-fixed-regex: 157
PROBE accepted-by-SURFACES-list: 3
PROBE BAD=[]
PROBE LEFT=[]

157 + 3 = 160: every staged path is accepted by exactly one filter, and nothing is
left behind. Legs 3-5 are the ones that matter most — a gate that can no longer fail
is worse than the bug it was hiding.

No release was performed: throwaway clone, no remotes, no publish, no tags, no
pre-mode entry or exit on origin, no workflow dispatch.

Scope

cut-rc.yml:285's "range is walkable" preflight is untouched — a disjoint block
belonging to another card. #9498 rewrites this same allowlist for the v3 pre-mode
file set and is currently held on a maintainer decision; per #9518's claim this lands
first and #9498 rebases onto it, which is the handover the filer asked for. That card
is not addressed here.

Gates

Re-derived for the actual diff with node scripts/pm/dispatch-gates.mjs (same five
families, no new ones) and run green at 0bfd6134b:
check:node-version, check:required-contexts, check:shard-attestation,
check:workflow-status-functions, plus check:nul-bytes.

Changeset

None — skip-changeset. The diff is one workflow file. It publishes no package,
changes no user-visible behaviour and ships nothing to consumers; the release-notes
input for a change like this is nil by construction.


Generated by Claude Code

`pnpm run version` has ended with `node scripts/sync-docs-image-tags.mjs`
since #9064, and `sync-template-versions.mjs` stamps `specVersion` into the
blank template's `objectstack.manifest.json`. Neither output was covered by
the staging pathspec in "Build the single version commit", so the step's
"nothing tracked may be left modified-but-unstaged" assertion tripped and
every cut refused to push.
Measured, not assumed: a full `pnpm run version` in a throwaway clone over
199 pending changesets (17.0.0 -> 18.0.0-rc.0) leaves 160 dirty paths, four
of which fall outside the old allowlist:
content/docs/deployment/self-hosting.mdx
content/docs/upgrading.mdx
docker/README.md
packages/create-objectstack/src/templates/blank/objectstack.manifest.json
The three doc surfaces are RESOLVED, not restated: `sync-docs-image-tags.mjs`
imports `SURFACES` from `scripts/check-docs-image-tag.mjs` rather than owning
a list, and this step now joins as a third consumer of that same declaration.
Deliberately not a wildcard over `content/docs/**` — a docs page `SURFACES`
does not name is still rejected, which is the signal the assertion exists for.
Two new guards refuse the cut when the declaration cannot be resolved or
resolves empty, so an unknown surface can never degrade into an empty
allowlist.
The template manifest is named literally beside its sibling
`objectstack.config.ts`: `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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDA9nN6nXQngoQUAAzRdMb
@os-project-manageros-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026 — with Claude
@os-project-manager
os-project-manager marked this pull request as ready for review August 18, 2026 09:33
@os-project-manager
os-project-manager added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 955ccf2Aug 18, 2026
22 of 23 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-9518-cut-rc-docs-image-tags branch August 18, 2026 09:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cut-rc.yml refuses every cut since #9064: sync-docs-image-tags.mjs writes three paths outside the enforced release file surface

2 participants

@os-project-manager@claude