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]>
10 KiB
Releases and the public package train
An Ekklesia platform release is one version. The platform image, the private workspace packages,
and the public @ekklesia/* module-development packages all carry it. This page covers the order a
release happens in and the public package train
(ADR 0053): how its
artifacts are produced, what proves them, and what a future publication step will do with them.
Registry publication is not implemented. Nothing in this repository publishes a package, and no CI job holds a registry credential. The release artifacts below are produced and proven on every CI run. A future publication step publishes exactly these artifacts, unchanged, and nothing else.
Where each step sits
version set ─▶ build ─▶ test ─▶ pack the public package train ─▶ standalone consumer proof
│
tag and release (maintainer) ◀─ [future: publish] ◀──┘
- Version set. A
chore(release): prepare Xpull request sets the release version: the rootpackage.json, every platform workspace package (the module template versions as a module, not with the platform), the image build example indeploy/README.md, and the standalone fixture's exact@ekklesia/module-packpin (tests/fixtures/standalone-module/package.json). The tag must point at a commit that carries the version it names. - Build and test.
pnpm verify, the integration suite, and the deploy smoke, as for any change. - Pack the public package train.
pnpm packages:release(below). - Standalone consumer proof. A module outside the repository consumes exactly those artifacts (below).
- [Future] publish. Not implemented. Once it is, it must run only after steps 3 and 4 passed for the same commit. It must check every artifact against the release manifest before publishing it, and publish nothing else.
- Tag and release. Unchanged: the maintainer tags the merge commit of the version-set pull request. Nothing here creates, moves, or requires a tag.
Steps 2 to 4 run in CI on every pull request and on main. The package-release job is the gate a
future publication job must depend on.
The public package train
| Package | Role |
|---|---|
@ekklesia/module-sdk, @ekklesia/module-database, @ekklesia/module-ui-sdk, @ekklesia/design-tokens |
author API |
@ekklesia/module-ui-build, @ekklesia/module-pack, @ekklesia/typescript-config |
tooling |
@ekklesia/ui |
publishable, pre-1.0 component API |
@ekklesia/contracts |
implementation dependency of the packages above: published only because they need it, never a supported direct dependency of a module |
The list lives in scripts/package-release.ts (PUBLIC_PACKAGES). Every other workspace package is
private, and the release builder refuses to run if a package outside the list is publishable or
carries the module-development distribution marker.
- One version. Every package in the train has the platform release version.
- One train. Every
@ekklesia/*dependency in a packed manifest names a package of the same train at exactly that version.workspace:ranges become those exact versions when packed. - No independent versions. A package cannot be released on its own.
Producing the release artifacts
pnpm packages:release # writes .package-release/ekklesia-packages-<version>/
scripts/package-release.ts:
- Refuses a modified source tree. A release is built from a commit.
--allow-modifiedexists for local development only; its manifest then says"modified": true, and such a release must never be published. - Checks the release set and versions in the workspace.
- Rebuilds from clean output. It removes each public package's
dist/and rebuilds without the build cache, so output from a deleted source file can never ship. - Packs each package with
pnpm pack. Pack-only pnpm hooks (scripts/lib/pack-hooks.cjs) apply, never the workspace's install settings (Reproducibility). - Inspects every tarball. It checks:
- the expected name, version, and file name;
- a
LICENSE, and only thefilesthe manifest declares, each built file with the source it came from, and no source, test, or configuration file; - no
workspace:range, nodevDependencies, nopublishConfig, and no install-time lifecycle script; - only public
@ekklesia/*dependencies, at the train's version; - the distribution marker;
- no local path (the checkout, the home directory, or the temporary directory) in any file.
- Checks the set. The release must hold exactly the expected tarballs: none missing, none extra.
- Writes the release directory only when everything passed, in one step. It refuses to overwrite one that exists.
ekklesia-packages-<version>/
├── ekklesia-contracts-<version>.tgz
├── ekklesia-design-tokens-<version>.tgz
├── … one .tgz per package in the train
├── SHA256SUMS `sha256sum -c SHA256SUMS` compatible
└── ekklesia-package-train.json the release manifest
The release manifest
ekklesia-package-train.json describes the train by content. It is provider-neutral: it names no
registry and is not a registry catalog.
{
"packageTrain": 1,
"version": "0.1.2",
"source": { "modified": false, "revision": "<full commit id>" },
"packages": [
{
"file": "ekklesia-contracts-0.1.2.tgz",
"integrity": "sha512-<base64>",
"name": "@ekklesia/contracts",
"role": "implementation",
"sha256": "<hex>",
"size": 87379,
"version": "0.1.2"
}
]
}
integrity is the npm dist.integrity form: a registry computes the same value from the same
bytes, and a module's lockfile records it. The packages are sorted by name, and the file's bytes
depend only on the commit.
Proving the artifacts
pnpm test:package-release # scripts/test-package-release.sh
- Builds the train from this checkout.
- Builds it again from a second worktree of the same commit at another absolute path, and
requires both release directories to be byte-identical: every tarball,
SHA256SUMS, and the manifest. - Runs the standalone consumer proof (
scripts/test-standalone-module.sh --release <dir>) on that release:- The release is checked first. Every artifact must match the manifest's size, SHA-256, and integrity, and the release must not be built from a modified tree.
- The fixture module installs from those tarballs alone. It is
tests/fixtures/standalone-module, copied outside the repository. Every@ekklesia/*package, direct or transitive, resolves to its release tarball. There is no workspace link and no Ekklesia source checkout. The install runs offline: third-party packages come from the local pnpm store, so no registry is involved at all. - The lockfile matches the release. It must record each artifact's own integrity. The module
then reinstalls from the lockfile alone with
--frozen-lockfile. - The install has the right shape. There is no
drizzle-orm, and exactly onereact, which@ekklesia/uiresolves to as well.@ekklesia/contractsis installed only transitively: it is not in the module'spackage.jsonand not directly undernode_modules/@ekklesia. Every installed@ekklesia/*package,module-packincluded, is exactly the release version, and the fixture pinsmodule-packexactly. - The module builds. It typechecks with
skipLibCheck: false, so every published declaration it reaches is checked, then builds its backend and UI. - It packages and verifies. It is packed with the release's own
ekklesia-pack-module, with no workspace file present. Core's verifier then verifies it. The package must:- incorporate the train's packages at the release version;
- name the release's module-pack as its packager;
- credit no Ekklesia package and exactly one React;
- contain no local path.
Reproducibility
Two builds of the same commit produce byte-identical tarballs, and CI proves it on every run.
- pnpm normalizes the tar entries. Every entry gets the fixed modification time 1985-10-26, owner 0, and a mode of 0644, or 0755 when executable. The gzip header records no time.
- The build output is deterministic, and source maps use relative paths.
- One nondeterminism came from pnpm itself. pnpm rewrites
workspace:ranges concurrently and inserts each dependency as its lookup resolves. A package with more than oneworkspace:dependency in one field could therefore be packed with those keys in a different order. Builds from two checkouts of one commit differed in exactly that way:@ekklesia/module-pack'spackage.jsonordered@ekklesia/module-sdkand@ekklesia/module-databasedifferently.- The fix is pnpm's own hook. The pack-only
beforePackinghook puts each dependency map back in the sourcepackage.json's key order. It changes no name, range, or field. The tarball stays an ordinarypnpm packtarball. - The inspection enforces it. It fails any packed map whose order differs from its source.
- The fix is pnpm's own hook. The pack-only
- The hook also drops
devDependenciesfrom packed manifests. A consumer never installs them, and a tooling package's test-only development dependencies named private workspace packages outside the train.
What reproducibility does not cover:
- The module package the consumer proof builds is deterministic only for a fixed install layout.
pnpm names a
file:tarball's install directory after its path, and bundlers keep those names in comments. So the proof always installs from the same relative path. From a registry, the directory is simply<name>@<version>. - Byte identity is proven for the pinned toolchain: Node.js from
.node-version, and pnpm frompackageManager. Another Node.js or zlib build may compress differently. Each artifact's integrity is recorded either way, and that, not byte identity, is what a publication step must verify.