From 0ad0a9744d488771a45fce995ee6a3f18e8400a1 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Fri, 9 Oct 2026 14:49:24 -0500 Subject: [PATCH 1/3] fix(ci): stop changeset release PRs from failing structurally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #307 (chore: version packages) failed three checks that cannot pass on a release PR by construction: - Runtime image smoke boots identity-driven against the published config, so it compares the PR's framework train against bundles the last deploy published — a deploy-parity check the deploy train (with its bos mf check gate) owns post-merge. Skip it on changeset-release/main; the image's self-contained boot path stays covered by the regression browser start:* modes. - init.full resolved better-near-auth from npm, where the bumped rc version does not exist until publish. Pack it as a third framework tarball so child installs stay hermetic. - intent stale flags the version-marker drift every release PR creates by construction; skip the PR-time gate there — markers are reconciled by the post-publish review flow. Also: assert in container-build that every built remote stamps the same every-plugin train as the framework package (fails the build before staging/boot instead of 503-ing at smoke), and decouple two unit tests from the live framework version. --- .github/workflows/check-skills.yml | 6 +++- .github/workflows/ci.yml | 8 ++++- .../unit/plugins-shared-registration.test.ts | 7 ++-- .../tests/unit/shared-deps-spec.test.ts | 4 +-- .../tests/integration/framework-packages.ts | 26 ++++++++++---- scripts/regression/container-build.ts | 34 +++++++++++++++++++ 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml index 23900a039..e3952edf9 100644 --- a/.github/workflows/check-skills.yml +++ b/.github/workflows/check-skills.yml @@ -31,7 +31,11 @@ on: jobs: validate: name: Validate intent skills - if: github.event_name == 'pull_request' + # Changeset release PRs bump every package version at once, so `stale` + # structurally reports version drift there — the library_version markers + # mean "reviewed against this published version" and are reconciled by the + # review job below (or its review PR) after the release actually publishes. + if: github.event_name == 'pull_request' && github.head_ref != 'changeset-release/main' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79dc37cf7..b98a17dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -606,7 +606,13 @@ jobs: name: Runtime image smoke runs-on: ubuntu-latest needs: [detect-changes, lint-and-typecheck] - if: needs.detect-changes.outputs.image-smoke == 'true' + # The smoke boots identity-driven against the published config, so it is a + # deploy-parity check: an image built on a release PR (framework train + # bumped) cannot match the bundles the last deploy published. The deploy + # train re-aligns config + image post-merge and gates itself on + # `bos mf check`; the image's self-contained boot path is covered by the + # regression browser start:* modes. + if: needs.detect-changes.outputs.image-smoke == 'true' && github.head_ref != 'changeset-release/main' timeout-minutes: 25 env: CI: true diff --git a/host/tests/unit/plugins-shared-registration.test.ts b/host/tests/unit/plugins-shared-registration.test.ts index 30517c89c..ae7986743 100644 --- a/host/tests/unit/plugins-shared-registration.test.ts +++ b/host/tests/unit/plugins-shared-registration.test.ts @@ -6,12 +6,15 @@ describe("host shared registration entries", () => { it("derives normalized registration entries from config-declared shared deps", () => { const entries = buildSharedRegistrationEntries( mergeSharedMaps( - { "every-plugin": { version: "2.10.1", singleton: true } }, + // Neutral literal — the helper normalizes whatever version it is + // given; pinning the live framework train here would break on every + // release PR. + { "every-plugin": { version: "9.9.9", singleton: true } }, { react: { version: "19.2.4", singleton: true, eager: true, shareScope: "default" } }, ), ); expect(entries["every-plugin"]).toEqual({ - version: "2.10.1", + version: "9.9.9", shareScope: "default", shareConfig: { singleton: true, diff --git a/packages/every-plugin/tests/unit/shared-deps-spec.test.ts b/packages/every-plugin/tests/unit/shared-deps-spec.test.ts index 28a75f36f..41ca647c6 100644 --- a/packages/every-plugin/tests/unit/shared-deps-spec.test.ts +++ b/packages/every-plugin/tests/unit/shared-deps-spec.test.ts @@ -232,8 +232,8 @@ describe("ticket 12 pins (no runtime debug)", () => { describe("host registration policy helpers", () => { it("normalizes config-declared entries with the host defaults", () => { - expect(toHostSharedEntry("every-plugin", { version: "2.10.1", singleton: true })).toEqual({ - version: "2.10.1", + expect(toHostSharedEntry("every-plugin", { version: "9.9.9", singleton: true })).toEqual({ + version: "9.9.9", shareScope: "default", shareConfig: { singleton: true, diff --git a/packages/everything-dev/tests/integration/framework-packages.ts b/packages/everything-dev/tests/integration/framework-packages.ts index beb174e4f..8ca21c5d9 100644 --- a/packages/everything-dev/tests/integration/framework-packages.ts +++ b/packages/everything-dev/tests/integration/framework-packages.ts @@ -7,6 +7,7 @@ import { globSync } from "glob"; import { readWorkspaceCatalog } from "../../src/workspace-catalog"; type FrameworkTarballs = { + "better-near-auth": string; "every-plugin": string; "everything-dev": string; }; @@ -21,11 +22,19 @@ export function getFrameworkTarballs(repoRoot: string): Promise { const tarballDir = mkdtempSync(join(tmpdir(), "everything-dev-framework-packages-")); + await execa("pnpm", ["--dir", "packages/better-near-auth", "run", "build"], { cwd: repoRoot }); await execa("pnpm", ["--dir", "packages/every-plugin", "run", "build"], { cwd: repoRoot }); await execa("pnpm", ["--dir", "packages/everything-dev", "run", "build"], { cwd: repoRoot }); const rootCatalog = readWorkspaceCatalog(repoRoot); + const betterNearAuthTarball = await stageAndPackFrameworkPackage({ + repoRoot, + packageName: "better-near-auth", + tarballDir, + rootCatalog, + }); + const everyPluginTarball = await stageAndPackFrameworkPackage({ repoRoot, packageName: "every-plugin", @@ -33,11 +42,12 @@ async function buildFrameworkTarballs(repoRoot: string): Promise { console.log(`[container-build] plugin ${key} (api remote)…`); run("pnpm", ["run", "build"], workspace); } + + assertFederationTrain("api/dist"); + if (authWorkspace) assertFederationTrain(path.join(authWorkspace, "dist")); + for (const [, workspace] of localPlugins) assertFederationTrain(path.join(workspace, "dist")); }; +// Every built remote must carry the same `every-plugin` share version as the +// framework package on this checkout — a stale train here surfaces at boot as +// a strict-singleton mismatch ("Version X from host … needs Y") and every +// /api/* route 503s. Fail the build train before anything stages or boots. +const frameworkTrain = JSON.parse( + readFileSync(path.join(root, "packages/every-plugin/package.json"), "utf8"), +).version as string; + +function assertFederationTrain(distDir: string): void { + const manifestPath = path.join(root, distDir, "mf-manifest.json"); + if (!existsSync(manifestPath)) { + throw new Error( + `[container-build] ${distDir} has no mf-manifest.json — the plugin build did not emit federation metadata`, + ); + } + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { + shared?: Array<{ name?: string; version?: string; requiredVersion?: string | null }>; + }; + const entry = manifest.shared?.find((dep) => dep.name === "every-plugin"); + const stamped = entry?.version ?? entry?.requiredVersion ?? null; + if (stamped !== frameworkTrain) { + throw new Error( + `[container-build] ${distDir} was built against every-plugin@${stamped ?? "unknown"} ` + + `but the framework train is ${frameworkTrain} — the plugin build resolved a stale ` + + `every-plugin. Rebuild so all remotes stamp the same framework version.`, + ); + } + console.log(`[container-build] ${distDir} stamps every-plugin@${stamped} ✓`); +} + // Fixed in-container port map — every service is container-local, so only the // host port needs mapping at `docker run`. const basePort = 4100; From ac7165121eb846b3e56fe134782e44069cdeb419 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Fri, 9 Oct 2026 14:57:03 -0500 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20apply=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20loop=20tarball=20copies,=20uniform=20train-assert?= =?UTF-8?q?=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - framework-packages.ts: the stage-pack-then-copy pattern for the two dependency framework packages is now one loop, so pack ordering (everything-dev consumes the transitive copies) is structural instead of incidental - container-build.ts: assertFederationTrain callers all pass root-relative dist paths the same way, and a train mismatch now lists the dists already checked --- .../tests/integration/framework-packages.ts | 50 ++++++++----------- scripts/regression/container-build.ts | 10 ++-- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/everything-dev/tests/integration/framework-packages.ts b/packages/everything-dev/tests/integration/framework-packages.ts index 8ca21c5d9..d2aa7fc20 100644 --- a/packages/everything-dev/tests/integration/framework-packages.ts +++ b/packages/everything-dev/tests/integration/framework-packages.ts @@ -22,48 +22,42 @@ export function getFrameworkTarballs(repoRoot: string): Promise { const tarballDir = mkdtempSync(join(tmpdir(), "everything-dev-framework-packages-")); - await execa("pnpm", ["--dir", "packages/better-near-auth", "run", "build"], { cwd: repoRoot }); - await execa("pnpm", ["--dir", "packages/every-plugin", "run", "build"], { cwd: repoRoot }); - await execa("pnpm", ["--dir", "packages/everything-dev", "run", "build"], { cwd: repoRoot }); + for (const packageName of ["better-near-auth", "every-plugin", "everything-dev"] as const) { + await execa("pnpm", ["--dir", `packages/${packageName}`, "run", "build"], { cwd: repoRoot }); + } const rootCatalog = readWorkspaceCatalog(repoRoot); - const betterNearAuthTarball = await stageAndPackFrameworkPackage({ - repoRoot, - packageName: "better-near-auth", - tarballDir, - rootCatalog, - }); - - const everyPluginTarball = await stageAndPackFrameworkPackage({ - repoRoot, - packageName: "every-plugin", - tarballDir, - rootCatalog, - }); - // The everything-dev tarball depends on the other framework packages and the // scaffolded child depends on them directly — both references must use - // distinct tarball paths, so stage copies for the transitive side. - const betterNearAuthTarballCopy = join(tarballDir, "better-near-auth-transitive-copy.tgz"); - cpSync(betterNearAuthTarball, betterNearAuthTarballCopy); - const everyPluginTarballCopy = join(tarballDir, "every-plugin-transitive-copy.tgz"); - cpSync(everyPluginTarball, everyPluginTarballCopy); + // distinct tarball paths, so stage copies for the transitive side. These + // packages must pack before everything-dev, which consumes the copies. + const transitiveTarballs: Record = {}; + const directTarballs: Record = {}; + for (const packageName of ["better-near-auth", "every-plugin"] as const) { + const tarball = await stageAndPackFrameworkPackage({ + repoRoot, + packageName, + tarballDir, + rootCatalog, + }); + const copy = join(tarballDir, `${packageName}-transitive-copy.tgz`); + cpSync(tarball, copy); + directTarballs[packageName] = tarball; + transitiveTarballs[packageName] = copy; + } const everythingDevTarball = await stageAndPackFrameworkPackage({ repoRoot, packageName: "everything-dev", tarballDir, rootCatalog, - otherFrameworkTarballs: { - "better-near-auth": betterNearAuthTarballCopy, - "every-plugin": everyPluginTarballCopy, - }, + otherFrameworkTarballs: transitiveTarballs, }); return { - "better-near-auth": betterNearAuthTarball, - "every-plugin": everyPluginTarball, + "better-near-auth": directTarballs["better-near-auth"], + "every-plugin": directTarballs["every-plugin"], "everything-dev": everythingDevTarball, }; } diff --git a/scripts/regression/container-build.ts b/scripts/regression/container-build.ts index 6f7169b48..3db1fa1e7 100644 --- a/scripts/regression/container-build.ts +++ b/scripts/regression/container-build.ts @@ -266,8 +266,8 @@ const build = () => { } assertFederationTrain("api/dist"); - if (authWorkspace) assertFederationTrain(path.join(authWorkspace, "dist")); - for (const [, workspace] of localPlugins) assertFederationTrain(path.join(workspace, "dist")); + if (authWorkspace) assertFederationTrain(`${authWorkspace}/dist`); + for (const [, workspace] of localPlugins) assertFederationTrain(`${workspace}/dist`); }; // Every built remote must carry the same `every-plugin` share version as the @@ -278,6 +278,8 @@ const frameworkTrain = JSON.parse( readFileSync(path.join(root, "packages/every-plugin/package.json"), "utf8"), ).version as string; +const trainCheckedDists: string[] = []; + function assertFederationTrain(distDir: string): void { const manifestPath = path.join(root, distDir, "mf-manifest.json"); if (!existsSync(manifestPath)) { @@ -294,9 +296,11 @@ function assertFederationTrain(distDir: string): void { throw new Error( `[container-build] ${distDir} was built against every-plugin@${stamped ?? "unknown"} ` + `but the framework train is ${frameworkTrain} — the plugin build resolved a stale ` + - `every-plugin. Rebuild so all remotes stamp the same framework version.`, + `every-plugin (checked so far, all matching: ${trainCheckedDists.join(", ") || "none"}). ` + + `Rebuild so all remotes stamp the same framework version.`, ); } + trainCheckedDists.push(distDir); console.log(`[container-build] ${distDir} stamps every-plugin@${stamped} ✓`); } From f22173de2a8bf7b989eb3e681a61bf722cacef42 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Fri, 9 Oct 2026 15:17:43 -0500 Subject: [PATCH 3/3] =?UTF-8?q?fix(regression):=20unbreak=20container-buil?= =?UTF-8?q?d=20=E2=80=94=20hoist=20authWorkspace,=20extract=20typed=20help?= =?UTF-8?q?ers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review-driven assertion referenced authWorkspace outside the if-block where it was declared; the Docker dist-builder stage crashed with a ReferenceError after all builds succeeded, failing every image-building job (runtime image smoke, regression browser/http start modes). scripts/ is covered by no workspace typecheck, so only the Docker runtime caught it. - hoist the auth app-slot derivation to module level and share it between build() and stage() (stage() duplicated the same expression) - extract resolveAuthWorkspace + checkFederationTrain into typed package code (packages/everything-dev/src/federation-train.ts) with unit tests — the orchestrator keeps only a thin throwing wrapper that accumulates the checked-dists list - drop the script's local readBuildReport duplicate in favor of the package helper (schema-validated) Verified by running scripts/regression/container-build.ts locally: all 7 dists stamp every-plugin@2.10.1 and staging completes. --- .../everything-dev/src/federation-train.ts | 57 +++++++++ .../tests/unit/federation-train.test.ts | 109 ++++++++++++++++++ scripts/regression/container-build.ts | 64 ++++------ 3 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 packages/everything-dev/src/federation-train.ts create mode 100644 packages/everything-dev/tests/unit/federation-train.test.ts diff --git a/packages/everything-dev/src/federation-train.ts b/packages/everything-dev/src/federation-train.ts new file mode 100644 index 000000000..cdb58a84a --- /dev/null +++ b/packages/everything-dev/src/federation-train.ts @@ -0,0 +1,57 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * The auth app-slot's local workspace, derived from the resolved config: + * `app.auth.development: "local:"` → ``, null otherwise. Shared by + * the container-build orchestrator's build and staging phases — deriving it + * per-site duplicated the expression and scoped it away from callers. + */ +export function resolveAuthWorkspace( + bosConfig: { app?: { auth?: { development?: unknown } } } | undefined | null, +): string | null { + const development = bosConfig?.app?.auth?.development; + return typeof development === "string" && development.startsWith("local:") + ? development.slice("local:".length) + : null; +} + +export interface MfManifest { + shared?: Array<{ name?: string; version?: string; requiredVersion?: string | null }>; +} + +/** The `every-plugin` share version a built remote stamps in its mf-manifest. */ +export function everyPluginStamp(manifest: MfManifest): string | null { + const entry = manifest.shared?.find((dep) => dep.name === "every-plugin"); + return entry?.version ?? entry?.requiredVersion ?? null; +} + +export type FederationTrainCheck = + | { ok: true; stamp: string } + | { + ok: false; + reason: "missing-manifest" | "no-stamp" | "mismatch"; + stamp: string | null; + }; + +/** + * A built remote must stamp the same `every-plugin` share version as the + * framework package on this checkout — a stale train surfaces at boot as a + * strict-singleton mismatch ("Version X from host … needs Y") and every + * /api/* route 503s. `distDir` is the workspace's built dist root. + */ +export function checkFederationTrain(distDir: string, expectedTrain: string): FederationTrainCheck { + const manifestPath = join(distDir, "mf-manifest.json"); + if (!existsSync(manifestPath)) { + return { ok: false, reason: "missing-manifest", stamp: null }; + } + let manifest: MfManifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as MfManifest; + } catch { + return { ok: false, reason: "missing-manifest", stamp: null }; + } + const stamp = everyPluginStamp(manifest); + if (!stamp) return { ok: false, reason: "no-stamp", stamp: null }; + return stamp === expectedTrain ? { ok: true, stamp } : { ok: false, reason: "mismatch", stamp }; +} diff --git a/packages/everything-dev/tests/unit/federation-train.test.ts b/packages/everything-dev/tests/unit/federation-train.test.ts new file mode 100644 index 000000000..237c40524 --- /dev/null +++ b/packages/everything-dev/tests/unit/federation-train.test.ts @@ -0,0 +1,109 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + checkFederationTrain, + everyPluginStamp, + resolveAuthWorkspace, +} from "../../src/federation-train"; + +describe("resolveAuthWorkspace", () => { + it("resolves a local auth app-slot to its workspace directory", () => { + expect(resolveAuthWorkspace({ app: { auth: { development: "local:plugins/auth" } } })).toBe( + "plugins/auth", + ); + }); + + it("returns null when auth is absent", () => { + expect(resolveAuthWorkspace(undefined)).toBeNull(); + expect(resolveAuthWorkspace(null)).toBeNull(); + expect(resolveAuthWorkspace({})).toBeNull(); + expect(resolveAuthWorkspace({ app: {} })).toBeNull(); + expect(resolveAuthWorkspace({ app: { auth: {} } })).toBeNull(); + }); + + it("returns null for malformed or non-local development values", () => { + expect(resolveAuthWorkspace({ app: { auth: { development: 42 } } })).toBeNull(); + expect( + resolveAuthWorkspace({ app: { auth: { development: "https://auth.example.com" } } }), + ).toBeNull(); + }); +}); + +describe("everyPluginStamp", () => { + it("reads the version from the every-plugin shared entry", () => { + expect( + everyPluginStamp({ + shared: [ + { name: "zod", version: "4.6.5" }, + { name: "every-plugin", version: "2.10.1" }, + ], + }), + ).toBe("2.10.1"); + }); + + it("falls back to requiredVersion when version is absent", () => { + expect( + everyPluginStamp({ shared: [{ name: "every-plugin", requiredVersion: "3.0.0-rc.0" }] }), + ).toBe("3.0.0-rc.0"); + }); + + it("returns null when every-plugin is not shared", () => { + expect(everyPluginStamp({ shared: [{ name: "zod", version: "4.6.5" }] })).toBeNull(); + expect(everyPluginStamp({})).toBeNull(); + expect(everyPluginStamp({ shared: [] })).toBeNull(); + }); +}); + +describe("checkFederationTrain", () => { + const tempDirs: string[] = []; + + afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) rmSync(dir, { recursive: true, force: true }); + } + }); + + const distWithManifest = (manifest: unknown): string => { + const dir = mkdtempSync(join(tmpdir(), "federation-train-")); + tempDirs.push(dir); + writeFileSync(join(dir, "mf-manifest.json"), `${JSON.stringify(manifest)}\n`); + return dir; + }; + + it("passes when the stamp matches the expected train", () => { + const check = checkFederationTrain( + distWithManifest({ shared: [{ name: "every-plugin", version: "2.10.1" }] }), + "2.10.1", + ); + expect(check).toEqual({ ok: true, stamp: "2.10.1" }); + }); + + it("fails on a stale train and reports the stamped version", () => { + const check = checkFederationTrain( + distWithManifest({ shared: [{ name: "every-plugin", version: "2.10.1" }] }), + "3.0.0-rc.0", + ); + expect(check).toEqual({ ok: false, reason: "mismatch", stamp: "2.10.1" }); + }); + + it("fails when the dist predates federation metadata", () => { + const dir = mkdtempSync(join(tmpdir(), "federation-train-")); + tempDirs.push(dir); + expect(checkFederationTrain(dir, "2.10.1")).toEqual({ + ok: false, + reason: "missing-manifest", + stamp: null, + }); + }); + + it("fails when the manifest shares no every-plugin entry", () => { + const check = checkFederationTrain( + distWithManifest({ shared: [{ name: "zod", version: "4.6.5" }] }), + "2.10.1", + ); + expect(check).toEqual({ ok: false, reason: "no-stamp", stamp: null }); + }); +}); diff --git a/scripts/regression/container-build.ts b/scripts/regression/container-build.ts index 3db1fa1e7..f20715a51 100644 --- a/scripts/regression/container-build.ts +++ b/scripts/regression/container-build.ts @@ -12,11 +12,17 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; +import type { BuildEntryReport } from "every-plugin/build/artifact-names"; import { emitCoreUiStubs } from "every-plugin/build/ui"; import { containerName } from "every-plugin/identity"; import { composeVersionManifest } from "every-plugin/version-manifest"; import { writeResolvedConfig } from "../../packages/everything-dev/src/config"; +import { + checkFederationTrain, + resolveAuthWorkspace, +} from "../../packages/everything-dev/src/federation-train"; import { openResolution } from "../../packages/everything-dev/src/resolution/session"; +import { readBuildReport as readWorkspaceBuildReport } from "../../packages/everything-dev/src/version-manifest-deploy"; /** * Local production fixture rewriting (ADR 0009): a section named in the plan @@ -221,6 +227,10 @@ const localPlugins = Object.entries(bosConfig.plugins ?? {}) .map(([key, ref]) => [key, ref.development.slice("local:".length)] as const) .sort(([a], [b]) => a.localeCompare(b)); +// app.auth is an app-slot, not a plugins.* entry — its workspace build +// (`every-plugin build`) covers the api remote AND the folder-form ui. +const authWorkspace = resolveAuthWorkspace(bosConfig); + const run = (cmd: string, args: string[], cwd: string, env: Record = {}) => { const result = spawnSync(cmd, args, { cwd: path.join(root, cwd), @@ -251,11 +261,7 @@ const build = () => { console.log("[container-build] api remote…"); run("pnpm", ["run", "build"], "api"); - // app.auth is an app-slot, not a plugins.* entry — its workspace build - // (`every-plugin build`) covers the api remote AND the folder-form ui. - const authDevelopment = bosConfig.app?.auth?.development; - if (typeof authDevelopment === "string" && authDevelopment.startsWith("local:")) { - const authWorkspace = authDevelopment.slice("local:".length); + if (authWorkspace) { console.log("[container-build] auth app-slot (api remote + folder-form ui)…"); run("pnpm", ["run", "build"], authWorkspace); } @@ -281,27 +287,22 @@ const frameworkTrain = JSON.parse( const trainCheckedDists: string[] = []; function assertFederationTrain(distDir: string): void { - const manifestPath = path.join(root, distDir, "mf-manifest.json"); - if (!existsSync(manifestPath)) { - throw new Error( - `[container-build] ${distDir} has no mf-manifest.json — the plugin build did not emit federation metadata`, - ); - } - const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { - shared?: Array<{ name?: string; version?: string; requiredVersion?: string | null }>; - }; - const entry = manifest.shared?.find((dep) => dep.name === "every-plugin"); - const stamped = entry?.version ?? entry?.requiredVersion ?? null; - if (stamped !== frameworkTrain) { + const check = checkFederationTrain(path.join(root, distDir), frameworkTrain); + if (!check.ok) { + const reason = + check.reason === "missing-manifest" + ? "has no mf-manifest.json — the plugin build did not emit federation metadata" + : check.reason === "no-stamp" + ? "stamps no every-plugin share version in its mf-manifest.json" + : `was built against every-plugin@${check.stamp} but the framework train is ${frameworkTrain}`; throw new Error( - `[container-build] ${distDir} was built against every-plugin@${stamped ?? "unknown"} ` + - `but the framework train is ${frameworkTrain} — the plugin build resolved a stale ` + + `[container-build] ${distDir} ${reason} — the plugin build resolved a stale or missing ` + `every-plugin (checked so far, all matching: ${trainCheckedDists.join(", ") || "none"}). ` + `Rebuild so all remotes stamp the same framework version.`, ); } trainCheckedDists.push(distDir); - console.log(`[container-build] ${distDir} stamps every-plugin@${stamped} ✓`); + console.log(`[container-build] ${distDir} stamps every-plugin@${check.stamp} ✓`); } // Fixed in-container port map — every service is container-local, so only the @@ -318,19 +319,8 @@ const ports = { const sri384 = (bytes: string | Uint8Array): string => `sha384-${createHash("sha384").update(bytes).digest("base64")}`; -function readBuildReport( - distDir: string, -): { entry: string; browserManifest?: string; css?: string } | null { - const reportPath = path.join(root, distDir, "build-report.json"); - if (!existsSync(reportPath)) return null; - try { - const report = JSON.parse(readFileSync(reportPath, "utf8")) as { entry?: string }; - if (typeof report.entry !== "string" || !report.entry) return null; - return report as { entry: string; browserManifest?: string; css?: string }; - } catch { - return null; - } -} +const readBuildReport = (distDir: string): BuildEntryReport | null => + readWorkspaceBuildReport(path.join(root, distDir)); /** * The fixture pins every slot it serves (ADR 0009 amendment: version-manifest @@ -391,12 +381,8 @@ const stage = () => { }; // app.auth is an app-slot, not a plugins.* entry — stage its api dist and - // give it its server entry explicitly. - const authDevelopment = bosConfig.app?.auth?.development; - const authWorkspace = - typeof authDevelopment === "string" && authDevelopment.startsWith("local:") - ? authDevelopment.slice("local:".length) - : null; + // give it its server entry explicitly. `authWorkspace` is derived once at + // module level from the resolved config. // Slot pins are composed FIRST, from the repo dist dirs, so both the image // copy and the namespace copy carry the version manifests.