Repository navigation
fix(everything-dev): release-scoped extends inheritance + deploy-time composition gate - #656
Merged
Merged
Conversation
… composition gate A root publish moved the inherited `app.auth` slot (children never author it; `bos init` deletes it) to the newest release while a platform-pinned child's own ui kept building against its lockfile — the next child deploy/restart composed auth@rc.N+1 with ui@rc.N and the #546 boot guard degraded health. The platform pin governed image, sync, and compat reporting, but nothing mapped it onto inherited slot pins: extends inheritance was pointer-live, not release-scoped. Two layers (ADR 0034 amendment): - The release manifest records its own slot-pin map (`slots`, additive, composed from the release payload on the release leg — sorted keys so identical payloads compose identical bytes). - The deploy train stamps every inherited slot (absent from the authored config) with the authored platform pin's release-recorded production URL + pin — child-wins over the parent's live pointer at every consumer boot; a child's runtime becomes a deterministic function of (code, pinned release). Config-only publishes carry the stamps forward via mergeAuthoredOverPublished; rollback payloads skip stamping. - The deploy train gains the composition gate (`PublishTrainError` kind "composition"): extends-merged shared-singleton cross-check pre-publish — the host boot guard's mirror — plus the platform compat check scoped to the child's own resolved release. A skewed composition now dies as a red CI run with the `bos upgrade` remediation named. - boot-check gains `resolveBootShape` (the walk shared with `verifyBootPayload`) and `boot-check`'s e2e coverage extends to the incident replay.
- runPublishTrain skips the composition gate for exactPayload rollbacks —
the recovery path must never be blockable by the parent's moved-on pins
(boot-check + pin verification already gate rollback bytes); a missing
boot shape fails loud instead of silently gating a stale fallback.
- boot-check splits verifyBootPayload into resolveBootShape (walk) +
verifyResolvedBootShape (pure verify); the train shares one walk with the
gate, the untyped preResolved param and its unreachable payload fallback
are gone, and the pre-rewrite non-object payload message is restored.
- new services/config-slots.ts — the one app/plugins slot walker (canonical
dotted paths; ui:false and bare-ref visits included) plus
ensureObjectContainers; collectPinnedSlots, collectReleaseSlotPins,
authoredSlotPaths, and slotPins/pointerFingerprint all derive from it,
ending the four silently-divergent traversal copies (rollback's hardcoded
app keys vs iterate-all). Rollback slot names canonicalize to the shared
dotted paths ("auth" → "plugins.auth").
- the composition gate reports unresolvable merged-shape UI pins outside
the pin-verification-verified set as violations instead of skipping
silently — inherited pins were not covered by that leg.
- resolveReleaseManifestForDeploy returns the manifest directly (the
single-field wrapper is gone); shared tests/helpers/version-manifests.ts
replaces the duplicated manifest-fixture fetchers; unmarked comments in
the touched hunks are deleted or KEEP-marked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident
A child app pinned to framework rc.24 pushes a commit. CI runs, the deploy train pulls the extends chain — and the auth slot (children never author
app.auth;bos initdeletes it) resolves from the parent's live published config, which the root moved to rc.25 with its latest deploy. The child's ownuiis still built against its lockfile (rc.24). At the next host boot:SSR composition fails, health stays degraded / the prod host exits. Any host restart after the root's publish hits this — the child's git push is only the common trigger. This should never happen.
Why the plan 057 semver fix doesn't cover it:
checkPlatformCompatonly ran inbos upgradereports /bos mf check— never in the deploy train — and a child resolving via its own rc.24 slot pin passes compat; the inherited rc.25 auth pin was invisible to every gate. The host boot guard was the only detector — i.e. after the broken composition was already live.The fix (ADR 0034 amendment — release-scoped extends inheritance)
Layer 2 — the root cause: extends inheritance is pointer-live, not release-scoped
slots— additive, optional; keyed by dotted config path,base+ immutable version-manifestpin), composed from the release payload on the release leg (sorted keys — byte-stable; the burned-version guard already enforces immutability).slotsmap that the authored config does not declare) with the authored platform pin's release-recorded production URL + pin. Child-wins deep merge then shadows the parent's live pointer at every consumer boot — a pinned child's runtime becomes a deterministic function of (child code, pinned release). Authored slots are untouched;ui: falseopt-outs own their slot; manifests withoutslots(pre-amendment releases) stamp nothing. Config-only publishes carry the stamps forward as pipeline state (mergeAuthoredOverPublishedkeeps published-only keys); rollback payloads skip stamping (canonical).verifyRollbackSnapshotleg.Layer 1 — the deploy-time composition gate (
PublishTrainErrorkindcomposition)boot-check.ts:resolveBootShapeshares one walk withverifyBootPayload).platformpins, the ADR 0034 compat check now runs in the deploy train, scoped to the child's own resolved release (slot-first) — never the parent's latest. Catches the reverse skew too (lockfile bumped without the platform pin, e.g. a Renovate auto-merge).bos upgraderemediation named — never a degraded production boot.Explicitly rejected: relaxing the
everything-dev/ui/authstrict singleton to sonner-style tolerance — it exists because two divergent session-read copies disagreed into redirect loops (d57b8f4); tolerance reintroduces that bug class.Rollout notes
slotsreleases are protected by the composition gate (deploy blocked, remediation named) until onebos upgrade+ deploy; after that they're permanently immune.platformfield — hand-rolled;bos initalways writes one) resolve latest and stamp the latest release's slots; the gate covers their drift.Tests
tests/unit/composition.test.ts(16 tests): stamping (inherited/authored/ui:false/stale-override/no-slots),authoredSlotPaths, skew violation + skip-unresolvable + agree + non-UI, compat leg (aligned / lockfile drift / unpinned), release-slot composition + byte stability.platform-upgrade-e2e.test.ts(2 new legs on the in-memory registry + a local HTTP server for pinned bytes): the incident replay — root publishes rc.1 (slots recorded) then rc.2 (live config moves to react 19.3.0); the pinned rc.1 child's deploy stamps auth slots from rc.1 and the gate is green; without stamps the gate blocks with the named skew and remediation.pnpm --filter everything-dev test— 1689 passed. Typecheck + biome + effect lint clean.Addresses the recurring "auth skew on redeploy" class; #546 supplies the error taxonomy.