|
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
# 0049: A module release is one immutable, self-contained package: Ekklesia Module Package v1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- Status: Proposed
|
|
|
|
|
|
|
|
- Date: 2026-10-07
|
|
|
|
|
|
|
|
- Supersedes: none
|
|
|
|
|
|
|
|
- Extends: [0048](0048-generic-image-and-installed-module-storage.md)
|
|
|
|
|
|
|
|
- Related: [ADR 0006](0006-versioned-capabilities-and-compiled-module-runtime.md),
|
|
|
|
|
|
|
|
[ADR 0007](0007-module-owned-drizzle-schemas.md),
|
|
|
|
|
|
|
|
[ADR 0008](0008-module-owned-migration-contracts.md),
|
|
|
|
|
|
|
|
[ADR 0009](0009-transactional-module-migrations.md),
|
|
|
|
|
|
|
|
[ADR 0016](0016-module-database-access.md),
|
|
|
|
|
|
|
|
[ADR 0021](0021-modules-as-hostable-products.md),
|
|
|
|
|
|
|
|
[ADR 0023](0023-module-ui-build-pipeline.md),
|
|
|
|
|
|
|
|
[ADR 0028](0028-module-operational-lifecycle.md),
|
|
|
|
|
|
|
|
[ADR 0040](0040-module-stylesheets-and-artifact-isolation.md),
|
|
|
|
|
|
|
|
[ADR 0041](0041-module-browser-contributions.md),
|
|
|
|
|
|
|
|
[ADR 0044](0044-self-hosted-reference-deployment.md),
|
|
|
|
|
|
|
|
[ADR 0047](0047-module-ci-harness.md)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ADR 0048 took modules out of the platform image. An installation keeps them in its own module
|
|
|
|
|
|
|
|
storage, mounted read-only, and discovers them at a controlled restart. Each installed module must
|
|
|
|
|
|
|
|
be a self-contained directory: the platform lends it no dependencies. ADR 0048 deferred the format
|
|
|
|
|
|
|
|
a module is delivered in.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
That gap is now the obstacle. Nothing yet produces an installed module directory except a test-only
|
|
|
|
|
|
|
|
Dockerfile, so a module release cannot be built, distributed, and installed on its own. An
|
|
|
|
|
|
|
|
investigation against the module template and Signage 0.1.2 established:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **`dist/` alone does not run.** Compiled server and schema code imports `@ekklesia/module-sdk`
|
|
|
|
|
|
|
|
(and through it `@ekklesia/contracts`, zod, and semver) and `drizzle-orm` by name, and Node never
|
|
|
|
|
|
|
|
resolves those from the platform's `node_modules`.
|
|
|
|
|
|
|
|
- **Bundling works.** Bundling the template's server and schema with esbuild gave about 720 KB of
|
|
|
|
|
|
|
|
backend code and no `node_modules` at all. Inside the generic image, run as its unprivileged
|
|
|
|
|
|
|
|
user from a read-only mount at `/var/lib/ekklesia/modules`, it passed module, schema, and
|
|
|
|
|
|
|
|
migration validation, and its server entrypoint imported. Core's own Drizzle instance built a
|
|
|
|
|
|
|
|
query over the bundled module's table.
|
|
|
|
|
|
|
|
- **Native dependencies cannot be bundled.** Signage needs `sharp`, a Node-API addon whose
|
|
|
|
|
|
|
|
platform packages carry an 18.6 MB `libvips` shared library for one operating system, CPU, and
|
|
|
|
|
|
|
|
C library (Linux, x64, glibc ≥ 2.28). With its backend bundled and only `sharp` and its own
|
|
|
|
|
|
|
|
dependencies kept beside it, Signage was 22 MB and contained no link. It validated and processed
|
|
|
|
|
|
|
|
an image inside the same image.
|
|
|
|
|
|
|
|
- **`pnpm deploy` is the wrong tool.** Non-legacy mode refuses unless the whole workspace switches
|
|
|
|
|
|
|
|
to injected workspace packages. Legacy mode leaves a link escaping the package. Every mode
|
|
|
|
|
|
|
|
ships packages nothing loads at run time (React and its DOM, type packages, database drivers
|
|
|
|
|
|
|
|
pulled in as peers) and leaves `workspace:` ranges in copied manifests. Running it inside a
|
|
|
|
|
|
|
|
workspace rewrites that workspace's install state to production-only, which then breaks the
|
|
|
|
|
|
|
|
workspace's own builds. It did so in the investigation's scratch workspaces, and earlier in a
|
|
|
|
|
|
|
|
developer's own checkout.
|
|
|
|
|
|
|
|
- **A deterministic archive is cheap.** A tar archive with sorted paths, fixed owner and
|
|
|
|
|
|
|
|
timestamps, and a gzip header without a name or time produced the same SHA-256 twice. Gzip packed
|
|
|
|
|
|
|
|
Signage to 8.5 MB.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Module releases also need a contract that does not depend on where they are hosted. Ekklesia's
|
|
|
|
|
|
|
|
source and CI live on a Gitea server today; a marketplace, another host, object storage, or offline
|
|
|
|
|
|
|
|
media may carry the same release tomorrow.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Decision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Purpose
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A separately versioned Ekklesia module is distributed as an **Ekklesia Module Package**: one
|
|
|
|
|
|
|
|
immutable, self-contained archive. Installing, updating, or removing a module never rebuilds the
|
|
|
|
|
|
|
|
platform image (ADR 0048).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The package contract is independent of marketplace discovery, of the source-code host, of any
|
|
|
|
|
|
|
|
artifact registry, of the church installing it, and of the URL it was fetched from. Catalogues,
|
|
|
|
|
|
|
|
marketplaces, and signing are separate concerns, and none of them is decided here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### The artifact is a deterministic `.tar.gz`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Module Package v1 is a gzip-compressed tar archive. Its identity is its contents, never its
|
|
|
|
|
|
|
|
transport: the same archive may travel through Gitea, GitHub, an OCI registry, object storage,
|
|
|
|
|
|
|
|
offline media, or a future Ekklesia marketplace without any change to what it installs. No registry,
|
|
|
|
|
|
|
|
host, or OCI concept is part of the package format.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The canonical packager produces the archive **deterministically**: the same packager version and
|
|
|
|
|
|
|
|
the same inputs produce byte-identical output. At minimum:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **Entries.** Every directory and regular file appears in byte-wise sorted path order, directories
|
|
|
|
|
|
|
|
before their contents.
|
|
|
|
|
|
|
|
- **Ownership.** uid and gid are 0 and the user and group names are empty.
|
|
|
|
|
|
|
|
- **Timestamps.** Every entry's modification time is 0, and the archive holds no access, change, or
|
|
|
|
|
|
|
|
creation time.
|
|
|
|
|
|
|
|
- **Modes.** Directories are `0755` and files `0644` (see [Archive safety](#archive-safety)).
|
|
|
|
|
|
|
|
- **No host metadata.** No extended attributes, ACLs, or archive-level headers that carry host or
|
|
|
|
|
|
|
|
time information.
|
|
|
|
|
|
|
|
- **gzip.** The header has no file name and a modification time of 0, at a fixed compression level.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The SHA-256 of the complete archive is distributed beside it, as a separate `.sha256` file in the
|
|
|
|
|
|
|
|
`sha256sum` format. The **recommended** file name is `<module id>-<module version>-<target>.tar.gz`,
|
|
|
|
|
|
|
|
for example `signage-0.1.2-linux-x64-glibc.tar.gz`. A file name is a convenience, never trusted:
|
|
|
|
|
|
|
|
what the archive is comes from its contents and its digest.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### What a package contains
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A v1 archive contains exactly one top-level directory, named with the module id. Its contents are
|
|
|
|
|
|
|
|
the installed module directory ADR 0048 requires:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
|
|
<module id>/
|
|
|
|
|
|
|
|
package.json generated runtime metadata
|
|
|
|
|
|
|
|
ekklesia.module.json the module's manifest, unchanged
|
|
|
|
|
|
|
|
ekklesia.package.json package metadata (below)
|
|
|
|
|
|
|
|
THIRD_PARTY_NOTICES licence and notice material for everything bundled or included
|
|
|
|
|
|
|
|
dist/ bundled backend entrypoints, their shared chunks, and the declared
|
|
|
|
|
|
|
|
UI, display, and contribution artifacts
|
|
|
|
|
|
|
|
drizzle/ migration SQL, journal, snapshots, and _ekklesia.json, when declared
|
|
|
|
|
|
|
|
node_modules/ only when the package needs a native runtime dependency (below)
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Every entrypoint the manifest declares exists at its declared path.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
It contains nothing else, and in particular:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- source files, unless something at run time reads them;
|
|
|
|
|
|
|
|
- tests;
|
|
|
|
|
|
|
|
- TypeScript declarations;
|
|
|
|
|
|
|
|
- source maps, until a later decision permits them;
|
|
|
|
|
|
|
|
- development dependencies;
|
|
|
|
|
|
|
|
- any `workspace:` reference;
|
|
|
|
|
|
|
|
- any link (see [Archive safety](#archive-safety)).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**The runtime `package.json` is generated by the packager, never copied** from the module's
|
|
|
|
|
|
|
|
development workspace. It holds only what module discovery reads:
|
|
|
|
|
|
|
|
- `name` and `version` (equal to the manifest's version);
|
|
|
|
|
|
|
|
- `"private": true`;
|
|
|
|
|
|
|
|
- `"type": "module"`;
|
|
|
|
|
|
|
|
- `"ekklesia": { "layer": "module" }`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
It declares no dependencies, scripts, exports, or imports, because nothing at run time resolves
|
|
|
|
|
|
|
|
through them.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The manifest is copied unchanged. It stays the module's authoritative runtime contract, and its
|
|
|
|
|
|
|
|
`compatibility.platform` and `compatibility.moduleSdk` ranges stay the only place those are stated.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Backend code is bundled
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The canonical packager bundles the module's backend entrypoints (server, schema, and jobs) with
|
|
|
|
|
|
|
|
every pure-JavaScript dependency they use. That includes `@ekklesia/module-sdk`,
|
|
|
|
|
|
|
|
`@ekklesia/contracts`, and ordinary libraries. The bundles are ES modules at the manifest's declared
|
|
|
|
|
|
|
|
paths, and they may share chunks under `dist/`. Browser artifacts are already self-contained bundles
|
|
|
|
|
|
|
|
(ADR 0023, ADR 0040, ADR 0041) and are packaged as built.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The platform lends an installed module nothing (ADR 0048). **`node_modules` in a package is the
|
|
|
|
|
|
|
|
exception, not the default.** A dependency stays outside the bundle only when it must exist as
|
|
|
|
|
|
|
|
separate files at run time: a native Node addon, or a package that is equally platform-specific.
|
|
|
|
|
|
|
|
Then the package carries it, and exactly its own runtime dependencies, flat under
|
|
|
|
|
|
|
|
`node_modules/<name>`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**The canonical packager decides what stays external, and validates it.** It refuses to package a
|
|
|
|
|
|
|
|
module whose bundled code still needs a file it did not include, or whose externals are not native
|
|
|
|
|
|
|
|
or platform-specific. Choosing which dependencies to leave unbundled is not part of the module
|
|
|
|
|
|
|
|
author's interface.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Targets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Every package has one **target**:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **`any`**: the package contains no native or platform-specific file. It runs wherever its declared
|
|
|
|
|
|
|
|
Node.js range is met.
|
|
|
|
|
|
|
|
- **`<platform>-<arch>[-<libc>]`**: the package contains native code for exactly one environment.
|
|
|
|
|
|
|
|
- `<platform>` and `<arch>` use Node.js's own `process.platform` and `process.arch` names.
|
|
|
|
|
|
|
|
- On Linux, `<libc>` is required and is `glibc` or `musl`. On other platforms it is omitted.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Examples: `linux-x64-glibc`, `linux-arm64-glibc`, `linux-x64-musl`. Signage, because of `sharp`, is
|
|
|
|
|
|
|
|
`linux-x64-glibc` for the current production image.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A package has exactly one target. A module that ships for several environments publishes one
|
|
|
|
|
|
|
|
package per target. Packages carrying several architectures at once are not part of v1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### `ekklesia.package.json`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For example (the values are illustrative):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
"packageFormat": 1,
|
|
|
|
|
|
|
|
"module": { "id": "signage", "version": "0.1.2" },
|
|
|
|
|
|
|
|
"target": "linux-x64-glibc",
|
|
|
|
|
|
|
|
"runtime": {
|
|
|
|
|
|
|
|
"node": ">=24.11.0 <25",
|
|
|
|
|
|
|
|
"nodeApi": 9,
|
|
|
|
|
|
|
|
"libcVersion": ">=2.28"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"provenance": {
|
|
|
|
|
|
|
|
"revision": "86ae907f59458508e3277666cdd214f5bd7c1078",
|
|
|
|
|
|
|
|
"repository": "https://example.org/your-org/signage.git",
|
|
|
|
|
|
|
|
"packager": { "name": "@ekklesia/module-pack", "version": "0.2.0" }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"incorporates": {
|
|
|
|
|
|
|
|
"@ekklesia/module-sdk": "0.1.2",
|
|
|
|
|
|
|
|
"@ekklesia/contracts": "0.1.2"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"transitional": {
|
|
|
|
|
|
|
|
"databaseRuntime": { "drizzle-orm": "0.45.2" }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"files": [
|
|
|
|
|
|
|
|
{ "path": "THIRD_PARTY_NOTICES", "size": 48211, "sha256": "…" },
|
|
|
|
|
|
|
|
{ "path": "dist/server/index.js", "size": 1346, "sha256": "…" }
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The fields are:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **`packageFormat`**: the integer `1`. An installer refuses a format it does not implement. Format
|
|
|
|
|
|
|
|
versions are strict cutovers, like manifest versions.
|
|
|
|
|
|
|
|
- **`module.id`, `module.version`**: must equal the manifest's and the runtime `package.json`'s.
|
|
|
|
|
|
|
|
The top-level directory must be named with the id.
|
|
|
|
|
|
|
|
- **`target`**: as defined above.
|
|
|
|
|
|
|
|
- **`runtime`**: what the package needs from the Node.js runtime that will load it.
|
|
|
|
|
|
|
|
- `node`, a semantic-version range, is always present.
|
|
|
|
|
|
|
|
- A native package also states:
|
|
|
|
|
|
|
|
- `nodeApi`: the minimum Node-API version, for Node-API addons;
|
|
|
|
|
|
|
|
- or `nodeAbi`: the exact `process.versions.modules`, for an addon built against one Node
|
|
|
|
|
|
|
|
ABI;
|
|
|
|
|
|
|
|
- and on Linux, `libcVersion`: the minimum C library version.
|
|
|
|
|
|
|
|
- **`provenance`**: how the package was built.
|
|
|
|
|
|
|
|
- `revision`: the exact source revision, always present.
|
|
|
|
|
|
|
|
- `repository`: informational only. Nothing trusts it, and it ties the package to no host.
|
|
|
|
|
|
|
|
- `packager`: the packager's name and version.
|
|
|
|
|
|
|
|
- **`incorporates`**: the exact versions of the Ekklesia SDK and runtime libraries bundled into the
|
|
|
|
|
|
|
|
package. The packager refuses a package whose incorporated `@ekklesia/module-sdk` does not satisfy
|
|
|
|
|
|
|
|
the manifest's own `compatibility.moduleSdk` range, so the two can never disagree.
|
|
|
|
|
|
|
|
- **`transitional`**: compatibility facts that today's implementation needs and that are not part
|
|
|
|
|
|
|
|
of the long-term contract. See [Database runtime compatibility](#database-runtime-compatibility).
|
|
|
|
|
|
|
|
An installer must understand every key it finds there or refuse the package.
|
|
|
|
|
|
|
|
- **`files`**: one entry for every regular file in the module directory **except
|
|
|
|
|
|
|
|
`ekklesia.package.json` itself**, sorted by path. Each entry has its path relative to the module
|
|
|
|
|
|
|
|
directory, its size in bytes, and its SHA-256. The set is exact: a payload file missing from the
|
|
|
|
|
|
|
|
list, or a listed file missing from the payload, invalidates the package.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The archive's `.sha256` covers the whole archive, `ekklesia.package.json` included. The per-file
|
|
|
|
|
|
|
|
digests let an installer, or an operator later, check an extracted directory without the archive.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Platform and Module SDK compatibility stay in `ekklesia.module.json` and are not repeated here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Database runtime compatibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Core runs a module's queries through the platform's Drizzle instance over tables the module defined
|
|
|
|
|
|
|
|
with its own copy of Drizzle (ADR 0016, ADR 0048). Those objects only interoperate when the two
|
|
|
|
|
|
|
|
copies are interchangeable. So while it is needed, a package bundles Drizzle with its backend code,
|
|
|
|
|
|
|
|
records the version in `transitional.databaseRuntime`, and an installer refuses a package whose
|
|
|
|
|
|
|
|
Drizzle version differs from the platform's.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**This is an implementation constraint of today's platform, not the Ekklesia Module Database ABI.**
|
|
|
|
|
|
|
|
Object identity and version agreement between two Drizzle copies are not a contract this ADR
|
|
|
|
|
|
|
|
establishes or that module authors may rely on. A separate Module Database ABI decision will replace
|
|
|
|
|
|
|
|
it, and that ABI will make the platform's ORM an implementation detail. When it lands, the
|
|
|
|
|
|
|
|
`transitional.databaseRuntime` requirement goes away under the format-versioning rules here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Licence obligations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Bundling copies third-party code into the module's own files, where the licence headers that came
|
|
|
|
|
|
|
|
with it may not survive. **Every package therefore carries `THIRD_PARTY_NOTICES`**: a generated
|
|
|
|
|
|
|
|
plain-text file at the module directory's root. It names each third-party package bundled into
|
|
|
|
|
|
|
|
or included in the package, with its version and licence, and holds the licence and notice text
|
|
|
|
|
|
|
|
that redistribution requires.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The packager's implementation must establish exactly how licence and notice material is collected
|
|
|
|
|
|
|
|
and preserved, including licence comments that bundling would otherwise strip. Until it does, no
|
|
|
|
|
|
|
|
package is treated as a distributable release.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Archive safety
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**v1 packages contain no links of any kind.** Neither symbolic nor hard links are permitted, so no
|
|
|
|
|
|
|
|
link can escape the package, and there is no case where one is allowed. Native dependencies were
|
|
|
|
|
|
|
|
packaged without any.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A conforming archive also contains no:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- absolute path;
|
|
|
|
|
|
|
|
- `..` path segment;
|
|
|
|
|
|
|
|
- path outside the single top-level module directory;
|
|
|
|
|
|
|
|
- device node, FIFO, or socket;
|
|
|
|
|
|
|
|
- setuid, setgid, or sticky bit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
It holds only directories and regular files.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**Extraction normalizes ownership and modes.** Everything is owned by the installation, not by the
|
|
|
|
|
|
|
|
uid or gid in the archive. Directories are `0755` and files `0644`, readable by the API's
|
|
|
|
|
|
|
|
unprivileged user and writable by nobody but the installer. No file in a v1 package is executable.
|
|
|
|
|
|
|
|
Loading JavaScript and native addons needs no execute bit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
An installer refuses an archive that breaks any of these rules, and never extracts it partially into
|
|
|
|
|
|
|
|
module storage.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Installation model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This ADR records the intended installation lifecycle; it does not implement it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
|
|
acquire the archive
|
|
|
|
|
|
|
|
→ verify the archive's SHA-256
|
|
|
|
|
|
|
|
→ extract into a hidden staging directory inside module storage
|
|
|
|
|
|
|
|
→ validate ekklesia.package.json, then every file's size and SHA-256
|
|
|
|
|
|
|
|
→ validate the target and runtime against this platform and host
|
|
|
|
|
|
|
|
→ validate the manifest and the complete candidate inventory: the installed modules, with this
|
|
|
|
|
|
|
|
candidate in place of any version of the same module
|
|
|
|
|
|
|
|
→ load the backend entrypoints once in an isolated, short-lived validation process
|
|
|
|
|
|
|
|
→ produce the migration plan
|
|
|
|
|
|
|
|
→ stop the API
|
|
|
|
|
|
|
|
→ swap the candidate into the active module directory, keeping the previous version
|
|
|
|
|
|
|
|
→ apply migrations, only with explicit operator approval
|
|
|
|
|
|
|
|
→ start the API
|
|
|
|
|
|
|
|
→ verify readiness
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Details:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **Hidden directories.** Staging and previous versions live in dot-prefixed directories inside
|
|
|
|
|
|
|
|
module storage. Module discovery ignores hidden entries (ADR 0048), so neither is ever loaded.
|
|
|
|
|
|
|
|
- **The swap.** It happens only while the API is stopped. It is as close to atomic as the
|
|
|
|
|
|
|
|
filesystem allows, and an interrupted swap is recoverable from the kept previous version.
|
|
|
|
|
|
|
|
- **Who writes.** Module storage stays read-only to Ekklesia. Whatever performs the installation
|
|
|
|
|
|
|
|
writes it from outside the API process (ADR 0048).
|
|
|
|
|
|
|
|
- **No hot loading.** Discovery still happens only when a process starts.
|
|
|
|
|
|
|
|
- **Migrations stay explicit.** Nothing in this lifecycle applies a migration on its own (ADR 0009,
|
|
|
|
|
|
|
|
ADR 0044).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Rollback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **Before any migration from the new version is applied,** restoring the previous module directory
|
|
|
|
|
|
|
|
and restarting is a supported rollback.
|
|
|
|
|
|
|
|
- **After forward migrations are applied,** restoring older code is **not** a supported rollback.
|
|
|
|
|
|
|
|
The older package lacks the newer migrations, so migration planning refuses it as a downgrade
|
|
|
|
|
|
|
|
(ADR 0048). Even running it would mean old code against a newer schema. A true rollback then
|
|
|
|
|
|
|
|
means restoring the matching database and storage backup, unless a future, module-specific
|
|
|
|
|
|
|
|
compatibility mechanism says otherwise.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
There are no down migrations (ADR 0008, ADR 0009). An installer must make the point of no return
|
|
|
|
|
|
|
|
explicit: back up before applying, and offer code-only rollback only while no new migration has been
|
|
|
|
|
|
|
|
applied.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Trust
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The SHA-256 digest proves integrity: the archive is the one whose digest you were given. It says
|
|
|
|
|
|
|
|
nothing about who published it. Package signatures, publisher trust roots, marketplace verification,
|
|
|
|
|
|
|
|
and trust levels for third-party modules are deferred.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Installed modules remain trusted backend code under ADR 0048. A conforming package is not sandboxed,
|
|
|
|
|
|
|
|
and passing validation does not make a module safe; it makes it well-formed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Canonical tooling
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Packages are produced by one Ekklesia-owned packager, proposed as the workspace package
|
|
|
|
|
|
|
|
`packages/module-pack` with the command `ekklesia-pack-module`, run in a module's CI. Its command
|
|
|
|
|
|
|
|
line is an implementation detail; this ADR defines what it must produce.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**`pnpm deploy` is not the packaging mechanism**, for the reasons in Context:
|
|
|
|
|
|
|
|
- it includes runtime dependencies nothing loads;
|
|
|
|
|
|
|
|
- it leaves `workspace:` metadata;
|
|
|
|
|
|
|
|
- it leaves links that escape the package, or needs a workspace-wide linking change to avoid them;
|
|
|
|
|
|
|
|
- it rewrites the install state of the workspace it runs in.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The repository's test-only packaging of the module template
|
|
|
|
|
|
|
|
(`scripts/lib/test-module-package.Dockerfile`) is replaced by the canonical packager when one
|
|
|
|
|
|
|
|
exists.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Relationship to the module CI harness
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ADR 0047, still proposed, is already marked for revision against ADR 0048. Its harness should
|
|
|
|
|
|
|
|
consume canonical module packages: a module's CI builds its package, and the harness installs that
|
|
|
|
|
|
|
|
exact archive into module storage of the generic image. The harness then tests the artifact a church
|
|
|
|
|
|
|
|
would install, not a workspace build of it. Until the packager exists, the harness and smoke tests
|
|
|
|
|
|
|
|
may keep their current test-only packaging.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- A module release becomes one file plus its digest, built once by the module's CI and installable
|
|
|
|
|
|
|
|
on any compatible Ekklesia installation without touching the platform image.
|
|
|
|
|
|
|
|
- Module authors gain a packaging step and a target per native environment. A pure-JavaScript
|
|
|
|
|
|
|
|
module ships one `any` package; Signage ships `linux-x64-glibc` packages for the current fleet.
|
|
|
|
|
|
|
|
- Packages are small and auditable: bundled code plus, only where needed, a native dependency
|
|
|
|
|
|
|
|
tree. There are no links and no development dependencies, and every file is listed with its
|
|
|
|
|
|
|
|
digest.
|
|
|
|
|
|
|
|
- Each package carries its own copy of the SDK and contracts. Version agreement with the platform is
|
|
|
|
|
|
|
|
checked when the package is built, against the manifest's `moduleSdk` range, and Core keeps
|
|
|
|
|
|
|
|
validating module definitions structurally (ADR 0048).
|
|
|
|
|
|
|
|
- Rolling back after a schema-changing update is a backup restore. Operators must take a backup
|
|
|
|
|
|
|
|
before approving migrations.
|
|
|
|
|
|
|
|
- Licence handling becomes the packager's responsibility, and until it is proven no package
|
|
|
|
|
|
|
|
counts as a release.
|
|
|
|
|
|
|
|
- The Drizzle requirement is visible in package metadata as `transitional`, so its eventual removal
|
|
|
|
|
|
|
|
is a recorded format change and not a silent behaviour change.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Alternatives
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **`pnpm deploy` as the packager.** Rejected for the reasons in Context.
|
|
|
|
|
|
|
|
- **Copying the workspace's dependency tree without bundling.** Rejected as the default. It ships
|
|
|
|
|
|
|
|
far more than runs (49 MB against 22 MB for Signage), keeps `workspace:` ranges, and makes every
|
|
|
|
|
|
|
|
pure-JavaScript dependency a file-layout concern. Bundling is already how browser artifacts are
|
|
|
|
|
|
|
|
built.
|
|
|
|
|
|
|
|
- **Platform-lent runtime dependencies.** Rejected by ADR 0048.
|
|
|
|
|
|
|
|
- **Installing the SDK from a registry when the module is installed.** Rejected: installation would
|
|
|
|
|
|
|
|
need network access and a resolver on every installation, and an installation's code would depend
|
|
|
|
|
|
|
|
on what a registry served that day. Publishing the SDK packages for module *builds* is a separate,
|
|
|
|
|
|
|
|
compatible future step.
|
|
|
|
|
|
|
|
- **Module-author-chosen externals.** Rejected: an arbitrary external is an undeclared runtime
|
|
|
|
|
|
|
|
dependency the platform cannot verify, and it reintroduces the `node_modules` layout as a
|
|
|
|
|
|
|
|
contract.
|
|
|
|
|
|
|
|
- **An OCI artifact as the package format.** Not adopted for v1: it needs registry tooling and login
|
|
|
|
|
|
|
|
to install, and adds nothing to integrity that the digest does not already give. Nothing here
|
|
|
|
|
|
|
|
prevents carrying the same `.tar.gz` inside an OCI artifact.
|
|
|
|
|
|
|
|
- **`.tar.zst`.** Not adopted for v1: about 17 % smaller for Signage at a high compression level,
|
|
|
|
|
|
|
|
but about three times slower to produce there, and less universally available than gzip. A later
|
|
|
|
|
|
|
|
format version may add it.
|
|
|
|
|
|
|
|
- **Packages carrying several architectures at once.** Not adopted for v1: each extra architecture
|
|
|
|
|
|
|
|
adds about 18.5 MB for Signage, for a fleet that runs one architecture. A later format version may
|
|
|
|
|
|
|
|
allow it.
|
|
|
|
|
|
|
|
- **Permitting internal symbolic links.** Rejected: links are where escape checks are subtle, and
|
|
|
|
|
|
|
|
the native dependencies that motivated the question were packaged without any.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Deferred
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- Package signatures and publisher trust roots.
|
|
|
|
|
|
|
|
- A marketplace or catalogue, and registry or hosting-provider integration.
|
|
|
|
|
|
|
|
- An installer, and any installer UI; an automatic update service.
|
|
|
|
|
|
|
|
- The purge workflow (ADR 0048).
|
|
|
|
|
|
|
|
- Packages carrying several architectures at once.
|
|
|
|
|
|
|
|
- The stable Module Database ABI that retires `transitional.databaseRuntime`.
|
|
|
|
|
|
|
|
- Source maps and other debugging material in or beside packages.
|
|
|
|
|
|
|
|
- Publishing the Ekklesia SDK packages for module builds outside a platform workspace.
|
|
|
|
|
|
|
|
- Moving Signage to the new package format.
|