Files
Ekklesia/scripts/lib/test-module-package.Dockerfile
T
ekklesiaandClaude Opus 5.5 e36b70d8d4
CI / verify (pull_request) Failing after 47s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped
feat: one generic image, with installed modules in installation-owned storage (ADR 0048)
The production image now contains zero feature modules and is the same for every
installation. Installed modules live in module storage that one Core resolver
selects from EKKLESIA_MODULES_DIR: the checkout's modules/ when unset, an
absolute path that must exist when set (/var/lib/ekklesia/modules in the image).
A relative, blank, missing, or unreadable configured directory fails closed with
a sanitized error that never contains the value.

- Host configuration reads EKKLESIA_MODULES_DIR. The API, migration plan, apply
  and validate, module validation, module enablement, and Doctor all resolve it
  the same way; the test-only API override still takes precedence.
- Doctor receives Core's migrations directory explicitly instead of deriving it
  from the modules directory's parent.
- The migration runner retains and reports the applied history of a module that
  is not installed, and plans and applies Core's and present modules' migrations
  around it. Core's history and every installed module's history stay strict: a
  missing or changed applied migration, or a downgrade, still fails. No down
  migrations, no data deletion.
- deploy/Dockerfile copies no module and installs nothing for the module
  template; .dockerignore excludes modules/ and the build refuses a context that
  contains it; the runtime has an empty root-owned /var/lib/ekklesia/modules and
  sets EKKLESIA_MODULES_DIR.
- deploy/compose.yaml bind-mounts EKKLESIA_MODULES_HOST_DIR read-only into api
  and migrate, with create_host_path disabled.
- scripts/check-image.sh proves the image holds no modules and that a populated
  modules/ directory leaves its runtime file system identical; CI runs it.
- The split deploy smoke now uses the same generic image: it boots with no
  modules, installs a self-contained test module packaged from the template
  (test-only), migrates and restarts, removes it (Core runs, planning reports the
  retained history, data survives), and restores it with its data.

Docs: deploy README (module storage, install/update/remove/restore, the runtime
modules target), .env.example, API configuration, module development guide,
testing, workflow, security, and the architecture overview.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 08:38:14 -05:00

51 lines
2.8 KiB
Docker

# TEST-ONLY. Builds the repository's module template as one self-contained installed-module directory
# for scripts/smoke-deploy.sh, which installs it into a deployment's module storage (ADR 0048). It is
# not a module package format and defines none: the archive format, signing, and transport of real
# module packages are deferred. No image is produced; the build's only output is the directory:
#
# DOCKER_BUILDKIT=1 docker build --file scripts/lib/test-module-package.Dockerfile \
# --output type=local,dest=<directory> .
#
# writes <directory>/example: the module's package.json, manifest, built artifacts, migrations, and a
# node_modules holding exactly its production dependencies, including its own copies of the Module
# SDK, the contracts, and Drizzle. Nothing in it resolves to anything outside it, because the
# platform lends an installed module nothing.
#
# The same pinned Node.js base as deploy/Dockerfile, so the package targets the runtime it is
# installed into. Drizzle comes from the platform's own lockfile, which is what keeps it the exact
# version the platform uses: a constraint of the current implementation (ADR 0048), not a contract.
FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS build
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
PNPM_HOME=/pnpm \
TURBO_TELEMETRY_DISABLED=1 \
DO_NOT_TRACK=1
WORKDIR /opt/source
COPY package.json ./
RUN corepack enable && corepack install
COPY . .
RUN --mount=type=cache,id=ekklesia-pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --store-dir /pnpm/store
RUN pnpm exec turbo run build --filter=@ekklesia/module-template
# `--legacy` deploys from this workspace without switching it to injected workspace packages; the
# Module SDK and every other workspace dependency are still copied into the package, built.
RUN --mount=type=cache,id=ekklesia-pnpm-store,target=/pnpm/store \
set -eu; \
pnpm --filter @ekklesia/module-template deploy --prod --legacy --store-dir /pnpm/store \
/package/example; \
rm -f /package/example/README.md; \
# pnpm's hoisted self-reference points back into this workspace; an installed module has none.
rm -f /package/example/node_modules/.pnpm/node_modules/@ekklesia/module-template; \
escaping=""; \
for link in $(find /package/example -type l); do \
case "$(realpath -m "$link")" in \
/package/example/*) ;; \
*) escaping="$escaping $link" ;; \
esac; \
done; \
if [ -n "$escaping" ]; then echo "Links escape the package:$escaping" >&2; exit 1; fi; \
chmod -R a+rX,go-w /package/example; \
cd /package/example && node -e 'import("./dist/server/index.js").then((m) => { if (m.default.id !== "example") process.exit(1); })'
FROM scratch AS package
COPY --from=build /package/ /