feat: the public @ekklesia package release train, built, reproduced, and proven (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / package-release (pull_request) Failing after 16s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 44s
CI / deploy-smoke (split) (pull_request) Successful in 1m29s

Produces the exact public package tarballs of a platform release, validates
them, and proves a standalone module consumes those artifacts end to end.
Nothing is published and no credential is configured.

- scripts/package-release.ts (pnpm packages:release) is the canonical
  release builder: from a clean commit it rebuilds the nine public
  packages from clean output, packs each, inspects every tarball, checks
  the release set and coordinated versions, and only then writes
  ekklesia-packages-<version>/ with the tarballs, SHA256SUMS, and a
  provider-neutral manifest (ekklesia-package-train.json: version,
  revision, and each package's file, size, SHA-256, npm integrity).
- Packing uses a pack-only pnpm beforePacking hook (passed with
  --config.pnpmfile, never the workspace's install settings). pnpm inserts
  rewritten workspace: dependencies in async completion order, so two
  checkouts of one commit packed module-pack's package.json with keys in a
  different order; the hook restores the source key order (and the
  inspection enforces it). It also drops devDependencies, which named
  private packages outside the train.
- scripts/test-package-release.sh (pnpm test:package-release, the CI
  package-release gate) builds the train twice, the second time from a
  separate worktree, requires byte-identical release directories, then
  runs the standalone consumer proof on the release: manifest checked,
  offline install from the tarballs only, lockfile integrity equal to the
  manifest's, frozen-lockfile reinstall, no Drizzle, one React, contracts
  only transitive, every @ekklesia package and module-pack at the release
  version, skipLibCheck: false, UI build, pack, and Core verification.
- docs/development/releases.md documents the release order, the train,
  the artifacts, and the reproducibility findings; registry publication
  remains unimplemented.
- ADR 0053 is Accepted: everything it decides that does not need a
  registry is now proven, and registry provisioning is on its own
  Deferred list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-08 08:02:41 -05:00
co-authored by Claude Opus 5.5
parent 7ea2d7883f
commit 9debe4e891
17 changed files with 1253 additions and 370 deletions
+162 -8
View File
@@ -4,17 +4,25 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import {
artifactDigests,
PACKAGE_TRAIN_MANIFEST,
type PackageTrainManifest,
PUBLIC_PACKAGE_DISTRIBUTION,
PUBLIC_PACKAGES,
packedManifestProblems,
releaseDirectoryProblems,
releaseSetProblems,
serializeManifest,
tarballFileProblems,
} from "../scripts/pack-public-packages.js";
tarballName,
workspacePackages as releaseWorkspacePackages,
} from "../scripts/package-release.js";
/**
* The public module-development package boundary (ADR 0053): which packages may be published, what
* their manifests may say, and what the author-facing SDK exposes. The tarballs themselves are
* inspected by `pnpm packages:pack` and installed into a standalone module by
* `scripts/test-standalone-module.sh`.
* The public module-development package boundary and its release train (ADR 0053): which packages
* may be published, what their manifests may say, what the author-facing SDK exposes, and what a
* release directory must hold. The tarballs themselves are built and inspected by
* `pnpm packages:release` (`scripts/package-release.ts`), and proven by `pnpm test:package-release`.
*/
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -78,6 +86,41 @@ describe("the public package set", () => {
expect(packages.get("@ekklesia/module-template")?.manifest.private).toBe(true);
});
it("is what the release builder itself accepts, read without a YAML parser", async () => {
const platformVersion = (await readJson("package.json")).version;
const read = await releaseWorkspacePackages(repositoryRoot);
expect(read.map((entry) => entry.name).sort()).toEqual(
[...(await workspacePackages()).keys()].sort(),
);
expect(releaseSetProblems(read, platformVersion)).toEqual([]);
});
it("refuses a missing, mis-versioned, or private public package, and anything else publishable", async () => {
const read = await releaseWorkspacePackages(repositoryRoot);
const without = read.filter((entry) => entry.name !== "@ekklesia/ui");
expect(releaseSetProblems(without, "0.1.2").join(" ")).toContain("@ekklesia/ui is missing");
const changed = read.map((entry) =>
entry.name === "@ekklesia/module-sdk"
? { ...entry, manifest: { ...entry.manifest, private: true, version: "0.2.0" } }
: entry.name === "@ekklesia/core"
? { ...entry, manifest: { ...entry.manifest, private: undefined } }
: entry.name === "@ekklesia/module-ui-host"
? {
...entry,
manifest: {
...entry.manifest,
ekklesia: { distribution: PUBLIC_PACKAGE_DISTRIBUTION },
},
}
: entry,
);
const problems = releaseSetProblems(changed, "0.1.2").join(" ");
expect(problems).toContain("@ekklesia/module-sdk is version 0.2.0");
expect(problems).toContain("@ekklesia/module-sdk is private");
expect(problems).toContain("@ekklesia/core is publishable but not in the release set");
expect(problems).toContain("@ekklesia/module-ui-host carries the distribution marker");
});
it("uses one coordinated version, and depends only on other public packages through the workspace", async () => {
const platformVersion = (await readJson("package.json")).version;
const publicNames = new Set(PUBLIC_PACKAGES.map((entry) => entry.name));
@@ -208,10 +251,11 @@ describe("module authors' dependencies", () => {
}
});
it("pin module-pack exactly and declare engines.node in the standalone fixture", async () => {
it("pin module-pack exactly, at this release, and declare engines.node in the standalone fixture", async () => {
const fixture = await readJson("tests/fixtures/standalone-module/package.json");
const devDependencies = fixture.devDependencies as Record<string, string>;
expect(devDependencies["@ekklesia/module-pack"]).toMatch(/^\d+\.\d+\.\d+$/u);
// Part of setting a release version: the fixture consumes exactly this release's module-pack.
expect(devDependencies["@ekklesia/module-pack"]).toBe((await readJson("package.json")).version);
expect((fixture.engines as Record<string, string>).node).toBeTruthy();
});
});
@@ -231,7 +275,6 @@ describe("@ekklesia/typescript-config", () => {
describe("the tarball inspection", () => {
const good = {
dependencies: { "@ekklesia/contracts": "0.1.2", zod: "4.4.3" },
devDependencies: { "@ekklesia/core": "0.1.2" },
ekklesia: { distribution: PUBLIC_PACKAGE_DISTRIBUTION, layer: "module-sdk" },
files: ["dist"],
license: "AGPL-3.0-or-later",
@@ -255,6 +298,10 @@ describe("the tarball inspection", () => {
{ ...good, dependencies: { "@ekklesia/contracts": "0.1.1" } },
{ ...good, peerDependencies: { "@ekklesia/core": "0.1.2" } },
{ ...good, version: "0.1.3" },
// Development dependencies never ship: they name private packages outside the train.
{ ...good, devDependencies: { "@ekklesia/core": "0.1.2" } },
// A registry is the publication step's configuration, never part of an artifact.
{ ...good, publishConfig: { registry: "https://registry.example/" } },
];
for (const manifest of cases) {
expect(packedManifestProblems(manifest, expected), JSON.stringify(manifest)).not.toEqual([]);
@@ -280,4 +327,111 @@ describe("the tarball inspection", () => {
expect(tarballFileProblems(files, good, hasSource), files.join(" ")).not.toEqual([]);
}
});
it("refuses a dependency map whose order differs from its source", () => {
const source = { dependencies: { "@ekklesia/contracts": "workspace:*", zod: "4.4.3" } };
expect(packedManifestProblems(good, expected, source)).toEqual([]);
const reordered = { ...good, dependencies: { zod: "4.4.3", "@ekklesia/contracts": "0.1.2" } };
expect(packedManifestProblems(reordered, expected, source)).toEqual([
"orders dependencies differently from its source",
]);
});
});
describe("the pack hook", () => {
it("drops devDependencies and restores the source's key order, changing nothing else", async () => {
const { hooks } = await import("../scripts/lib/pack-hooks.cjs");
const directory = path.join(repositoryRoot, "packages", "module-pack");
const source = await readJson("packages/module-pack/package.json");
const dependencies = Object.fromEntries(
Object.keys(source.dependencies ?? {})
.reverse()
.map((name) => [name, name.startsWith("@ekklesia/") ? "0.1.2" : "1.0.0"]),
);
const packed = hooks.beforePacking(
{ ...source, dependencies, devDependencies: { "@ekklesia/core": "0.1.2" } },
directory,
);
expect(packed.devDependencies).toBeUndefined();
expect(Object.keys(packed.dependencies as object)).toEqual(
Object.keys(source.dependencies ?? {}),
);
expect(packed.dependencies).toEqual(dependencies);
expect(packed.name).toBe(source.name);
});
});
describe("the release directory", () => {
const version = "0.1.2";
function release(mutate?: (files: Map<string, Uint8Array>) => void) {
const files = new Map<string, Uint8Array>();
for (const entry of PUBLIC_PACKAGES) {
files.set(tarballName(entry.name, version), Buffer.from(`tarball of ${entry.name}`));
}
const manifest: PackageTrainManifest = {
packageTrain: 1,
packages: PUBLIC_PACKAGES.map((entry) => {
const file = tarballName(entry.name, version);
const data = files.get(file) ?? new Uint8Array();
return {
file,
name: entry.name,
role: entry.role,
size: data.length,
version,
...artifactDigests(data),
};
}),
source: { modified: false, revision: "86ae907f59458508e3277666cdd214f5bd7c1078" },
version,
};
mutate?.(files);
files.set(PACKAGE_TRAIN_MANIFEST, Buffer.from(serializeManifest(manifest)));
return { files, manifest };
}
it("names each tarball as pnpm pack does", () => {
expect(tarballName("@ekklesia/module-sdk", "0.1.2")).toBe("ekklesia-module-sdk-0.1.2.tgz");
});
it("records npm-compatible integrity and SHA-256 for each artifact", () => {
const digests = artifactDigests(Buffer.from("abc"));
expect(digests.sha256).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
expect(digests.integrity).toMatch(/^sha512-[A-Za-z0-9+/]{86}==$/u);
});
it("accepts exactly the expected train, matching its manifest", () => {
const { files, manifest } = release();
expect(releaseDirectoryProblems(manifest, files)).toEqual([]);
});
it("refuses a missing, extra, or altered artifact", () => {
const missing = release((files) => files.delete(tarballName("@ekklesia/ui", version)));
expect(releaseDirectoryProblems(missing.manifest, missing.files).join(" ")).toContain(
"ekklesia-ui-0.1.2.tgz is missing",
);
const extra = release((files) =>
files.set("ekklesia-module-ui-host-0.1.2.tgz", Buffer.from("x")),
);
expect(releaseDirectoryProblems(extra.manifest, extra.files).join(" ")).toContain(
"ekklesia-module-ui-host-0.1.2.tgz is not part of the release",
);
const altered = release((files) =>
files.set(tarballName("@ekklesia/module-pack", version), Buffer.from("changed")),
);
expect(releaseDirectoryProblems(altered.manifest, altered.files).join(" ")).toContain(
"does not match its size, SHA-256, or integrity",
);
});
it("serializes the manifest the same way every time, packages sorted by name", () => {
const { manifest } = release();
const reversed = { ...manifest, packages: [...manifest.packages].reverse() };
expect(serializeManifest(reversed)).toBe(serializeManifest(manifest));
const parsed = JSON.parse(serializeManifest(manifest)) as PackageTrainManifest;
expect(parsed.packages.map((entry) => entry.name)).toEqual(
[...PUBLIC_PACKAGES.map((entry) => entry.name)].sort(),
);
expect(Object.keys(parsed)).toEqual(["packageTrain", "version", "source", "packages"]);
});
});