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]>
84 lines
5.7 KiB
Markdown
84 lines
5.7 KiB
Markdown
# Ekklesia module template
|
|
|
|
This is the smallest supported Ekklesia module product. It proves the static manifest,
|
|
compiled runtime and schema entrypoints, capability registration, a module-owned Drizzle table, a
|
|
generated module-owned migration, logical storage-bucket declarations, and one permissioned HTTP
|
|
route (`GET /records`, declared in `register`) that reads that table through
|
|
`context.services.database` — the platform-provided `ModuleDatabase` facade, never a
|
|
module-constructed Drizzle instance or a raw `pg` client. Migration execution and storage
|
|
provisioning are intentionally not included.
|
|
|
|
It also ships two independently optional React roles: a primary/management UI at `src/ui/index.tsx`
|
|
and a full-viewport display at `src/display/index.tsx`. One invocation of `pnpm build` compiles each
|
|
declared role to the exact artifact in `entrypoints.ui` and `entrypoints.display.artifact`
|
|
([ADR 0023](../../docs/architecture/decisions/0023-module-ui-build-pipeline.md)). Both default-export
|
|
the same role-neutral `defineModuleUi({ id, version, mount })` ABI and receive the same
|
|
`ModuleUiHostContext`; there is deliberately no mode flag. The manifest entrypoint and the host route
|
|
choose the role.
|
|
|
|
Either role can be omitted independently. To remove only the display example, delete `src/display/`
|
|
and `entrypoints.display` (and remove `example.display.view` if nothing else uses it). To keep the
|
|
module backend-only, also delete `src/ui/`, `tsconfig.ui.json`, `tsconfig.ui-test.json`,
|
|
`tests/module-ui.test.ts`, the `ui` entrypoint, and the React/module-UI dependencies. The builder
|
|
succeeds with an empty artifact list when neither role is declared.
|
|
|
|
`tests/module-ui.test.ts` builds both artifacts from source, validates them with
|
|
`parseModuleUiExport`, mounts each into a real DOM, and unmounts it. Ekklesia mounts the primary UI
|
|
inside its Dashboard shell and serves the display without Dashboard chrome at the stable
|
|
`/display/example` page. See
|
|
[Declaring UI entrypoints](../../docs/module-development/README.md#declaring-ui-entrypoints).
|
|
|
|
To create a module inside this repository:
|
|
|
|
1. Copy this directory to `modules/<module-id>`.
|
|
2. Rename the package in `package.json` and remove `private` when it is ready for publication.
|
|
3. Update the ID, name, version, namespaces, compatibility ranges, and bucket requests in
|
|
`ekklesia.module.json`.
|
|
4. Keep the manifest capability versions and dependency ranges aligned with the contracts the
|
|
module actually implements and consumes.
|
|
5. Replace `src/server/index.ts` with backend behavior only. Its `defineModuleRuntime` ID and version
|
|
must match the manifest; do not duplicate the rest of the manifest in TypeScript. Declare HTTP
|
|
routes only in `register`, against a permission the manifest already declares; `start`, `stop`,
|
|
and every route handler — never `register` — receive `context.services.database`, a typed
|
|
`ModuleDatabase` view over the platform's one PostgreSQL pool.
|
|
6. Replace the pure Drizzle declarations in `src/schema/tables.ts` and keep its table prefix aligned
|
|
with the module ID. Keep the `defineModuleSchema` ownership wrapper in `src/schema/index.ts`
|
|
aligned too, and never import another module's schema.
|
|
7. Update `drizzle.config.ts`, generate forward migrations with `pnpm migration:generate
|
|
--name=lowercase_name` from the module directory, and review the SQL. Keep each generated tag in
|
|
`drizzle/meta/_ekklesia.json`, classify it as `none` or `approval-required`, and change that
|
|
file's `moduleId` when renaming the template.
|
|
8. Run `pnpm install`, `pnpm build`, `pnpm modules:validate`, `pnpm migrations:validate`, and
|
|
`pnpm verify` from the repository
|
|
root. The declared production entrypoint is the compiled `dist/server/index.js`, not source
|
|
TypeScript; the schema entrypoint is likewise compiled to `dist/schema/index.js`.
|
|
|
|
That `modules/<module-id>` directory is where a module is **developed**. An installation never runs
|
|
it from there: Ekklesia's image contains no modules, and an installation installs a self-contained
|
|
copy of the module into its module storage
|
|
([An installed module is self-contained](../../docs/module-development/README.md#an-installed-module-is-self-contained),
|
|
ADR 0048). Copying `dist/` alone is not enough, because the compiled server code imports the Module
|
|
SDK and Drizzle by name. After building, package the module with the canonical packager, which
|
|
bundles the backend into one deterministic Ekklesia Module Package (ADR 0049):
|
|
|
|
```bash
|
|
pnpm modules:pack --module templates/module --target any --out <directory> \
|
|
--source-revision "$(git rev-parse HEAD)"
|
|
```
|
|
|
|
The module owns its feature screens and display renderer. Do not add platform navigation, the
|
|
containing shell, global theme, authentication screens, or top-level routing; those belong to
|
|
surfaces and Core. A module UI asks its host for relative API paths and realtime state, and never
|
|
hardcodes Ekklesia routes. The display permission is enforced by the host before it serves the page
|
|
or descriptor; `context.permissions` remains a presentation-only hint.
|
|
Keep imports side-effect-free, register capabilities in `register`, acquire resources in `start`,
|
|
and release them in `stop`. Never construct a Drizzle instance, read `DATABASE_URL`, or reach for a
|
|
raw `pg` client — `context.services.database` is the only supported way to query this module's own
|
|
tables, and it is never available during `register`.
|
|
|
|
The migration directory uses the pinned stable Drizzle Kit `0.31.10` PostgreSQL journal and
|
|
snapshot format. Migrations are forward-only and may modify only this module's objects. Offline
|
|
validation treats SQL as opaque bytes, calculates SHA-256 from the exact contents, and never opens a
|
|
database or executes SQL. Destructive declarations are review metadata, not automatic SQL
|
|
classification or approval.
|