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
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:
co-authored by
Claude Opus 5.5
parent
7ea2d7883f
commit
9debe4e891
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user