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/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/integration/framework-packages.ts b/packages/everything-dev/tests/integration/framework-packages.ts
index beb174e4f..d2aa7fc20 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,35 +22,42 @@ export function getFrameworkTarballs(repoRoot: string): Promise {
const tarballDir = mkdtempSync(join(tmpdir(), "everything-dev-framework-packages-"));
- 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 everyPluginTarball = await stageAndPackFrameworkPackage({
- repoRoot,
- packageName: "every-plugin",
- tarballDir,
- rootCatalog,
- });
-
- // Bun fails to resolve a tarball when it is both a direct dependency and a
- // transitive dependency (through another tarball) and both use the same
- // file path. We create a copy with a different name for the transitive
- // reference inside everything-dev so the paths differ.
- const everyPluginTarballCopy = join(tarballDir, "every-plugin-2.7.0-copy.tgz");
- cpSync(everyPluginTarball, everyPluginTarballCopy);
+ // 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. 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: { "every-plugin": everyPluginTarballCopy },
+ otherFrameworkTarballs: transitiveTarballs,
});
return {
- "every-plugin": everyPluginTarball,
+ "better-near-auth": directTarballs["better-near-auth"],
+ "every-plugin": directTarballs["every-plugin"],
"everything-dev": everythingDevTarball,
};
}
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 46b4b1887..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);
}
@@ -264,8 +270,41 @@ const build = () => {
console.log(`[container-build] plugin ${key} (api remote)…`);
run("pnpm", ["run", "build"], workspace);
}
+
+ assertFederationTrain("api/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
+// 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;
+
+const trainCheckedDists: string[] = [];
+
+function assertFederationTrain(distDir: string): void {
+ 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} ${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@${check.stamp} ✓`);
+}
+
// Fixed in-container port map — every service is container-local, so only the
// host port needs mapping at `docker run`.
const basePort = 4100;
@@ -280,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
@@ -353,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.