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
+56
View File
@@ -0,0 +1,56 @@
// pnpm hooks for packing the public package train, and nothing else: passed to `pnpm pack` with
// `--pnpmfile` by scripts/package-release.ts, never installed as the workspace's .pnpmfile.cjs, so
// installs and the lockfile are unaffected.
//
// `beforePacking` runs after pnpm has rewritten `workspace:` ranges into exact versions, and does two
// things to the packed package.json only:
//
// - It drops `devDependencies`. A consumer never installs them, and a tooling package's test-only
// development dependencies name private workspace packages (Core, the module template) that are
// not part of the public package train.
// - It puts every remaining dependency map back in the source package.json's own key order. pnpm
// rewrites `workspace:` ranges concurrently and inserts each entry as its lookup resolves, so two
// packs of the same commit could order those keys differently, the one difference that made
// otherwise byte-identical tarballs differ. No name or range changes.
"use strict";
const { readFileSync } = require("node:fs");
const path = require("node:path");
const DEPENDENCY_FIELDS = [
"dependencies",
"optionalDependencies",
"peerDependencies",
"peerDependenciesMeta",
];
function inSourceOrder(packed, source) {
if (
packed === null ||
typeof packed !== "object" ||
source === null ||
typeof source !== "object"
) {
return packed;
}
const ordered = {};
for (const key of Object.keys(source)) {
if (Object.hasOwn(packed, key)) ordered[key] = packed[key];
}
for (const key of Object.keys(packed)) {
if (!Object.hasOwn(ordered, key)) ordered[key] = packed[key];
}
return ordered;
}
function beforePacking(manifest, directory) {
const source = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
const packed = { ...manifest };
delete packed.devDependencies;
for (const field of DEPENDENCY_FIELDS) {
if (packed[field] !== undefined) packed[field] = inSourceOrder(packed[field], source[field]);
}
return packed;
}
module.exports = { hooks: { beforePacking }, inSourceOrder };
+8
View File
@@ -0,0 +1,8 @@
/** Types for `pack-hooks.cjs`, the pnpm hooks applied to `pnpm pack` by scripts/package-release.ts. */
type Manifest = Record<string, unknown>;
export declare const hooks: {
beforePacking(manifest: Manifest, directory: string): Manifest;
};
export declare function inSourceOrder(packed: unknown, source: unknown): unknown;
-304
View File
@@ -1,304 +0,0 @@
import { execFile } from "node:child_process";
import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
/**
* The public module-development package set (ADR 0053), and the release check that packs it.
*
* node scripts/pack-public-packages.ts --out <directory>
*
* Rebuilds each public package from clean output, packs it with `pnpm pack`, and inspects every
* tarball before anything may consume it: only the package's built files, its declarations, and a
* `LICENSE`; a manifest with no `workspace:` range, no install-time lifecycle script, the explicit
* distribution marker, and the one coordinated version. It publishes nothing.
*/
const execFileAsync = promisify(execFile);
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
export type PublicPackageRole = "author-api" | "tooling" | "pre-1.0" | "implementation";
export interface PublicPackage {
readonly name: string;
/** Relative to the repository root. */
readonly directory: string;
readonly role: PublicPackageRole;
}
/** ADR 0053's public set. Every other workspace package is private and never published. */
export const PUBLIC_PACKAGES: readonly PublicPackage[] = Object.freeze([
{ directory: "packages/module-sdk", name: "@ekklesia/module-sdk", role: "author-api" },
{ directory: "packages/module-database", name: "@ekklesia/module-database", role: "author-api" },
{ directory: "packages/module-ui-sdk", name: "@ekklesia/module-ui-sdk", role: "author-api" },
{ directory: "packages/design-tokens", name: "@ekklesia/design-tokens", role: "author-api" },
{ directory: "packages/module-ui-build", name: "@ekklesia/module-ui-build", role: "tooling" },
{ directory: "packages/module-pack", name: "@ekklesia/module-pack", role: "tooling" },
{
directory: "packages/typescript-config",
name: "@ekklesia/typescript-config",
role: "tooling",
},
{ directory: "packages/ui", name: "@ekklesia/ui", role: "pre-1.0" },
// A dependency of the packages above, never a supported direct module-author dependency.
{ directory: "packages/contracts", name: "@ekklesia/contracts", role: "implementation" },
]);
/** `ekklesia.distribution` in a public package's manifest: the identity the canonical packager
* recognizes an installed Ekklesia package by (`packages/module-pack/src/packages.ts`). */
export const PUBLIC_PACKAGE_DISTRIBUTION = "module-development";
/** Scripts a package manager runs on a consumer's machine when it installs, packs, or publishes. */
const LIFECYCLE_SCRIPTS = Object.freeze([
"preinstall",
"install",
"postinstall",
"prepare",
"prepack",
"postpack",
"prepublish",
"prepublishOnly",
"publish",
"postpublish",
]);
/** The fields a consumer's install resolves. `devDependencies` are never installed from a published
* package, so a tooling package's test-only use of a private package is not a problem there. */
const INSTALLED_DEPENDENCY_FIELDS = Object.freeze([
"dependencies",
"peerDependencies",
"optionalDependencies",
]);
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** What is wrong with a packed `package.json`, or nothing. */
export function packedManifestProblems(
manifest: Readonly<Record<string, unknown>>,
expected: { readonly name: string; readonly version: string },
): string[] {
const problems: string[] = [];
if (manifest.name !== expected.name) problems.push(`is named ${String(manifest.name)}`);
if (manifest.version !== expected.version) {
problems.push(
`has version ${String(manifest.version)}, not the coordinated ${expected.version}`,
);
}
if (manifest.private !== undefined) problems.push("declares private");
if (manifest.license !== "AGPL-3.0-or-later") problems.push("does not declare its licence");
if (JSON.stringify(manifest).includes("workspace:")) problems.push("keeps a workspace: range");
const ekklesia = manifest.ekklesia;
if (!isRecord(ekklesia) || ekklesia.distribution !== PUBLIC_PACKAGE_DISTRIBUTION) {
problems.push("lacks the module-development distribution marker");
}
const scripts = isRecord(manifest.scripts) ? manifest.scripts : {};
for (const script of LIFECYCLE_SCRIPTS) {
if (Object.hasOwn(scripts, script)) problems.push(`runs a ${script} script`);
}
const publicNames = new Set(PUBLIC_PACKAGES.map((entry) => entry.name));
for (const field of INSTALLED_DEPENDENCY_FIELDS) {
const dependencies = manifest[field];
if (!isRecord(dependencies)) continue;
for (const [name, range] of Object.entries(dependencies)) {
if (!name.startsWith("@ekklesia/")) continue;
if (!publicNames.has(name)) problems.push(`${field} names the private ${name}`);
else if (range !== expected.version) {
problems.push(`${field} names ${name}@${String(range)}, not ${expected.version}`);
}
}
}
return problems;
}
const BUILT_FILE = /^dist\/(.+?)(?:\.d\.ts|\.js)(?:\.map)?$/u;
/**
* What is wrong with a tarball's file list, or nothing. Every file is `package.json`, `LICENSE`, or
* under one of the manifest's `files` entries; nothing is a source, test, or configuration file; and
* every built file has the source it was compiled from, so stale output from a deleted source fails.
*/
export function tarballFileProblems(
files: readonly string[],
manifest: Readonly<Record<string, unknown>>,
hasSource: (relativeWithoutExtension: string) => boolean,
): string[] {
const problems: string[] = [];
const declared = Array.isArray(manifest.files) ? manifest.files.map(String) : [];
if (!files.includes("LICENSE")) problems.push("has no LICENSE");
if (!files.includes("package.json")) problems.push("has no package.json");
for (const file of files) {
if (file === "package.json" || file === "LICENSE") continue;
if (!declared.some((entry) => file === entry || file.startsWith(`${entry}/`))) {
problems.push(`ships ${file}, which its files field does not name`);
continue;
}
if (
/(?:^|\/)(?:src|tests?|node_modules)\//u.test(file) ||
/\.test\.[cm]?[jt]sx?$/u.test(file)
) {
problems.push(`ships ${file}`);
continue;
}
const built = BUILT_FILE.exec(file);
if (file.startsWith("dist/") && (built === null || !hasSource(built[1] ?? ""))) {
problems.push(`ships ${file}, which no source produces`);
}
}
return problems;
}
async function readJson(file: string): Promise<Record<string, unknown>> {
return JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>;
}
async function filesUnder(directory: string): Promise<string[]> {
const entries = await readdir(directory, { recursive: true, withFileTypes: true });
return entries
.filter((entry) => entry.isFile())
.map((entry) =>
path.relative(directory, path.join(entry.parentPath, entry.name)).split(path.sep).join("/"),
)
.sort();
}
async function sourceExists(packageDirectory: string, relative: string): Promise<boolean> {
for (const extension of [".ts", ".tsx"]) {
try {
await stat(path.join(packageDirectory, "src", `${relative}${extension}`));
return true;
} catch {}
}
return false;
}
export interface PackedPackage {
readonly name: string;
readonly tarball: string;
readonly files: number;
readonly bytes: number;
}
async function pnpm(arguments_: readonly string[], cwd: string): Promise<string> {
const { stdout } = await execFileAsync("pnpm", [...arguments_], {
cwd,
maxBuffer: 64 * 1024 * 1024,
});
return stdout;
}
async function packOne(
entry: PublicPackage,
version: string,
outDirectory: string,
scratch: string,
): Promise<{ packed: PackedPackage; problems: string[] }> {
const directory = path.join(repositoryRoot, entry.directory);
const report = JSON.parse(
await pnpm(["pack", "--json", "--pack-destination", outDirectory], directory),
) as { filename: string };
const tarball = report.filename;
const extracted = path.join(scratch, entry.name.replace("/", "+"));
await mkdir(extracted, { recursive: true });
await execFileAsync("tar", ["-xzf", tarball, "-C", extracted]);
const root = path.join(extracted, "package");
const files = await filesUnder(root);
const manifest = await readJson(path.join(root, "package.json"));
const sources = new Map<string, boolean>();
for (const file of files) {
const built = BUILT_FILE.exec(file)?.[1];
if (built !== undefined && !sources.has(built)) {
sources.set(built, await sourceExists(directory, built));
}
}
const problems = [
...packedManifestProblems(manifest, { name: entry.name, version }),
...tarballFileProblems(files, manifest, (relative) => sources.get(relative) === true),
];
// Nothing the package says may name the machine or checkout it was packed on.
for (const file of files) {
const text = (await readFile(path.join(root, file))).toString("latin1");
for (const local of [repositoryRoot, homedir()]) {
if (text.includes(local)) problems.push(`${file} names a local path`);
}
}
return {
packed: { bytes: (await stat(tarball)).size, files: files.length, name: entry.name, tarball },
problems: [...new Set(problems)],
};
}
/** Rebuilds the public set from clean output, packs it into `outDirectory`, and inspects it. */
export async function packPublicPackages(outDirectory: string): Promise<{
readonly packed: readonly PackedPackage[];
readonly problems: readonly string[];
}> {
const version = String((await readJson(path.join(repositoryRoot, "package.json"))).version);
await mkdir(outDirectory, { recursive: true });
const out = await realpath(outDirectory);
// Stale output from a deleted source would otherwise ship: the build only ever adds files, and
// Turbo's cache would restore whatever an earlier build left behind.
for (const entry of PUBLIC_PACKAGES) {
await rm(path.join(repositoryRoot, entry.directory, "dist"), { force: true, recursive: true });
}
await pnpm(
[
"exec",
"turbo",
"run",
"build",
"--force",
"--output-logs=errors-only",
...PUBLIC_PACKAGES.map((entry) => `--filter=${entry.name}`),
],
repositoryRoot,
);
const scratch = await mkdtemp(path.join(tmpdir(), "ekklesia-public-packages-"));
try {
const packed: PackedPackage[] = [];
const problems: string[] = [];
for (const entry of PUBLIC_PACKAGES) {
const result = await packOne(entry, version, out, scratch);
packed.push(result.packed);
problems.push(...result.problems.map((problem) => `${entry.name} ${problem}.`));
}
return { packed, problems };
} finally {
await rm(scratch, { force: true, recursive: true });
}
}
async function main(argv: readonly string[]): Promise<number> {
if (argv.length !== 2 || argv[0] !== "--out" || !argv[1]) {
process.stderr.write("Usage: node scripts/pack-public-packages.ts --out <directory>\n");
return 64;
}
try {
const { packed, problems } = await packPublicPackages(path.resolve(argv[1]));
for (const entry of packed) {
process.stdout.write(
`${path.basename(entry.tarball)}: ${entry.files} files, ${entry.bytes} bytes\n`,
);
}
if (problems.length > 0) {
for (const problem of problems) process.stderr.write(`${problem}\n`);
return 1;
}
process.stdout.write(`Packed and inspected ${packed.length} public packages.\n`);
return 0;
} catch {
process.stderr.write("Packing the public packages failed while building or packing.\n");
return 1;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await main(process.argv.slice(2));
}
+600
View File
@@ -0,0 +1,600 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
/**
* The public package release train (ADR 0053): the canonical way to produce the exact
* `@ekklesia/*` package tarballs of one platform release. It publishes nothing.
*
* node scripts/package-release.ts --out <directory> [--allow-modified]
*
* From a clean source tree, it removes the public packages' build output, rebuilds them without the
* build cache, packs each with `pnpm pack`, inspects every tarball, and only when everything passed
* writes the release directory `<directory>/ekklesia-packages-<version>/`:
*
* ekklesia-<name>-<version>.tgz one per public package, exactly the expected set
* SHA256SUMS `sha256sum -c` compatible
* ekklesia-package-train.json the release manifest (`packageTrainManifest`)
*
* Those files are what a future registry publication step publishes, unchanged, after checking them
* against the manifest. `--allow-modified` builds from a source tree with uncommitted changes, for
* local development only; the manifest then says so, and such a release must never be published.
*/
const execFileAsync = promisify(execFile);
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
/** pnpm hooks applied to `pnpm pack` only (see the file itself). */
const PACK_HOOKS = path.join(repositoryRoot, "scripts", "lib", "pack-hooks.cjs");
export type PublicPackageRole = "author-api" | "tooling" | "pre-1.0" | "implementation";
export interface PublicPackage {
readonly name: string;
/** Relative to the repository root. */
readonly directory: string;
readonly role: PublicPackageRole;
}
/** ADR 0053's public set. Every other workspace package is private and never published. */
export const PUBLIC_PACKAGES: readonly PublicPackage[] = Object.freeze([
{ directory: "packages/module-sdk", name: "@ekklesia/module-sdk", role: "author-api" },
{ directory: "packages/module-database", name: "@ekklesia/module-database", role: "author-api" },
{ directory: "packages/module-ui-sdk", name: "@ekklesia/module-ui-sdk", role: "author-api" },
{ directory: "packages/design-tokens", name: "@ekklesia/design-tokens", role: "author-api" },
{ directory: "packages/module-ui-build", name: "@ekklesia/module-ui-build", role: "tooling" },
{ directory: "packages/module-pack", name: "@ekklesia/module-pack", role: "tooling" },
{
directory: "packages/typescript-config",
name: "@ekklesia/typescript-config",
role: "tooling",
},
{ directory: "packages/ui", name: "@ekklesia/ui", role: "pre-1.0" },
// A dependency of the packages above, never a supported direct module-author dependency.
{ directory: "packages/contracts", name: "@ekklesia/contracts", role: "implementation" },
]);
/** `ekklesia.distribution` in a public package's manifest: the identity the canonical packager
* recognizes an installed Ekklesia package by (`packages/module-pack/src/packages.ts`). */
export const PUBLIC_PACKAGE_DISTRIBUTION = "module-development";
/** The release manifest's file name, and its format version. */
export const PACKAGE_TRAIN_MANIFEST = "ekklesia-package-train.json";
export const PACKAGE_TRAIN_FORMAT = 1;
/** `@ekklesia/module-sdk` → `ekklesia-module-sdk-<version>.tgz`, the name `pnpm pack` writes. */
export function tarballName(name: string, version: string): string {
return `${name.replace(/^@/u, "").replace("/", "-")}-${version}.tgz`;
}
/** The directory one release's artifacts are written to, inside `--out`. */
export function releaseDirectoryName(version: string): string {
return `ekklesia-packages-${version}`;
}
/** Scripts a package manager runs on a consumer's machine when it installs, packs, or publishes. */
const LIFECYCLE_SCRIPTS = Object.freeze([
"preinstall",
"install",
"postinstall",
"prepare",
"prepack",
"postpack",
"prepublish",
"prepublishOnly",
"publish",
"postpublish",
]);
const DEPENDENCY_FIELDS = Object.freeze([
"dependencies",
"peerDependencies",
"optionalDependencies",
"devDependencies",
]);
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* What is wrong with a packed `package.json`, or nothing. `source` is the package's own source
* manifest, when there is one to compare with: a packed dependency map must keep its key order.
*/
export function packedManifestProblems(
manifest: Readonly<Record<string, unknown>>,
expected: { readonly name: string; readonly version: string },
source?: Readonly<Record<string, unknown>>,
): string[] {
const problems: string[] = [];
if (manifest.name !== expected.name) problems.push(`is named ${String(manifest.name)}`);
if (manifest.version !== expected.version) {
problems.push(
`has version ${String(manifest.version)}, not the coordinated ${expected.version}`,
);
}
if (manifest.private !== undefined) problems.push("declares private");
if (manifest.license !== "AGPL-3.0-or-later") problems.push("does not declare its licence");
if (JSON.stringify(manifest).includes("workspace:")) problems.push("keeps a workspace: range");
// A registry belongs to the publication step's configuration, never to an artifact.
if (manifest.publishConfig !== undefined) problems.push("names a publishConfig");
if (manifest.devDependencies !== undefined) problems.push("keeps devDependencies");
const ekklesia = manifest.ekklesia;
if (!isRecord(ekklesia) || ekklesia.distribution !== PUBLIC_PACKAGE_DISTRIBUTION) {
problems.push("lacks the module-development distribution marker");
}
const scripts = isRecord(manifest.scripts) ? manifest.scripts : {};
for (const script of LIFECYCLE_SCRIPTS) {
if (Object.hasOwn(scripts, script)) problems.push(`runs a ${script} script`);
}
const publicNames = new Set(PUBLIC_PACKAGES.map((entry) => entry.name));
for (const field of DEPENDENCY_FIELDS) {
const dependencies = manifest[field];
if (!isRecord(dependencies)) continue;
for (const [name, range] of Object.entries(dependencies)) {
if (!name.startsWith("@ekklesia/")) continue;
if (!publicNames.has(name)) problems.push(`${field} names the private ${name}`);
else if (range !== expected.version) {
problems.push(`${field} names ${name}@${String(range)}, not ${expected.version}`);
}
}
const sourceOrder = source?.[field];
if (isRecord(sourceOrder)) {
const order = Object.keys(sourceOrder).filter((key) => Object.hasOwn(dependencies, key));
if (JSON.stringify(order) !== JSON.stringify(Object.keys(dependencies))) {
problems.push(`orders ${field} differently from its source`);
}
}
}
return problems;
}
const BUILT_FILE = /^dist\/(.+?)(?:\.d\.ts|\.js)(?:\.map)?$/u;
/**
* What is wrong with a tarball's file list, or nothing. Every file is `package.json`, `LICENSE`, or
* under one of the manifest's `files` entries; nothing is a source, test, or configuration file; and
* every built file has the source it was compiled from, so stale output from a deleted source fails.
*/
export function tarballFileProblems(
files: readonly string[],
manifest: Readonly<Record<string, unknown>>,
hasSource: (relativeWithoutExtension: string) => boolean,
): string[] {
const problems: string[] = [];
const declared = Array.isArray(manifest.files) ? manifest.files.map(String) : [];
if (!files.includes("LICENSE")) problems.push("has no LICENSE");
if (!files.includes("package.json")) problems.push("has no package.json");
for (const file of files) {
if (file === "package.json" || file === "LICENSE") continue;
if (!declared.some((entry) => file === entry || file.startsWith(`${entry}/`))) {
problems.push(`ships ${file}, which its files field does not name`);
continue;
}
if (
/(?:^|\/)(?:src|tests?|node_modules)\//u.test(file) ||
/\.test\.[cm]?[jt]sx?$/u.test(file)
) {
problems.push(`ships ${file}`);
continue;
}
const built = BUILT_FILE.exec(file);
if (file.startsWith("dist/") && (built === null || !hasSource(built[1] ?? ""))) {
problems.push(`ships ${file}, which no source produces`);
}
}
return problems;
}
export interface WorkspacePackage {
readonly name: string;
/** Relative to the repository root. */
readonly directory: string;
readonly manifest: Readonly<Record<string, unknown>>;
}
/** Every package `pnpm-workspace.yaml` names, read without a YAML dependency: the file lists
* literal directories and `<directory>/*` patterns only. */
export async function workspacePackages(root = repositoryRoot): Promise<WorkspacePackage[]> {
const workspace = await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8");
const block = /^packages:\n((?:[ \t]+-[^\n]*\n)+)/mu.exec(workspace)?.[1] ?? "";
const patterns = [...block.matchAll(/-\s*["']?([^"'\n]+?)["']?\s*$/gmu)].map(
(match) => match[1] ?? "",
);
const packages: WorkspacePackage[] = [];
for (const pattern of patterns) {
const directories = pattern.endsWith("/*")
? (
await readdir(path.join(root, pattern.slice(0, -2)), { withFileTypes: true }).catch(
() => [],
)
)
.filter((entry) => entry.isDirectory())
.map((entry) => `${pattern.slice(0, -2)}/${entry.name}`)
: [pattern];
for (const directory of directories) {
const manifest = await readJson(path.join(root, directory, "package.json")).catch(
() => undefined,
);
if (manifest && typeof manifest.name === "string") {
packages.push({ directory, manifest, name: manifest.name });
}
}
}
return packages.sort((left, right) => (left.name < right.name ? -1 : 1));
}
/**
* What is wrong with the workspace's release set, or nothing: every public package exists where the
* list says, carries the marker and the platform version, and is not private; every other package is
* private and unmarked, so nothing can enter the train unnoticed.
*/
export function releaseSetProblems(
packages: readonly WorkspacePackage[],
platformVersion: string,
): string[] {
const problems: string[] = [];
const byName = new Map(packages.map((entry) => [entry.name, entry]));
const publicNames = new Set(PUBLIC_PACKAGES.map((entry) => entry.name));
for (const expected of PUBLIC_PACKAGES) {
const found = byName.get(expected.name);
if (found === undefined || found.directory !== expected.directory) {
problems.push(`${expected.name} is missing from ${expected.directory}.`);
continue;
}
if (found.manifest.version !== platformVersion) {
problems.push(
`${expected.name} is version ${String(found.manifest.version)}, not the platform's ${platformVersion}.`,
);
}
if (found.manifest.private !== undefined) problems.push(`${expected.name} is private.`);
}
for (const entry of packages) {
if (publicNames.has(entry.name)) continue;
const ekklesia = entry.manifest.ekklesia;
if (isRecord(ekklesia) && ekklesia.distribution === PUBLIC_PACKAGE_DISTRIBUTION) {
problems.push(`${entry.name} carries the distribution marker but is not in the release set.`);
}
if (entry.manifest.private !== true) {
problems.push(`${entry.name} is publishable but not in the release set.`);
}
}
return problems;
}
export interface TrainPackage {
readonly name: string;
readonly version: string;
readonly role: PublicPackageRole;
readonly file: string;
readonly size: number;
/** Hexadecimal SHA-256 of the tarball. */
readonly sha256: string;
/** The npm `dist.integrity` of the tarball: `sha512-<base64>`. */
readonly integrity: string;
}
/** The release manifest: what one train is, by content. Provider-neutral, and no registry catalog. */
export interface PackageTrainManifest {
readonly packageTrain: typeof PACKAGE_TRAIN_FORMAT;
/** The platform release version, and every package's version. */
readonly version: string;
readonly source: {
readonly revision: string;
/** True only for a local `--allow-modified` build, which must never be published. */
readonly modified: boolean;
};
readonly packages: readonly TrainPackage[];
}
/** What is wrong with a release directory's contents against its manifest, or nothing. */
export function releaseDirectoryProblems(
manifest: PackageTrainManifest,
files: ReadonlyMap<string, Uint8Array>,
): string[] {
const problems: string[] = [];
if (manifest.packageTrain !== PACKAGE_TRAIN_FORMAT) problems.push("unknown manifest format.");
const expected = new Map(
PUBLIC_PACKAGES.map((entry) => [tarballName(entry.name, manifest.version), entry.name]),
);
const listed = new Map(manifest.packages.map((entry) => [entry.file, entry]));
for (const [file, name] of expected) {
if (!listed.has(file)) problems.push(`the manifest does not list ${name}.`);
if (!files.has(file)) problems.push(`${file} is missing.`);
}
for (const file of files.keys()) {
if (!expected.has(file) && file !== PACKAGE_TRAIN_MANIFEST && file !== "SHA256SUMS") {
problems.push(`${file} is not part of the release.`);
}
}
for (const entry of manifest.packages) {
if (!expected.has(entry.file) || expected.get(entry.file) !== entry.name) {
problems.push(`the manifest lists ${entry.name} as ${entry.file}, which is not expected.`);
continue;
}
if (entry.version !== manifest.version) {
problems.push(`the manifest lists ${entry.name} at ${entry.version}.`);
}
const data = files.get(entry.file);
if (data === undefined) continue;
const digests = artifactDigests(data);
if (
data.length !== entry.size ||
digests.sha256 !== entry.sha256 ||
digests.integrity !== entry.integrity
) {
problems.push(`${entry.file} does not match its size, SHA-256, or integrity.`);
}
}
return problems;
}
export function artifactDigests(data: Uint8Array): { sha256: string; integrity: string } {
return {
integrity: `sha512-${createHash("sha512").update(data).digest("base64")}`,
sha256: createHash("sha256").update(data).digest("hex"),
};
}
/** The manifest's exact bytes: sorted packages, two-space JSON, one trailing newline. */
export function serializeManifest(manifest: PackageTrainManifest): string {
const packages = [...manifest.packages]
.sort((left, right) => (left.name < right.name ? -1 : 1))
.map((entry) => ({
file: entry.file,
integrity: entry.integrity,
name: entry.name,
role: entry.role,
sha256: entry.sha256,
size: entry.size,
version: entry.version,
}));
return `${JSON.stringify(
{
packageTrain: manifest.packageTrain,
version: manifest.version,
source: { modified: manifest.source.modified, revision: manifest.source.revision },
packages,
},
null,
2,
)}\n`;
}
async function readJson(file: string): Promise<Record<string, unknown>> {
return JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>;
}
async function filesUnder(directory: string): Promise<string[]> {
const entries = await readdir(directory, { recursive: true, withFileTypes: true });
return entries
.filter((entry) => entry.isFile())
.map((entry) =>
path.relative(directory, path.join(entry.parentPath, entry.name)).split(path.sep).join("/"),
)
.sort();
}
async function sourceExists(packageDirectory: string, relative: string): Promise<boolean> {
for (const extension of [".ts", ".tsx"]) {
try {
await stat(path.join(packageDirectory, "src", `${relative}${extension}`));
return true;
} catch {}
}
return false;
}
async function run(command: string, arguments_: readonly string[], cwd: string): Promise<string> {
const { stdout } = await execFileAsync(command, [...arguments_], {
cwd,
maxBuffer: 64 * 1024 * 1024,
});
return stdout;
}
async function inspectTarball(
entry: PublicPackage,
version: string,
tarball: string,
scratch: string,
): Promise<string[]> {
const directory = path.join(repositoryRoot, entry.directory);
const extracted = path.join(scratch, entry.name.replace("/", "+"));
await mkdir(extracted, { recursive: true });
await run("tar", ["-xzf", tarball, "-C", extracted], repositoryRoot);
const root = path.join(extracted, "package");
const files = await filesUnder(root);
const manifest = await readJson(path.join(root, "package.json"));
const source = await readJson(path.join(directory, "package.json"));
const sources = new Map<string, boolean>();
for (const file of files) {
const built = BUILT_FILE.exec(file)?.[1];
if (built !== undefined && !sources.has(built)) {
sources.set(built, await sourceExists(directory, built));
}
}
const problems = [
...packedManifestProblems(manifest, { name: entry.name, version }, source),
...tarballFileProblems(files, manifest, (relative) => sources.get(relative) === true),
];
// Nothing the package says may name the machine or checkout it was packed on.
for (const file of files) {
const text = (await readFile(path.join(root, file))).toString("latin1");
for (const local of [repositoryRoot, homedir(), tmpdir()]) {
if (text.includes(local)) problems.push(`${file} names a local path`);
}
}
return [...new Set(problems)].map((problem) => `${entry.name} ${problem}.`);
}
export class PackageReleaseError extends Error {
readonly problems: readonly string[];
constructor(problems: readonly string[]) {
super("The package release failed its checks.");
this.name = "PackageReleaseError";
this.problems = problems;
}
}
export interface PackageReleaseOptions {
/** The parent of the release directory, which must not exist yet. */
readonly outDirectory: string;
readonly allowModified?: boolean;
}
/**
* Builds, packs, and inspects the public package train, and writes the release directory only when
* every check passed. Returns its path and manifest; throws `PackageReleaseError` with every problem
* found otherwise.
*/
export async function buildPackageRelease(
options: PackageReleaseOptions,
): Promise<{ readonly directory: string; readonly manifest: PackageTrainManifest }> {
const version = String((await readJson(path.join(repositoryRoot, "package.json"))).version);
const setProblems = releaseSetProblems(await workspacePackages(), version);
if (setProblems.length > 0) throw new PackageReleaseError(setProblems);
const revision = (await run("git", ["rev-parse", "HEAD"], repositoryRoot)).trim();
const modified =
(
await run("git", ["status", "--porcelain", "--untracked-files=all"], repositoryRoot)
).trim() !== "";
if (modified && options.allowModified !== true) {
throw new PackageReleaseError([
"The source tree has uncommitted changes. A release is built from a clean commit.",
]);
}
const release = path.join(path.resolve(options.outDirectory), releaseDirectoryName(version));
try {
await stat(release);
throw new PackageReleaseError([`${release} already exists. A release is written once.`]);
} catch (error) {
if (error instanceof PackageReleaseError) throw error;
}
// Stale output from a deleted source would otherwise ship: the build only ever adds files, and
// Turbo's cache would restore whatever an earlier build left behind.
for (const entry of PUBLIC_PACKAGES) {
await rm(path.join(repositoryRoot, entry.directory, "dist"), { force: true, recursive: true });
}
await run(
"pnpm",
[
"exec",
"turbo",
"run",
"build",
"--force",
"--output-logs=errors-only",
...PUBLIC_PACKAGES.map((entry) => `--filter=${entry.name}`),
],
repositoryRoot,
);
const scratch = await mkdtemp(path.join(tmpdir(), "ekklesia-package-release-"));
try {
const staging = path.join(scratch, "release");
await mkdir(staging);
const problems: string[] = [];
const packages: TrainPackage[] = [];
for (const entry of PUBLIC_PACKAGES) {
const report = JSON.parse(
await run(
"pnpm",
[`--config.pnpmfile=${PACK_HOOKS}`, "pack", "--json", "--pack-destination", staging],
path.join(repositoryRoot, entry.directory),
),
) as { filename: string };
const file = path.basename(report.filename);
if (file !== tarballName(entry.name, version)) {
problems.push(`${entry.name} packed as ${file}.`);
continue;
}
problems.push(...(await inspectTarball(entry, version, report.filename, scratch)));
const data = await readFile(report.filename);
packages.push({
file,
name: entry.name,
role: entry.role,
size: data.length,
version,
...artifactDigests(data),
});
}
const manifest: PackageTrainManifest = {
packageTrain: PACKAGE_TRAIN_FORMAT,
packages,
source: { modified, revision },
version,
};
const contents = new Map<string, Uint8Array>();
for (const file of await readdir(staging)) {
contents.set(file, await readFile(path.join(staging, file)));
}
problems.push(...releaseDirectoryProblems(manifest, contents));
if (problems.length > 0) throw new PackageReleaseError(problems);
const sorted = [...manifest.packages].sort((left, right) => (left.file < right.file ? -1 : 1));
await writeFile(
path.join(staging, "SHA256SUMS"),
sorted.map((entry) => `${entry.sha256} ${entry.file}\n`).join(""),
);
await writeFile(path.join(staging, PACKAGE_TRAIN_MANIFEST), serializeManifest(manifest));
await mkdir(path.dirname(release), { recursive: true });
// Into place in one step: the release directory either holds a whole, checked release or does
// not exist. A rename across file systems is not atomic, so copy then, never half a release.
try {
await rename(staging, release);
} catch {
await run("cp", ["-R", staging, release], repositoryRoot);
}
return { directory: release, manifest };
} finally {
await rm(scratch, { force: true, recursive: true });
}
}
async function main(argv: readonly string[]): Promise<number> {
const allowModified = argv.includes("--allow-modified");
const rest = argv.filter((argument) => argument !== "--allow-modified");
if (rest.length !== 2 || rest[0] !== "--out" || !rest[1]) {
process.stderr.write(
"Usage: node scripts/package-release.ts --out <directory> [--allow-modified]\n",
);
return 64;
}
try {
const { directory, manifest } = await buildPackageRelease({
allowModified,
outDirectory: rest[1],
});
for (const entry of manifest.packages) {
process.stdout.write(`${entry.file}: ${entry.size} bytes, sha256 ${entry.sha256}\n`);
}
process.stdout.write(
`Built and inspected the ${manifest.version} package train (${manifest.packages.length} packages${
manifest.source.modified ? ", from a modified source tree: never publish it" : ""
}) in ${directory}\n`,
);
return 0;
} catch (error) {
if (error instanceof PackageReleaseError) {
for (const problem of error.problems) process.stderr.write(`${problem}\n`);
return 1;
}
process.stderr.write("Building the package release failed while building or packing.\n");
return 1;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await main(process.argv.slice(2));
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# The package release gate (ADR 0053): what must pass before a future step may publish a package
# train. It publishes nothing.
#
# bash scripts/test-package-release.sh [--allow-modified]
#
# 1. Builds the public package train from this checkout (`scripts/package-release.ts`): clean build
# output, pack, inspect, release manifest.
# 2. Builds it again from a separate worktree of the same commit, at another absolute path, and
# requires the two release directories to be byte-identical: every tarball, SHA256SUMS, and the
# manifest.
# 3. Installs the release into the standalone fixture and proves it builds, packages, and verifies
# (`scripts/test-standalone-module.sh`).
#
# `--allow-modified` is for local development on a modified source tree. The second build is then
# made in this checkout again, because a worktree of the commit would not hold the modifications.
set -euo pipefail
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
allow_modified=()
if [[ "${1:-}" == "--allow-modified" ]]; then
allow_modified=(--allow-modified)
elif (($#)); then
echo "usage: test-package-release.sh [--allow-modified]" >&2
exit 64
fi
work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-package-release-XXXXXX")"
worktree="$work/second-checkout"
cleanup() {
if [[ -d "$worktree" ]]; then
git -C "$repository_root" worktree remove --force "$worktree" >/dev/null 2>&1 || true
fi
rm -rf "$work"
}
trap cleanup EXIT
echo "==> Building the package train"
node "$repository_root/scripts/package-release.ts" --out "$work/first" "${allow_modified[@]}"
release="$(find "$work/first" -mindepth 1 -maxdepth 1 -type d)"
echo "==> Building it again, to prove the artifacts are reproducible"
if ((${#allow_modified[@]})); then
node "$repository_root/scripts/package-release.ts" --out "$work/second" "${allow_modified[@]}" >/dev/null
else
git -C "$repository_root" worktree add --detach --quiet "$worktree" HEAD
(cd "$worktree" && pnpm install --offline --frozen-lockfile --reporter=silent)
node "$worktree/scripts/package-release.ts" --out "$work/second" >/dev/null
fi
if ! diff -r "$release" "$(find "$work/second" -mindepth 1 -maxdepth 1 -type d)"; then
echo "package release gate failed: two builds of the same commit produced different artifacts" >&2
exit 1
fi
echo " byte-identical: $(ls "$release" | wc -l) files"
echo "==> Consuming the release from a standalone module"
bash "$repository_root/scripts/test-standalone-module.sh" --release "$release" "${allow_modified[@]}"
echo "Package release gate passed."
+142 -39
View File
@@ -1,77 +1,170 @@
#!/usr/bin/env bash
# The standalone module-development proof (ADR 0053): a module outside this repository, installed
# from locally packed @ekklesia/* tarballs, with no workspace link and no Ekklesia registry, must
# install, typecheck, build its backend and UI, package with the canonical packager, and verify with
# the platform's own verifier.
# The standalone consumer proof (ADR 0053): a module outside this repository consumes one package
# release train, the exact tarballs `scripts/package-release.ts` produced, and nothing else.
#
# bash scripts/test-standalone-module.sh
# bash scripts/test-standalone-module.sh --release <release directory> [--allow-modified]
#
# Third-party packages (React, TypeScript, esbuild, ...) come from the public npm registry as usual.
# Nothing is published, and nothing outside a temporary directory is written except the packed
# public packages' rebuilt dist/ output.
# The release directory is checked against its manifest first; then every @ekklesia/* package, the
# module's own and the ones they depend on, installs from those tarballs, with no workspace link, no
# Ekklesia source checkout, and no registry: third-party packages come from the local pnpm store the
# workspace install already filled (`--offline`). The module then installs again from its lockfile
# alone, type-checks every declaration it reaches (`skipLibCheck: false`), builds its backend and
# UI, packages with the release's own `@ekklesia/module-pack`, and verifies with Core's verifier.
#
# `--allow-modified` accepts a release built from a modified source tree, for local development.
set -euo pipefail
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-standalone-XXXXXX")"
trap 'rm -rf "$work"' EXIT
fail() {
echo "standalone module proof failed: $*" >&2
echo "standalone consumer proof failed: $*" >&2
exit 1
}
version="$(node -p 'require(process.argv[1]).version' "$repository_root/package.json")"
release="" allow_modified=""
while (($#)); do
case "$1" in
--release)
release="${2:-}"
shift 2 || fail "--release needs a directory"
;;
--allow-modified)
allow_modified=1
shift
;;
*) fail "usage: test-standalone-module.sh --release <release directory> [--allow-modified]" ;;
esac
done
[[ -n "$release" && -d "$release" ]] || fail "no release directory given"
echo "==> Packing and inspecting the public package set ($version)"
node "$repository_root/scripts/pack-public-packages.ts" --out "$work/tarballs"
work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-standalone-XXXXXX")"
trap 'rm -rf "$work"' EXIT
# The module installs from a copy beside it, named by a fixed relative path. pnpm names a file:
# tarball's install directory after that path, and the bundler keeps those names in its comments, so
# an absolute path would make the module package depend on where the release happened to sit. (From
# a registry, the directory is simply `<name>@<version>`.) The copy is what is checked and installed.
cp -R "$release" "$work/release"
release="$work/release"
echo "==> Checking the release against its manifest"
# Prints the train version, then one `name<TAB>file<TAB>integrity` line per package.
node --input-type=module - "$repository_root" "$release" "$allow_modified" >"$work/train.tsv" <<'EOF'
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [repositoryRoot, release, allowModified] = process.argv.slice(2);
const { PACKAGE_TRAIN_MANIFEST, releaseDirectoryProblems } = await import(
pathToFileURL(path.join(repositoryRoot, "scripts", "package-release.ts")).href
);
const manifest = JSON.parse(readFileSync(path.join(release, PACKAGE_TRAIN_MANIFEST), "utf8"));
const files = new Map(readdirSync(release).map((file) => [file, readFileSync(path.join(release, file))]));
const problems = releaseDirectoryProblems(manifest, files);
if (manifest.source?.modified !== false && allowModified !== "1") {
problems.push("the release was built from a modified source tree.");
}
if (problems.length > 0) {
console.error(problems.join("\n"));
process.exit(1);
}
console.log(manifest.version);
for (const entry of manifest.packages) console.log(`${entry.name}\t${entry.file}\t${entry.integrity}`);
EOF
version="$(head -1 "$work/train.tsv")"
echo " package train $version, $(($(wc -l <"$work/train.tsv") - 1)) packages"
module="$work/standalone-proof"
cp -R "$repository_root/tests/fixtures/standalone-module" "$module"
cp "$repository_root/LICENSE" "$module/LICENSE"
# The fixture pins the release's module-pack exactly, as every module repository must.
pinned="$(node -p 'require(process.argv[1]).devDependencies["@ekklesia/module-pack"]' "$module/package.json")"
[[ "$pinned" == "$version" ]] ||
fail "the fixture pins @ekklesia/module-pack $pinned, not the release's $version"
# A module repository's own pnpm settings: esbuild (used by the UI build and the packager) runs its
# install script. And, for this proof only, every @ekklesia/* package, the module's own and the ones
# they depend on, resolves to its packed tarball, exactly as it would to a published version.
# install script. And, for this proof only, every @ekklesia/* package resolves to its release
# tarball, exactly as it would to the published version.
{
echo "allowBuilds:"
echo " esbuild: true"
echo "overrides:"
for tarball in "$work"/tarballs/*.tgz; do
name="$(tar -xzOf "$tarball" package/package.json | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).name')"
echo " \"$name\": \"file:$tarball\""
tail -n +2 "$work/train.tsv" | while IFS=$'\t' read -r name file _; do
echo " \"$name\": \"file:../release/$file\""
done
} >"$module/pnpm-workspace.yaml"
echo "==> Installing the module from the tarballs"
(cd "$module" && pnpm install --reporter=append-only >"$work/install.log" 2>&1) ||
{ tail -40 "$work/install.log" >&2; fail "install"; }
echo "==> Installing the module from the release tarballs, offline"
(cd "$module" && pnpm install --offline --reporter=append-only >"$work/install.log" 2>&1) ||
{
tail -40 "$work/install.log" >&2
fail "install (third-party packages come from the local store: run pnpm install in the workspace first)"
}
echo "==> Checking the lockfile records each release artifact's own integrity"
node --input-type=module - "$module/pnpm-lock.yaml" "$work/train.tsv" <<'EOF'
import { readFileSync } from "node:fs";
const [lockfile, train] = process.argv.slice(2);
const lock = readFileSync(lockfile, "utf8");
const failures = [];
for (const line of readFileSync(train, "utf8").trim().split("\n").slice(1)) {
const [name, file, integrity] = line.split("\t");
// pnpm records a file: tarball under `<name>@file:<path>` with its computed integrity.
const at = lock.indexOf(`\n '${name}@file:`);
const entry = at === -1 ? "" : lock.slice(at, lock.indexOf("\n\n", at));
if (!entry.includes(file)) failures.push(`${name} is not installed from ${file}`);
else if (!entry.includes(`integrity: ${integrity}`)) failures.push(`${name}'s integrity differs from the release manifest`);
}
if (failures.length > 0) {
console.error(failures.join("\n"));
process.exit(1);
}
EOF
echo "==> Reinstalling from the lockfile alone (--frozen-lockfile)"
rm -rf "$module/node_modules"
(cd "$module" && pnpm install --offline --frozen-lockfile --reporter=append-only >"$work/frozen.log" 2>&1) ||
{ tail -40 "$work/frozen.log" >&2; fail "frozen-lockfile install"; }
echo "==> Checking what the install resolved"
(cd "$module" && node --input-type=module - "$module" <<'EOF')
import { readdirSync, realpathSync } from "node:fs";
(cd "$module" && node --input-type=module - "$module" "$version" <<'EOF')
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
const [moduleDirectory] = process.argv.slice(2);
const store = readdirSync(path.join(moduleDirectory, "node_modules", ".pnpm"));
const [moduleDirectory, version] = process.argv.slice(2);
const modules = path.join(moduleDirectory, "node_modules");
const store = readdirSync(path.join(modules, ".pnpm"));
const failures = [];
// The Module SDK and its dependencies never pull Drizzle into a module that does not use it.
// The Module SDK and its dependencies never pull Drizzle into a module that does not ask for it.
if (store.some((entry) => entry.startsWith("drizzle-orm@"))) failures.push("drizzle-orm is installed");
// One React, the module's own, shared by @ekklesia/ui.
const reacts = store.filter((entry) => /^react@/u.test(entry));
if (reacts.length !== 1) failures.push(`${reacts.length} copies of react are installed`);
const fromModule = createRequire(path.join(moduleDirectory, "package.json"));
const ui = realpathSync(path.join(moduleDirectory, "node_modules", "@ekklesia", "ui", "package.json"));
const fromUi = createRequire(ui);
if (realpathSync(fromModule.resolve("react")) !== realpathSync(fromUi.resolve("react"))) {
const ui = realpathSync(path.join(modules, "@ekklesia", "ui", "package.json"));
if (realpathSync(fromModule.resolve("react")) !== realpathSync(createRequire(ui).resolve("react"))) {
failures.push("@ekklesia/ui resolves a different react");
}
// Every Ekklesia package came from a tarball into the module's own store, never from a link.
for (const name of readdirSync(path.join(moduleDirectory, "node_modules", "@ekklesia"))) {
const real = realpathSync(path.join(moduleDirectory, "node_modules", "@ekklesia", name));
if (!real.startsWith(path.join(moduleDirectory, "node_modules", ".pnpm") + path.sep)) {
// @ekklesia/contracts is installed only because the packages above depend on it: never a direct
// dependency, and never reachable by name from the module's own code.
const own = JSON.parse(readFileSync(path.join(moduleDirectory, "package.json"), "utf8"));
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
if (own[field]?.["@ekklesia/contracts"] !== undefined) failures.push(`the module lists contracts in ${field}`);
}
if (existsSync(path.join(modules, "@ekklesia", "contracts"))) failures.push("contracts is a direct install");
if (!store.some((entry) => entry.startsWith("@ekklesia+contracts@"))) failures.push("contracts is not installed");
// Every Ekklesia package came from a tarball into the module's own store, never from a link, and
// each one, module-pack included, is exactly the release's version.
for (const name of readdirSync(path.join(modules, "@ekklesia"))) {
const real = realpathSync(path.join(modules, "@ekklesia", name));
if (!real.startsWith(path.join(modules, ".pnpm") + path.sep)) {
failures.push(`@ekklesia/${name} is linked from outside the module`);
}
const installed = JSON.parse(readFileSync(path.join(real, "package.json"), "utf8"));
if (installed.version !== version) failures.push(`@ekklesia/${name} is ${installed.version}`);
}
if (failures.length > 0) {
console.error(failures.join("\n"));
@@ -95,7 +188,7 @@ echo "==> Typechecking (skipLibCheck: false) and building"
# binary is run directly, because pnpm would re-resolve the module without the proof's overrides.
rm "$module/pnpm-workspace.yaml"
echo "==> Packaging with the canonical packager"
echo "==> Packaging with the release's @ekklesia/module-pack"
(cd "$module" && ./node_modules/.bin/ekklesia-pack-module --module . --target any --out "$work/out" \
--license-file LICENSE --source-revision 86ae907f59458508e3277666cdd214f5bd7c1078)
archive="$work/out/standalone-proof-0.1.0-any.tar.gz"
@@ -105,11 +198,11 @@ echo "==> Verifying with the platform verifier"
--sha256-file "$archive.sha256" --extract-to "$work/verified")
echo "==> Checking the package's own description"
node --input-type=module - "$work/verified" "$version" "$repository_root" "$work" <<'EOF'
node --input-type=module - "$work/verified" "$version" "$repository_root" "$work" "$release" <<'EOF'
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
const [verified, version, repositoryRoot, work] = process.argv.slice(2);
const [verified, version, repositoryRoot, work, release] = process.argv.slice(2);
const read = (file) => readFileSync(path.join(verified, file), "utf8");
const metadata = JSON.parse(read("ekklesia.package.json"));
const failures = [];
@@ -117,6 +210,10 @@ const incorporates = metadata.incorporates ?? {};
for (const name of ["@ekklesia/contracts", "@ekklesia/module-database", "@ekklesia/module-sdk"]) {
if (incorporates[name] !== version) failures.push(`incorporates ${name}@${incorporates[name]}`);
}
const packager = metadata.provenance?.packager ?? {};
if (packager.name !== "@ekklesia/module-pack" || packager.version !== version) {
failures.push(`packed by ${packager.name}@${packager.version}, not the release's module-pack`);
}
if (metadata.runtime?.node !== ">=24.11.0 <25") failures.push("runtime.node is not engines.node");
if (metadata.transitional !== undefined) failures.push("records transitional database facts");
const notices = read("THIRD_PARTY_NOTICES");
@@ -127,8 +224,14 @@ if (reacts.length !== 1) failures.push(`credits ${reacts.length} copies of react
for (const entry of readdirSync(verified, { recursive: true, withFileTypes: true })) {
if (!entry.isFile()) continue;
const text = readFileSync(path.join(entry.parentPath, entry.name), "latin1");
if (text.includes(repositoryRoot) || text.includes(work)) failures.push(`${entry.name} names a local path`);
for (const local of [repositoryRoot, work, release]) {
if (text.includes(local)) failures.push(`${entry.name} names a local path`);
}
if (/drizzle-orm/u.test(text) && entry.name.endsWith(".js")) failures.push(`${entry.name} bundles Drizzle`);
// A file: install directory's name keeps the path pnpm installed it from: only the fixed one.
for (const match of text.matchAll(/@file\+([^/"'\s]*)/gu)) {
if (!match[1]?.startsWith("..+release+")) failures.push(`${entry.name} names an install path`);
}
}
if (failures.length > 0) {
console.error(failures.join("\n"));
@@ -137,4 +240,4 @@ if (failures.length > 0) {
console.log(`incorporates ${JSON.stringify(incorporates)}`);
EOF
echo "Standalone module proof passed."
echo "Standalone consumer proof passed for package train $version."