Skip to content

ci(scaffold-e2e): record why the unpublished-window fallback stays in the workflow, and narrow what its skip covers - #9374

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9149-scaffold-e2e-unpublished-window
Aug 17, 2026
Merged

ci(scaffold-e2e): record why the unpublished-window fallback stays in the workflow, and narrow what its skip covers#9374
os-zhuang merged 1 commit into
mainfrom
claude/issue-9149-scaffold-e2e-unpublished-window

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9149

Workflow-only change to .github/workflows/scaffold-e2e.yml. The shipped scaffolder is not touched — see "Scope fork" below, which the reviewer should read first.

What landed

1. The designed degradation gains a recorded reason (the graded route).

The pinned-shape leg warns and skips during the unpublished-version window because the range-rewrite fallback lives in this workflow's install step, not in the scaffolder. That was tribal knowledge spread across three issues. The step's comment now states it in place: why the fallback sits here, that relocating it into the published scaffolder was considered and declined at triage on 2026-08-17 as a user-visible public-surface expansion, and that reopening it needs a decision card answering this card's three questions. The specVersion coupling introduced by #9264 is named there too, so a future taker inherits it instead of rediscovering it.

2. The skip no longer swallows two real defects.

The registry answer was classified two ways. "npm says no version matches" and "npm could not be reached" both surface as an empty string, so a registry outage presented itself as the known window and carried the leg green. And once the skip fired, it asserted nothing at all, so a bogus tag emitted during the window was invisible.

Now three ways:

registry answerverdict
unreachable::error:: — an unclassifiable answer is never evidence of the window
reachable, range unsatisfiablethe known window: warn, skip pin-equality, still assert the degraded shape — with no resolvable CLI the scaffolder pins nothing, so the template's own latest tag must survive untouched
reachable, range satisfiable::error::unchanged; still the branch that catches a genuinely broken scaffolder install

The degradation itself is intact: the unpublished-version window still does not redden this leg.

Scope fork — please rule before closing

The dispatch that produced this PR framed the defect as where the fallback lives and opened the file surface to packages/create-objectstack/src/**, suggesting the fallback be relocated into the scaffolder.

The maintainer's first-touch grading on the issue (2026-08-17) says the opposite, and is quoted verbatim:

Route (b) — relocating the range-rewrite fallback into the published scaffolder — is deliberately not chosen at triage: it converts a CI-only fallback into user-visible published behaviour (rewriting a real user's ranges to latest), i.e. a public-surface expansion, which sits on the manual floor. Anyone wanting (b) should raise a decision card answering this card's three questions.

File surface: the workflow file carrying the scaffold-e2e leg only.

I followed the maintainer's ruling and stayed in the workflow file. Relocating would have meant writing speculative code on an explicitly-undecided public-surface question. The narrowing above is the part of the dispatch's goal — reduce the gap between what CI exercises and what a user runs — that is reachable without expanding the published surface, and it is the alternative the dispatch itself named as acceptable.

A measured correction to the dispatch's premise

The dispatch stated that a relocated fallback "must cover specVersion alongside engines.protocol". Measured: specVersion is advisory-only and, on this path, unread.

  • checkSpecVersionGap (packages/cli/src/utils/spec-version.ts) documents itself as "advisory-only — it never fails a build/validate", and only fires when the declared major is older than the installed one.
  • It reads config.manifest.specVersion, and the blank template's objectstack.config.ts manifest declares no specVersion at all — only objectstack.manifest.json does, which is template-registry metadata.

So the workflow fallback's asymmetry (it re-stamps engines.protocol but not specVersion) is not a live defect: engines.protocol is re-stamped because the ADR-0087 D1 handshake hard-refuses the boot, and specVersion has no equivalent enforcement. The coupling is real for the release-time pass; it is not load-bearing for this fallback. A relocation card should still handle it, but as tidiness, not as a boot-blocker.

Verification

Gate union re-run on the final commit c9da9ca0e; dispatch-gates.mjs re-derived from the actual changed path.

checkresult
check:node-versionexit 0
check:required-contextsexit 0
check:shard-attestationexit 0
check:workflow-status-functionsexit 0
node scripts/check-shard-attestation.mjsexit 0
check:nul-bytesexit 0
YAML parse + bash -n on the extracted stepOK

Behavioural verification of the new classifier. The classification block was extracted verbatim from the workflow (parsed out of the YAML, not retyped) and run against a stubbed npm in four scenarios:

scenarionewold (origin/main)
registry unreachableexit 1 ::error::exit 0, green
window, tag latestexit 0, warn + degraded assertexit 0
window, tag 16.2.0 (invented)exit 1 ::error::exit 0, green
range satisfiableexit 1 ::error::exit 1

The two bolded rows are the previously-swallowed defects; the reverse verification ran the old block from origin/main against the same fixtures to prove the narrowing is real rather than cosmetic.

Not verified locally, and it cannot be:Scaffold E2E is CI-only (its header declares dispatch-gates: no-check-families) and needs a real registry, Docker and a network. The stubbed run above exercises the classification logic, not the end-to-end leg. A gate that refuses to run is not a gate that passed.

Prettier: not configured and not a gate in this repo; the file fails prettier defaults identically before and after this change, so no formatting regression is introduced and the file's existing quote style is left alone.

No changeset, by the workflow's own prescription

This PR changes CI only and releases nothing, which lint.yml calls "the textbook skip-changeset case — such a PR releases nothing". The skip-changeset label is applied instead of a changeset, which would otherwise fabricate a user-facing release note for a change no user can observe.


Generated by Claude Code

… the workflow, and stop the skip swallowing two real defects
Co-Authored-By: Claude <noreply@anthropic.com>
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 17, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 17, 2026 19:26
@os-zhuang
os-zhuang added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit d51106dAug 17, 2026
28 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9149-scaffold-e2e-unpublished-window branch August 17, 2026 19:38
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

2 participants

@os-zhuang@claude-bot