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]>
254 lines
14 KiB
Markdown
254 lines
14 KiB
Markdown
# 0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-10-07
|
|
- Extends: [0049](0049-module-package-v1.md). This is the step ADR 0049 deferred: "publishing the
|
|
Ekklesia SDK packages for module builds outside a platform workspace". A registry is used only to
|
|
build a module. Installing one stays registry-free: an installed package is self-contained, as ADR
|
|
0049 decided.
|
|
- Related: [ADR 0002](0002-pnpm-workspace-and-typescript.md),
|
|
[ADR 0006](0006-versioned-capabilities-and-compiled-module-runtime.md),
|
|
[ADR 0021](0021-modules-as-hostable-products.md),
|
|
[ADR 0023](0023-module-ui-build-pipeline.md),
|
|
[ADR 0039](0039-shared-design-tokens-and-elements.md),
|
|
[ADR 0047](0047-module-ci-harness.md),
|
|
[ADR 0048](0048-generic-image-and-installed-module-storage.md),
|
|
[ADR 0050](0050-module-database-abi-v1.md),
|
|
[ADR 0051](0051-module-package-license-material.md),
|
|
[ADR 0052](0052-reviewed-license-supplements.md)
|
|
|
|
## Context
|
|
|
|
Modules are independently versioned products in their own repositories (ADR 0021). Yet no module can
|
|
be built outside Ekklesia's monorepo today. Signage's continuous integration clones the platform at
|
|
a pinned commit, places Signage under `modules/signage`, and installs and builds inside that
|
|
workspace, because its Ekklesia dependencies exist only as `workspace:*` packages.
|
|
|
|
An investigation of the current packages found the following.
|
|
|
|
- **One coordinated version.** Every `@ekklesia/*` package, and the platform, is version 0.1.2. The
|
|
release process already bumps them together. The platform reports its Module SDK version, which
|
|
manifests' `compatibility.moduleSdk` ranges are checked against (ADR 0006), by reading
|
|
`@ekklesia/module-sdk`'s own package version.
|
|
- **The tarballs already stand alone.** `pnpm pack` rewrites `workspace:*` ranges to exact versions,
|
|
and each tarball carries only its built `dist`, declarations, and `LICENSE`. But every package's
|
|
`prepack` runs a command at the monorepo root, and `@ekklesia/typescript-config` is marked
|
|
`private`.
|
|
- **The SDK exposes everything.** `@ekklesia/module-sdk` re-exports the whole `@ekklesia/contracts`
|
|
barrel. That includes contracts no module needs, such as authorization and identity, and the
|
|
legacy Drizzle `ModuleDatabase` type. `@ekklesia/contracts` depends on `drizzle-orm` for that type,
|
|
so every module install pulls Drizzle in.
|
|
- Neither the module template nor Signage imports `@ekklesia/contracts` directly.
|
|
- **Frameworks are declared twice.** `@ekklesia/ui` lists `react`, and `@ekklesia/module-sdk` lists
|
|
`drizzle-orm`, as both a dependency and a peer dependency. That invites a second copy of a runtime
|
|
that must be single within one module bundle.
|
|
- **The canonical packager assumes the monorepo**, in two ways:
|
|
- it finds the module's Node.js range only through an enclosing `pnpm-workspace.yaml`;
|
|
- it decides that an Ekklesia package is the platform's own, not third-party, because its real
|
|
path does not contain `node_modules`.
|
|
|
|
An `@ekklesia/*` package installed from a registry would therefore be treated as third-party.
|
|
Packaging would omit it from `incorporates` and skip the Module SDK compatibility check.
|
|
- **The registry and its hosts.**
|
|
- The current private host is a Gitea 1.27.2 server. Its npm registry is enabled for the
|
|
organization, and that endpoint and the repositories are currently readable anonymously.
|
|
- pnpm leaves a tarball URL out of its lockfile only when the URL has the canonical
|
|
`<registry>/<name>/-/<name>-<version>.tgz` form. Gitea advertises another form, so lockfiles
|
|
produced against Gitea name the Gitea host.
|
|
- The integrity hashes in those lockfiles are independent of the host.
|
|
|
|
## Decision
|
|
|
|
### The npm registry protocol, not a provider
|
|
|
|
Ekklesia's module-development packages are distributed over the **standard npm registry protocol**,
|
|
and modules install them with ordinary pnpm or npm commands.
|
|
|
|
- **No provider is part of the architecture.** Not Gitea, GitHub, npmjs, or any other. A module
|
|
repository depends on the protocol and on a scope-to-registry mapping in normal npm/pnpm
|
|
configuration.
|
|
- **Gitea may host the private phase.** It is a host for this phase, not a permanent decision.
|
|
- **No host-neutral lockfile or Ekklesia proxy is required.** Moving to another registry may need a
|
|
one-time change of a module's registry configuration and a regeneration of its lockfile. The
|
|
integrity hashes keep verifying the package content across such a move.
|
|
|
|
A module repository's registry configuration is, for example:
|
|
|
|
```ini
|
|
# .npmrc, committed: where the @ekklesia scope lives, and nothing secret.
|
|
@ekklesia:registry=https://<registry endpoint>/
|
|
```
|
|
|
|
```ini
|
|
# User or CI configuration, never committed: the credential, from the environment.
|
|
//<registry endpoint>/:_authToken=${EKKLESIA_PACKAGES_TOKEN}
|
|
```
|
|
|
|
### The public package set
|
|
|
|
Only packages a module author needs are published. Nothing is published merely because it exists in
|
|
the workspace.
|
|
|
|
| Category | Packages |
|
|
| --- | --- |
|
|
| Author API | `@ekklesia/module-sdk`, `@ekklesia/module-database`, `@ekklesia/module-ui-sdk`, `@ekklesia/design-tokens` |
|
|
| Tooling | `@ekklesia/module-ui-build`, `@ekklesia/module-pack`, `@ekklesia/typescript-config` |
|
|
| Pre-1.0 component API | `@ekklesia/ui`: publishable, explicitly unstable before 1.0 |
|
|
| Implementation dependency only | `@ekklesia/contracts`: may be published because the packages above depend on it, but it is **not** a supported direct module-author API |
|
|
| Never published | `@ekklesia/core`, the applications, `@ekklesia/module-ui-host`, `@ekklesia/module-template`, and every other platform internal |
|
|
|
|
### A curated SDK surface
|
|
|
|
Before the first external publication, the author-facing SDK is narrowed.
|
|
|
|
- **A curated API.** `@ekklesia/module-sdk` exposes what a module author needs, and no longer
|
|
re-exports the complete `@ekklesia/contracts` barrel.
|
|
- **No forced Drizzle.** The stable SDK root does not make Drizzle a dependency of modules that do
|
|
not use it.
|
|
- **No legacy database contract.** The legacy Drizzle database contract (ADR 0016's facade type) is
|
|
not part of the standalone author-facing surface. Core still serves the legacy facade for its
|
|
deprecation window (ADR 0050). This decision only keeps its type out of what new standalone
|
|
modules build against.
|
|
- **Drizzle support lives in one place.** Drizzle-specific helpers belong to the
|
|
`@ekklesia/module-database` Drizzle adapter (ADR 0050).
|
|
|
|
### Dependency rules
|
|
|
|
These rules apply to the published packages:
|
|
|
|
- **React and ReactDOM** are peer dependencies of the public UI packages, plus development
|
|
dependencies for those packages' own builds and tests. This does not change ADR 0023: a module UI
|
|
artifact still bundles React. The peer only ensures there is one React in that bundle, the
|
|
module's own.
|
|
- **Drizzle** is an optional peer dependency only where the Drizzle adapter needs it.
|
|
- **Real implementation dependencies,** at run time or build time, remain normal dependencies.
|
|
|
|
These rules apply to a module repository:
|
|
|
|
- **Runtime and module-code packages** go in `dependencies`: the Author API packages, `@ekklesia/ui`,
|
|
React, and the module's ORM if it uses one.
|
|
- **Build tooling and configuration** go in `devDependencies`: `@ekklesia/module-ui-build`,
|
|
`@ekklesia/module-pack`, `@ekklesia/typescript-config`, and type packages.
|
|
|
|
### Versioning
|
|
|
|
- **One coordinated version during 0.x.** Every public Ekklesia module-development package uses one
|
|
release version, equal to the platform release version. The version the platform reports for
|
|
`moduleSdk` compatibility is therefore the version of the packages a module was built with.
|
|
- **No mixed trains.** Within a published train, Ekklesia-owned dependencies name the coordinated,
|
|
tested version, never a version from another train.
|
|
- **Ranges in source, exact artifacts in lockfiles.** A module may declare compatible ranges in its
|
|
source `package.json`; its lockfile pins exact artifacts with their integrity hashes.
|
|
- **`@ekklesia/module-pack` is pinned exactly.** A module repository pins it in its source
|
|
`package.json`, not only in its lockfile, because its version affects the package it produces.
|
|
- **Independent package versions are deferred.**
|
|
|
|
### Standalone packaging
|
|
|
|
- **No monorepo checkout.** A standalone module builds and packages from its own repository and its
|
|
installed dependencies.
|
|
- **Its own Node.js range.** The module declares its own Node.js compatibility, preferably in
|
|
`engines.node`, and the canonical packager reads it from there or from an explicit option. It
|
|
never requires an enclosing `pnpm-workspace.yaml`.
|
|
- **Ekklesia packages recognized by identity.** The canonical packager recognizes Ekklesia-owned
|
|
installed packages by explicit package identity and metadata, never by workspace path. So
|
|
`incorporates` and the Module SDK compatibility check (ADR 0049) work the same whether a package
|
|
came from the monorepo or from a registry. Ekklesia packages are never reported as third-party
|
|
software (ADR 0051).
|
|
- **No monorepo-root commands at install time.** Publishing workflows for the public packages run
|
|
their build in the platform's release pipeline. Nothing in a published package runs a
|
|
monorepo-root command when a consumer installs or packs it.
|
|
|
|
### Supply chain
|
|
|
|
- **Immutable versions.** A version is never republished with other content, and never withdrawn to
|
|
be reused.
|
|
- **Lockfiles.** pnpm lockfile integrity is part of the reproducibility model: modules install with a
|
|
frozen lockfile.
|
|
- **Release CI proves the tarballs before publication.** It must:
|
|
1. build and pack the public package set;
|
|
2. inspect every tarball: only the intended contents, a `LICENSE`, and no `workspace:` range;
|
|
3. install those tarballs into a clean standalone module fixture, outside any monorepo;
|
|
4. prove there that the fixture builds, packages with the canonical packager, and verifies.
|
|
|
|
Only then are the versions published.
|
|
- **Signing and provenance are deferred.**
|
|
|
|
### Registry access and the private phase
|
|
|
|
- **No secrets in module repositories.** Registry configuration for the scope is normal npm/pnpm
|
|
configuration.
|
|
- **Credentials from the environment.** Read and publish credentials come from environment or user
|
|
configuration: a read credential in module CI where reads need one, and a publish credential only
|
|
in Ekklesia's release pipeline.
|
|
- **No permanent provider.**
|
|
- **Verify access before the first publication.** Check the chosen registry's actual private and
|
|
read-access behaviour first. Ekklesia's packages must not be published unintentionally to a
|
|
registry that serves them anonymously while Ekklesia is not public.
|
|
|
|
### The release workflow
|
|
|
|
During 0.x, public package releases form a **coordinated SDK train** associated with platform
|
|
releases:
|
|
|
|
1. build;
|
|
2. pack;
|
|
3. standalone tarball smoke (see [Supply chain](#supply-chain));
|
|
4. publish immutable versions;
|
|
5. downstream module CI installs and tests those versions.
|
|
|
|
### Signage
|
|
|
|
The intended result is a normal standalone Signage repository:
|
|
- versioned `@ekklesia/*` dependencies;
|
|
- its own lockfile;
|
|
- continuous integration that installs, builds, packages, and verifies on its own.
|
|
|
|
That CI must not clone Ekklesia or place Signage under `modules/`. The migration itself is a separate
|
|
change.
|
|
|
|
## Consequences
|
|
|
|
- A module repository is an ordinary pnpm project. Clone it, install, build, and package it with the
|
|
canonical packager, with no Ekklesia checkout.
|
|
- Module authors see a smaller, deliberate API, and the platform can change its internal contracts
|
|
without breaking them.
|
|
- One coordinated version keeps compatibility simple: SDK version X is what platform version X
|
|
reports and accepts. The cost is republishing unchanged packages with each platform release.
|
|
- A registry move costs each module a configuration change and a lockfile regeneration, but no
|
|
source change. Package content stays verifiable by integrity hash throughout.
|
|
- Before the first publication, the SDK surface must be curated and the packager's two monorepo
|
|
assumptions removed. Both are breaking for code that relied on them.
|
|
- ADR 0047, still proposed, assembles a module into the platform workspace to test it. Before it is
|
|
accepted it must instead consume published packages and canonical module packages, consistent with
|
|
this decision and with ADR 0048.
|
|
|
|
## Alternatives
|
|
|
|
- **Git URL, tag, or commit dependencies.** Rejected: they tie modules to one Git host, need a build
|
|
on every install because `dist` is not committed, and cannot express version ranges.
|
|
- **Release-asset tarball URLs.** Rejected: each dependency becomes a host-specific URL, and ranges
|
|
cannot be resolved.
|
|
- **OCI artifacts for packages.** Rejected: npm and pnpm cannot install from them. (A harness image
|
|
is a different matter; see ADR 0047.)
|
|
- **Vendoring Ekklesia packages into each module repository.** Rejected: the copies drift, updates
|
|
are manual, and there would be no single canonical SDK.
|
|
- **Requiring host-neutral lockfiles, or an Ekklesia registry proxy that rewrites tarball URLs.** Not
|
|
required: a one-time lockfile regeneration on a registry move is an acceptable cost, and integrity
|
|
hashes already keep content verifiable. Ekklesia may still choose a stable endpoint later.
|
|
- **Publishing `@ekklesia/contracts` as a direct author API.** Rejected: it holds platform-wide
|
|
contracts. Modules get a curated surface through the SDK packages.
|
|
- **Externalizing React from module UI bundles to share one runtime.** Rejected again: ADR 0023's
|
|
reasoning stands, and peers only prevent duplicates within one bundle.
|
|
- **Independent versions per package now.** Deferred: during 0.x a coordinated train is simpler for
|
|
module authors and matches how the platform reports its SDK version.
|
|
|
|
## Deferred
|
|
|
|
- Provisioning a registry, and its credentials.
|
|
- Public publication on npmjs or any other public registry.
|
|
- Package signing and provenance.
|
|
- Independent package versioning.
|
|
- Dashboard and UI redesign.
|
|
- The Signage migration itself.
|