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]>
188 lines
10 KiB
Markdown
188 lines
10 KiB
Markdown
# 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](../architecture/decisions/0053-standalone-module-development-packages.md)): 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
|
|
|
|
```text
|
|
version set ─▶ build ─▶ test ─▶ pack the public package train ─▶ standalone consumer proof
|
|
│
|
|
tag and release (maintainer) ◀─ [future: publish] ◀──┘
|
|
```
|
|
|
|
1. **Version set.** A `chore(release): prepare X` pull request sets the release version: the root
|
|
`package.json`, every platform workspace package (the module template versions as a module, not
|
|
with the platform), the image build example in `deploy/README.md`, and the standalone fixture's
|
|
exact `@ekklesia/module-pack` pin (`tests/fixtures/standalone-module/package.json`). The tag
|
|
must point at a commit that carries the version it names.
|
|
2. **Build and test.** `pnpm verify`, the integration suite, and the deploy smoke, as for any change.
|
|
3. **Pack the public package train.** `pnpm packages:release` (below).
|
|
4. **Standalone consumer proof.** A module outside the repository consumes exactly those
|
|
artifacts (below).
|
|
5. **[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.
|
|
6. **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
|
|
|
|
```bash
|
|
pnpm packages:release # writes .package-release/ekklesia-packages-<version>/
|
|
```
|
|
|
|
`scripts/package-release.ts`:
|
|
|
|
1. **Refuses a modified source tree.** A release is built from a commit. `--allow-modified` exists
|
|
for local development only; its manifest then says `"modified": true`, and such a release must
|
|
never be published.
|
|
2. **Checks the release set and versions** in the workspace.
|
|
3. **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.
|
|
4. **Packs each package with `pnpm pack`.** Pack-only pnpm hooks (`scripts/lib/pack-hooks.cjs`)
|
|
apply, never the workspace's install settings ([Reproducibility](#reproducibility)).
|
|
5. **Inspects every tarball.** It checks:
|
|
- the expected name, version, and file name;
|
|
- a `LICENSE`, and only the `files` the manifest declares, each built file with the source it
|
|
came from, and no source, test, or configuration file;
|
|
- no `workspace:` range, no `devDependencies`, no `publishConfig`, 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.
|
|
6. **Checks the set.** The release must hold exactly the expected tarballs: none missing, none extra.
|
|
7. **Writes the release directory only when everything passed,** in one step. It refuses to
|
|
overwrite one that exists.
|
|
|
|
```text
|
|
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.
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
pnpm test:package-release # scripts/test-package-release.sh
|
|
```
|
|
|
|
1. **Builds the train** from this checkout.
|
|
2. **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.
|
|
3. **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 one `react`, which
|
|
`@ekklesia/ui` resolves to as well. `@ekklesia/contracts` is installed only transitively: it is
|
|
not in the module's `package.json` and not directly under `node_modules/@ekklesia`. Every
|
|
installed `@ekklesia/*` package, `module-pack` included, is exactly the release version, and
|
|
the fixture pins `module-pack` exactly.
|
|
- **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 one `workspace:`
|
|
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`'s
|
|
`package.json` ordered `@ekklesia/module-sdk` and `@ekklesia/module-database` differently.
|
|
- **The fix is pnpm's own hook.** The pack-only `beforePacking` hook puts each dependency map back
|
|
in the source `package.json`'s key order. It changes no name, range, or field. The tarball stays
|
|
an ordinary `pnpm pack` tarball.
|
|
- **The inspection enforces it.** It fails any packed map whose order differs from its source.
|
|
- **The hook also drops `devDependencies`** from 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 from
|
|
`packageManager`. 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.
|