feat: Ekklesia Module Package v1, the canonical packager and verifier (ADR 0049)
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m42s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Failing after 54s
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m42s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Failing after 54s
A separately versioned module is now distributed as one deterministic .tar.gz and its SHA-256, produced by an Ekklesia-owned packager and checked by Core before anything is installed. This slice implements the portable `any` target, with the module template as the end-to-end proof. ADR 0049 is accepted. @ekklesia/contracts: the Module Package v1 contract. The strict ekklesia.package.json schema (packageFormat 1, identity, target, runtime, provenance, incorporated Ekklesia packages, strict transitional facts, an exact byte-wise-sorted file list with size and SHA-256), target names, and the generated runtime package.json. @ekklesia/module-pack (new, tooling): `ekklesia-pack-module --module <dir> --target any --out <dir> --source-revision <commit>`. - Bundles server, schema, and jobs with esbuild, SDK, contracts, and Drizzle included; only Node built-ins remain imports. Any warning, unresolved import, external dependency, or native or platform-specific package fails. - Copies browser artifacts as built, after proving them current, and the migrations; generates package.json and ekklesia.package.json; never ships src, tests, declarations, source maps, or workspace ranges. - THIRD_PARTY_NOTICES: every third-party package traced from the bundles' own inputs, credited once with its declared licence and every licence, copying, copyright, and notice file it ships. A missing licence fails; a missing text fails unless its standard text is complete without copyright lines (Apache-2.0, vendored, needed by drizzle-orm). - Deterministic ustar and gzip: sorted paths, uid/gid 0, empty names, mtime 0, 0755/0644, no links. Byte-identical across runs and machines. - Writes only the archive and its .sha256; never deploys, links, or installs, so the workspace's install state is untouched. Core: verifyModulePackage and `verify-module-package.js <archive> [--sha256 | --sha256-file] [--extract-to <new dir>]`. Reads the archive in memory and refuses links, special entries, setuid/setgid/sticky modes, unsafe or escaping paths, and a second top-level directory; checks metadata, every file, identity, target, Node.js range, SDK, and the transitional Drizzle agreement; runs the platform's module and migration discovery; loads the backend once in a forked process with an empty environment; only then extracts, with normalized modes, into a new directory. Errors are sanitized. @ekklesia/module-ui-build: analyzeModuleUi reports each artifact's inputs and whether the built file is current. The nested stylesheet build now works from the module directory, so artifacts no longer depend on the caller's working directory. The split deploy smoke builds the package with the canonical packager (scripts/lib/build-module-package.Dockerfile replaces the test-only pnpm deploy packaging), verifies it with the same generic image, installs the verified tree, removes it, and reinstalls the exact same archive. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
45d6506c10
commit
35ef31df9f
@@ -78,42 +78,79 @@ names (`/var/lib/ekklesia/modules` in the production image, the checkout's `modu
|
||||
unset). Discovery reads it only when a process starts. Nothing is hot loaded, and an installed module
|
||||
is read-only to the platform.
|
||||
|
||||
An installed module directory is **self-contained for runtime use**. Everything the platform reads
|
||||
or loads from it, and everything those files import at run time, resolves inside it:
|
||||
|
||||
- `package.json`, with no `workspace:` range needed at run time, and `ekklesia.module.json`;
|
||||
- every built artifact the manifest declares: `dist/server`, `dist/schema`, jobs, and the UI,
|
||||
display, and contribution bundles;
|
||||
- the `drizzle/` migrations directory, with its journal and `_ekklesia.json`;
|
||||
- a `node_modules` holding the production dependencies of your server, schema, and jobs code,
|
||||
including your own copies of `@ekklesia/module-sdk`, `@ekklesia/contracts`, and `drizzle-orm`.
|
||||
An installed module directory is **self-contained for runtime use**: everything the platform reads
|
||||
or loads from it, and everything those files import at run time, resolves inside it. You never
|
||||
assemble that directory by hand. You build your module, and the canonical packager turns it into an
|
||||
**Ekklesia Module Package v1**
|
||||
([ADR 0049](../architecture/decisions/0049-module-package-v1.md)): one deterministic archive and its
|
||||
SHA-256, which an installation verifies and extracts into module storage.
|
||||
|
||||
**Copying `dist/` is not an installed module.** Your compiled server code imports
|
||||
`@ekklesia/module-sdk` and `drizzle-orm` by name, and Node resolves those only from your module's
|
||||
own directory upwards. The platform's `node_modules` is never on that path, and Ekklesia adds it
|
||||
through no link, `NODE_PATH`, loader hook, or alias. A module whose imports do not resolve stops the
|
||||
API from starting with a sanitized `MODULE_RUNTIME_IMPORT_FAILED`, so test the directory you ship,
|
||||
not your workspace.
|
||||
through no link, `NODE_PATH`, loader hook, or alias. The packager therefore bundles your backend.
|
||||
|
||||
**Native dependencies** must match the platform image's runtime: Node.js 24.18.0 on Debian 13
|
||||
(glibc), on the image's CPU architecture.
|
||||
#### Packaging your module
|
||||
|
||||
Build your module, then run the packager from the workspace your module is built in. It is a
|
||||
development dependency like `@ekklesia/module-ui-build`:
|
||||
|
||||
```bash
|
||||
pnpm exec ekklesia-pack-module --module . --target any --out ../packages \
|
||||
--source-revision "$(git rev-parse HEAD)"
|
||||
```
|
||||
|
||||
It writes `<id>-<version>-any.tar.gz` and its `.sha256` line, and nothing else anywhere: it never
|
||||
installs, links, or deploys, so your workspace is left exactly as it was. Never run `pnpm deploy` in
|
||||
a workspace instead: it rewrites that workspace's install state to production-only.
|
||||
|
||||
The archive holds one directory named with your module id:
|
||||
|
||||
```text
|
||||
<id>/
|
||||
├── package.json generated: name, version, private, "type": "module", the layer marker
|
||||
├── ekklesia.module.json your manifest, byte for byte
|
||||
├── ekklesia.package.json package metadata: target, runtime, provenance, file digests
|
||||
├── THIRD_PARTY_NOTICES licence and notice text of every third-party package bundled
|
||||
├── dist/server/index.js your backend entrypoints, bundled
|
||||
├── dist/schema/index.js
|
||||
├── dist/chunks/ code your backend entrypoints share
|
||||
├── dist/ui/ … dist/display/ … dist/contributions/ your browser artifacts, exactly as built
|
||||
└── drizzle/ your migrations
|
||||
```
|
||||
|
||||
- **Bundled backend.** Your server, schema, and jobs entrypoints are bundled with every
|
||||
pure-JavaScript dependency they use, the Module SDK, the contracts, and Drizzle included. The only
|
||||
imports left are Node.js built-ins. A dependency the bundle cannot resolve fails packaging; nothing
|
||||
is left to be found at run time.
|
||||
- **Never shipped:** `src/`, tests, TypeScript declarations, source maps, development dependencies,
|
||||
your development `package.json`, any `workspace:` range, and links of any kind.
|
||||
- **Browser artifacts must be current.** The packager rebuilds them in memory and refuses an artifact
|
||||
on disk that differs, so run your build first.
|
||||
- **`any` is the only target today.** A dependency that is native or platform-specific (a compiled
|
||||
addon, a package that declares an operating system or CPU, or one that loads a per-platform binary
|
||||
such as `sharp`) cannot be in an `any` package, and packaging fails. Native targets come later.
|
||||
- **Licences.** `THIRD_PARTY_NOTICES` credits every third-party package your bundles contain, with
|
||||
its declared licence and the licence and notice files it ships. A package that declares no licence
|
||||
fails packaging, and so does one that ships no licence text, unless its licence's standard text is
|
||||
complete without a copyright line (Apache-2.0, which the packager then supplies and says so). The
|
||||
Ekklesia platform's own packages are listed in `ekklesia.package.json` under `incorporates`.
|
||||
- **Deterministic.** The same packager version and the same built inputs produce the same archive,
|
||||
byte for byte, wherever the checkout is.
|
||||
|
||||
**Drizzle, for now.** You define tables with your own copy of Drizzle, and Core runs your queries
|
||||
through the platform's Drizzle instance (see [Database access](#database-access)). Objects from two
|
||||
copies interoperate only when the copies are interchangeable, so the current implementation requires
|
||||
**exactly the Drizzle version the platform uses**: the one `@ekklesia/module-sdk` depends on. This is
|
||||
a constraint of today's implementation, not a permanent contract.
|
||||
**exactly the Drizzle version the platform uses**: the one `@ekklesia/module-sdk` depends on. The
|
||||
package records it under `transitional`, and an installation refuses a package whose Drizzle differs.
|
||||
This is a constraint of today's implementation, not a permanent contract.
|
||||
|
||||
Browser artifacts are already self-contained bundles, so nothing changes for them.
|
||||
#### Verifying a package
|
||||
|
||||
The format modules are delivered in is proposed in
|
||||
[ADR 0049](../architecture/decisions/0049-module-package-v1.md): one deterministic archive per
|
||||
target, with bundled backend code, produced by an Ekklesia-owned packager that does not exist yet.
|
||||
Until it does, the repository's smoke test packages the module template with `pnpm deploy` in a
|
||||
disposable Docker build (`scripts/lib/test-module-package.Dockerfile`), a test fixture and not a
|
||||
package format. Never run `pnpm deploy` in your own workspace: it rewrites that workspace's install
|
||||
state to production-only, and the next build or `pnpm verify` then fails until a normal
|
||||
`pnpm install` repairs it.
|
||||
`node core/dist/cli/verify-module-package.js <archive> --sha256-file <archive>.sha256`, run from an
|
||||
Ekklesia installation (`/opt/ekklesia` in the image), checks a package against that platform without
|
||||
installing it. Pass `--extract-to <new directory>` to receive the verified module directory.
|
||||
[deploy/README.md](../../deploy/README.md#installed-modules) shows the whole installation.
|
||||
|
||||
The repository's boundary checks (`pnpm boundaries`) prove only source modules in a workspace. An
|
||||
installed package normally has no source, so the platform cannot check it the same way. Your
|
||||
|
||||
Reference in New Issue
Block a user