Uh oh!
There was an error while loading. Please reload this page.
fix(create-objectstack): pin the scaffolded Dockerfile's runtime image to the CLI that builds the artifact - #9115
Conversation
…e to the CLI that builds the artifact (#9017) The blank template shipped `FROM ghcr.io/objectstack-ai/objectstack:latest` directly beneath a comment telling the reader to pin the tag to the `@objectstack/cli` version in their package.json — an instruction the scaffold itself did not follow, baked into every `npx create-objectstack` app. The tag is resolved AFTER install, from the installed CLI, not from the generated package.json: that file carries a caret range, and npm resolves `^17.0.0` to the newest 17.x, so pinning the range's floor would ship a runtime image older than the CLI that built the artifact. This is the rule scaffold-e2e.yml already applies for the same reason. Both halves move together — the comment above the FROM line is rewritten in the same pass, so the scaffold no longer instructs a step it just performed. With --skip-install there is no resolved version: the tag stays `latest` and the imperative comment stays, which is true on that path. scaffold-e2e.yml now reads the tag it builds its local runtime image under out of the generated Dockerfile instead of hardcoding `:latest`. Those were two hand-matched literals; a skew would have made Docker pull the last published image instead of the one built from this checkout, leaving the job's stated hermeticity false while it stayed green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
… pin helper (#9017) The behavioural tests call pinRuntimeImage directly, so removing the call from index.ts would have left them green while every scaffolded app kept `latest`. index.ts cannot be imported (program.parse() at module scope), so its text is asserted — the same compromise template-consistency.test.ts already makes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9017
The blank template shipped
FROM ghcr.io/objectstack-ai/objectstack:latestdirectly beneath a comment telling the reader to "pin the tag to the@objectstack/cliversion in your package.json" — an instruction the scaffold itself did not follow, baked into every app made withnpx create-objectstack.docker/README.md's tag table already scopeslatestto quick starts and documentsX.Y.Zas the production pin.Measured on scaffolded output, not the template's bytes, before the fix:
This lands option 1 (pin), the direction triage settled. Option 2 was not taken and no impracticality is being claimed.
The tag is resolved after install, not read off
package.jsonThe generated
package.jsoncarries a caret range, and the two are not interchangeable. npm resolves^17.0.0to the newest 17.x, so pinning the range's floor would ship a runtime image older than the CLI that built the artifact — the exact promise the comment makes, broken in a new way. The rolling:17tag does match the range's float window but is what the tag table tells production not to use.So the tag comes from the installed CLI (
node_modules/@objectstack/cli), which is the same rule the repo already applies for the same reason inscaffold-e2e.yml:219— "Pin the runtime's CLI to the SAME version the generated project actually resolved to — NOT a hardcodedlatest", because during an RC window a fixed tag skews protocol majors andos startrefuses to boot.Registry contents were checked rather than assumed: ghcr publishes exact tags (
15.1.0,16.0.0,16.1.0,17.0.0, and prereleases like17.0.0-rc.6), so a resolved version always names a tag that exists. Worth recording:15.0.0is absent from that list, so older exact tags are not guaranteed to persist indefinitely.Both halves move together
Pinning the line while leaving an imperative to pin by hand would relocate the contradiction rather than remove it, so the comment above the
FROMline is rewritten in the same pass. With--skip-installthere is no resolved version: the tag stayslatestand the comment keeps telling the reader to pin — which is true on that path, because there the user really must do it by hand.The template's comment was reflowed so the swappable paragraph is separated by a bare
#. That is load-bearing, not cosmetic: the rewrite walks up from theFROMline over contiguous prose comments, and without the separator it would swallow the section header too. Ablation B below is what proves it.The e2e coupling this change had to keep intact
scaffold-e2e.ymlbuilds the runtime image from this checkout, tags it, and then builds the scaffolded app against it, with:203-205naming hermeticity as the reason — "no dependency on a prior release having published the tag". That tag and the template'sFROMtag were two hand-matched literals authored in the same commit (b50c0ef27). Had they skewed, Docker would have quietly pulled the last published image instead of the one built here, and that stated hermeticity would have been false while the job stayed green.The workflow now reads the tag out of the generated Dockerfile instead of hardcoding
:latest, so the two are derived from one source and cannot skew. Nothing was traded away to accommodate the pin: no--pullflip, no removed leg, nocontinue-on-error. That job scaffolds with--skip-install, so the tag it reads today is stilllatestand its behaviour is unchanged; it follows the file if that ever changes.Residual gap, stated rather than hidden: because the e2e uses
--skip-install, the pinned shape gets no e2e coverage — it is covered by the package suite below.Verification — union run at
110c59f06, the final commitdispatch-gates.mjsreported "No check family names the given paths in its own source" for the dispatched surface, so the test is the gate here. Every assertion runs against scaffolded output produced by the real copy/sync/pin path; none greps the template.pnpm --filter create-objectstack test— 4 files, 58 passed (14 new)pnpm --filter create-objectstack typecheck— cleancheck:changeset-gate-self-tests,check:node-version,check:objectui-changeset,check:required-contexts,check:shard-attestation,check:workflow-status-functions,check:nul-bytes,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-shard-attestation, pluscheck:docs-image-tag— all passcheck:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage— all passcheck:type-check-debt --re-measure— not measured: it refuses on an unbuilt worktree by design, and building the 55-package closure was disproportionate here.create-objectstackappears in no ledger, and the structural half is green, so no entry is required — but that is reasoning, not a measurement, and CI runs it for real.Reverse verification (direction predicted before running: red in all three; no
dist/is involved — vitest transformssrcdirectly, so no rebuild applies):pinRuntimeImagereturns success without writingindex.tsThat third ablation is why the wiring is asserted at all: the behavioural tests call
pinRuntimeImagedirectly, so without it the scaffolder could simply stop calling the pin and every user would getlatestwith nothing going red.index.tscannot be imported (program.parse()at module scope), so its text is asserted — the same compromisetemplate-consistency.test.tsalready makes for the skills command.Changeset
A real changeset, not
skip-changeset: this changes whatnpx create-objectstackemits, which is published user-visible behaviour of thecreate-objectstackpackage. Patch — a bug fix, no ADR-0087 block since nothing breaking is declared.Generated by Claude Code