fix(live-e2e): derive the showcase-app commit from the release tag at boot instead of pinning it - #8276
Conversation
… boot
`e2e/live/ci/backend.env` carried two pins that a header MUST said must agree:
`OBJECTSTACK_VERSION` (checked against `pnpm-lock.yaml` since objectui#7689)
and `OBJECTSTACK_REF`, a hand-moved sha whose stated rule — "always the commit
the `@objectstack/cli@${OBJECTSTACK_VERSION}` release tag points at" — nothing
could check. The file said so in as many words: "NOT machine-checked, and
deliberately so", on the reasoning that resolving the tag needs the objectstack
repository and the unit lane cannot reach it. True of the unit lane, and beside
the point: the live lane clones that very repository to get the app.
Retire the pin instead of adding a check nobody would read (the lane is
`informational`). `start-backend.sh` now resolves the commit at boot from the
release tag named by `OBJECTSTACK_VERSION` — `git ls-remote --tags`, taking the
peeled `^{}` sha of the annotated tag — and refuses to start, naming the tag,
when it does not resolve to a 40-character sha. `git ls-remote` reports a
missing tag as empty output and exit 0, so that shape check is the guard, not a
formality. The resolved sha stays in the reuse stamp so a re-pointed tag cannot
be served from a cached fixture in a reused scratch dir.
The pair the header describes then cannot disagree by construction: the
sparse-checkout of `examples/app-showcase` and the installed published packages
always come from the same release, and there is one value to move, not two.
Tests: the `keeps OBJECTSTACK_REF in the one shape start-backend.sh can fetch`
pin is retired deliberately — it asserted the shape of a value that no longer
exists — and replaced by three pins that now hold: backend.env declares no
`OBJECTSTACK_REF`; start-backend.sh still resolves the tag, still builds it from
`OBJECTSTACK_VERSION`, and still refuses on a bad shape; and backend.env still
explains the absence, so nobody restores the pin thinking it was dropped by
accident. Prose updated in the CI/CD page, `better-auth-pin.mjs` and the live
saved-view spec, all of which described a two-pin pair.
Refs objectui#7964, objectui#7689.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
ACCEPT (#7964, route 2) — reviewed on the tree at head Verified from the diff, not the report:
Not measured here: Next: CI convergence → flip ready → post-flip guard → auto-merge SQUASH → queue confirmation → landing probe ( Generated by Claude Code |
|
Hold on the arming of PR #8276 — Three readings after the flip to ready (07:54:37Z, head
Arming resumes after that push: CI convergence on the new head → post-flip guard (already ready, so a Generated by Claude Code |
Conflict in `e2e/live/ci/backend.env` only. objectui#7685 (639114c) moved the lockfile to `@objectstack/spec@17.3.0` and hand-moved BOTH backend pins with it: `OBJECTSTACK_VERSION=17.3.0` and `OBJECTSTACK_REF=8a1bad8b8ee7189a54229368400b5b427e3ad5e2`. This branch had already deleted the second key, so the two sides disagreed about whether it exists. Resolved to this branch's shape carrying main's value: `OBJECTSTACK_VERSION=17.3.0`, no `OBJECTSTACK_REF` key, this branch's rewritten header kept. `BETTER_AUTH_VERSION` and its paragraph are exactly as #7685 left them — whether 17.3.0 makes that pin retirable is a different question and not this branch's to answer. The conflict is itself the second measurement this branch exists to take. The `@objectstack/cli@17.3.0` tag peels to 8a1bad8b8ee7189a54229368400b5b427e3ad5e2 — byte-identical to the sha #7685 wrote by hand, on a version this branch had never seen. Two for two: the derivation reproduces the hand move, and the hand move is the step that is no longer needed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Merged
|
| sha | |
|---|---|
derived by start-backend.sh |
8a1bad8b8ee7189a54229368400b5b427e3ad5e2 |
| written by hand in #7685 | 8a1bad8b8ee7189a54229368400b5b427e3ad5e2 |
| EQUAL |
Two for two. The derivation reproduces the hand move on both versions — and the hand move is the step that is no longer needed. Note the tag object (1177d3ee…) is not the commit; taking the unpeeled line would have broken the fetch, which is why the script prefers the peeled one.
End-to-end, the derived sha fetches the way the script fetches it: git fetch -q --depth 1 origin 8a1bad8b… exit 0, checkout FETCH_HEAD exit 0, HEAD = 8a1bad8b…, examples/app-showcase/ present.
Gates on the merged head 3c234af3d
Exit codes captured by redirect, never through a pipe. Working tree clean; pnpm install --frozen-lockfile exit 0 against the 17.3.0 lockfile.
| Command | Exit | Verdict |
|---|---|---|
pnpm exec vitest run scripts/__tests__/ci-cd-pipeline-doc.test.ts |
0 | Tests 46 passed (46) — the version pin now holds 17.3.0 against a 17.3.0 lockfile |
bash -n e2e/live/ci/start-backend.sh |
0 | — |
pnpm lint:root |
0 | 32 problems (0 errors, 32 warnings), all pre-existing |
pnpm check:control-bytes |
0 | OK (scanned 6594 tracked text file(s)) |
node scripts/check-changeset-presence.mjs |
0 | 6 file(s) changed, 0 of them published source … no changeset is owed (vs merge-base fc32921aa) |
node scripts/check-governed-queue-guard.mjs --test (6 changed paths) |
0 | NOT GOVERNED — 6 path(s) checked against 5 governed surface(s) |
| resolution block, positive (17.3.0) | 0 | see above |
| resolution block, missing tag | 1 | refusal names the tag |
All five ablations re-run on the merged head, each with on-disk landing proof and a verified restore: M1 re-add the pin -> exit 1, 2 failed/44; M2 remove ls-remote -> 1 failed/45; M3 hard-code the tag -> 1 failed/45; M4 delete the shape guard -> 1 failed/45; M5 delete the header note -> 1 failed/45. Tree byte-identical to 3c234af3d afterwards, suite green at 46.
One thing worth a maintainer's eye
This merge is the defect the card describes, caught in the act. #7685 had to move a second value by hand and get it right, on a rule no check could enforce — it did get it right, and the branch it collided with is the one that removes the opportunity to get it wrong. That is the argument for the change, made by the tree rather than by the PR body.
Generated by Claude Code
|
Armed: PR #8276 on the merged head Generated by Claude Code |
|
LANDED: merged at 2026-09-07T09:36:54Z as Queue note, for the record. This entry sat green-but-unmerged from 08:41Z to 09:36Z (every required context success on the merge-group head, no removal event). At 09:32Z this seat disabled auto-merge and at 09:33:38Z converted the PR to draft as the only dequeue path without GraphQL; the queue then merged the ORIGINAL group at 09:36:53–54Z regardless, and the seat's 09:37:12Z ready-for-review flip landed on an already-closed PR. Both strokes were no-ops on the outcome and are recorded here so the timeline reads correctly. Whether the 55-minute stall was the queue waiting on something this seat cannot read, or a platform delay, is not knowable from REST; noted as a platform reading, not a defect of this PR. Card #7964 closes via Generated by Claude Code |
Fixes #7964
Implements the maintainer ruling on that card verbatim. Final head
cdf8ffd57, baseb6f821a8b.The ruling
The proof the ruling asks for
git ls-remote --tags https://github.com/objectstack-ai/objectstack.git 'refs/tags/@objectstack/cli@17.2.0' 'refs/tags/@objectstack/cli@17.2.0^{}'Annotated tag. The peeled
^{}line is the commit; the first line is the tag object and is not a commit — taking it would break the fetch. Running the resolution block extracted byte-for-byte from the committed script:Resolved sha
e7d2cc67fdef7fee9d2c6d65d7363fe1c78ce6a4is byte-identical to theOBJECTSTACK_REFthis PR deletes. The derivation reproduces the hand-moved value exactly — no drift to report, and the removal changes no behaviour on the current version.End-to-end, the derived sha is fetchable the way
start-backend.shfetches it:The change
e2e/live/ci/backend.env— theOBJECTSTACK_REF=line is gone, and with it the header MUST that nothing could check ("Always the commit the release tag points at ... NOT machine-checked, and deliberately so"). Its paragraph is replaced by one that documents the derivation, says why the old reasoning was wrong (the unit lane cannot reach objectstack; the live lane clones it anyway), and states one consequence rather than papering over it: the fixture cache key ishashFiles(backend.env)and the sha is no longer in the file, so a re-pointed release tag would still be served a stale Actions-cache fixture untilOBJECTSTACK_VERSIONmoves. A reused scratch dir is covered, because the resolved sha is in the stamp.e2e/live/ci/start-backend.sh— resolves@objectstack/cli@$OBJECTSTACK_VERSIONat boot, takes the peeled sha when present and the tag's own otherwise (lightweight tags do not peel), and refuses to start, naming the tag, when the result is not 40 hex characters. Three details that are load-bearing rather than stylistic:ls-remote. Asking forrefs/tags/TAGalone does not return the peeled line — measured, not assumed.git ls-remotereports a missing tag as empty output and exit 0. The shape check is therefore the real guard; an exit-code check alone would carry an empty ref intogit fetch --depth 1 origin "".[[ ... =~ ... ]], notprintf | grep -q. Under the script'sset -o pipefaila pipeline whose reader exits early can report failure for a value that matched.GIT_TERMINAL_PROMPT=0, so an unreachable or private remote fails in seconds with a named message instead of blocking the job on a credential prompt.No fallback to a hard-coded sha, no override key, no second path. The lane clones from the same anonymous URL three steps later, so no token is needed — verified, and the workflow injects none.
WANT_STAMPnow carries the resolved sha, so a re-pointed tag invalidates a reused fixture (A4).scripts/__tests__/ci-cd-pipeline-doc.test.ts— see below.Prose that described a two-pin pair —
content/docs/guide/ci-cd-pipeline.md(which calledBETTER_AUTH_VERSION"a third pin" and warned against repairing "the two pins above"),e2e/live/ci/better-auth-pin.mjs, and the header comment ofe2e/live/saved-view-filter.spec.ts. Leaving those would have reproduced this card's own defect one file over. The sentencePIN_RULE_SENTENCEpins is untouched.Pins retired and added, deliberately
Retired:
keeps OBJECTSTACK_REF in the one shape start-backend.sh can fetch. It asserted that a hand-moved sha was 40 hex characters — the only half of the pin a network-less lane could read. There is no hand-moved sha any more, so it has nothing to hold. The file says so at the retirement site, with a standing prohibition on restoring the key.Added, all three in the same
describe:declares no OBJECTSTACK_REF — the commit is derived, not pinnedderives the commit from the @objectstack/cli release tag in start-backend.shgit ls-remote --tagsis still there, the tag is still built fromOBJECTSTACK_VERSION, and the refuse-to-start shape check is still theredocuments the derivation in backend.env, where the pin used to beEither of the first two alone would be vacuous: an absent key is a design only while something derives the value, and a derivation is load-bearing only while no pin overrides it. The header block of the
describesays this in full.The version pin's failure message no longer instructs the reader to hand-move a second value.
Ablations — each new pin proven able to fail
Mutation applied to the committed tree, landing proven on disk (anchor
grep -cbefore/after andgit hash-objectcompared to theHEADblob), test run, then restored withgit checkout HEAD -- PATHandgit diff HEADproven empty. Undertrap ... EXIT INT TERMwith absolute paths.OBJECTSTACK_REF=e7d2cc67…tobackend.envgit ls-remote --tagswith a non-resolution@objectstack/cli@17.2.0instead of deriving from$OBJECTSTACK_VERSIONAll five turned red (the predicted direction). Tree byte-identical to
cdf8ffd57afterwards; the suite is green again at 46 passed.The refusal path was also exercised directly, on the resolution block extracted byte-for-byte from the committed script:
Gates — exit codes captured by redirect, never through a pipe
Run in the worktree at
cdf8ffd57, working tree clean.pnpm exec vitest run scripts/__tests__/ci-cd-pipeline-doc.test.ts(on the base, before edits)Tests 44 passed (44)pnpm exec vitest run scripts/__tests__/ci-cd-pipeline-doc.test.tsTests 46 passed (46)pnpm exec vitest run scripts/__tests__ --maxWorkers=2Test Files 115 passed (115)/Tests 3417 passed (3417)bash -n e2e/live/ci/start-backend.shpnpm lint:root32 problems (0 errors, 32 warnings)— all pre-existing, none in the changed filespnpm check:control-bytescheck-control-bytes: OK (scanned 6580 tracked text file(s))pnpm check:entry-guard70 scripts/ file(s) — no entry guard outside the baselinenode scripts/check-changeset-presence.mjsNo source or published contract of a released package changed in this range, so no changeset is owed.node scripts/check-governed-queue-guard.mjs --test(all 6 changed paths)NOT GOVERNED — 6 path(s) checked against 5 governed surface(s); none matched.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the 6 changed filesshellcheckis not installed in this container, so it is declared to CI rather than claimed here.bash -nis green and the script is unchanged in structure.No changeset, on the gate's own verdict: 6 files changed, 0 of them published source of a released package, 0 a manifest whose published contract moved. The
skip-changesetlabel is deliberately not applied — in this repo it reads nothing and exempts nothing.The live lane itself was not run locally and cannot be: it installs from npm, builds the console and boots a backend behind a 300s readiness gate. What was proven locally is every step the change touches — tag resolution, the refusal, and the fetch of the resolved sha.
Not in this PR
The ruling's last sentence, filed as its own card: #8275 — whether the lane should stay
informational, with the evidence of what it proves once this lands and the counter-evidence (#7990, the lane red onmainfrom an upstream float, not an objectui diff). Labelfinding, unassigned..github/workflows/live-e2e.ymlis untouched here on purpose; removing theOBJECTSTACK_REFline does change itshashFiles(backend.env)fixture cache key, which is expected and desired — the next run rebuilds the fixture once.Generated by Claude Code