feat: one generic image, with installed modules in installation-owned storage (ADR 0048)
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
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
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4d28a82937
commit
e36b70d8d4
@@ -8,10 +8,11 @@ import {
|
||||
diagnoseEkklesia,
|
||||
formatDoctorReport,
|
||||
} from "../src/diagnostics/index.js";
|
||||
import type {
|
||||
DiscoveredModule,
|
||||
ModuleMigrationAggregate,
|
||||
ModuleSchemaAggregate,
|
||||
import {
|
||||
type DiscoveredModule,
|
||||
discoverModules,
|
||||
type ModuleMigrationAggregate,
|
||||
type ModuleSchemaAggregate,
|
||||
} from "../src/modules/index.js";
|
||||
|
||||
/**
|
||||
@@ -34,8 +35,8 @@ const moduleSecret = "module-secret-material";
|
||||
|
||||
const metadata = Object.freeze({
|
||||
moduleSdkVersion: "0.1.0",
|
||||
modulesDirectory: "/not-rendered/modules",
|
||||
platformVersion: "0.1.0",
|
||||
repositoryRoot: "/not-rendered",
|
||||
runtimeRange: ">=24.11.0 <25",
|
||||
runtimeVersion: "24.15.0",
|
||||
});
|
||||
@@ -454,3 +455,33 @@ describe("Doctor host-configuration parity — sanitization", () => {
|
||||
expect(JSON.stringify(report)).not.toContain("S3_ACCESS_KEY");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Doctor reads the modules directory from the same host configuration (ADR 0048)", () => {
|
||||
it("uses the repository's own modules directory when EKKLESIA_MODULES_DIR is unset", () => {
|
||||
const doctorOptions = createDoctorOptions(environment(), metadata);
|
||||
expect(doctorOptions.moduleDiscovery.modulesDirectory).toBe("/not-rendered/modules");
|
||||
expect(doctorOptions.moduleDiscovery.modulesDirectoryRequired).toBe(false);
|
||||
expect(doctorOptions.coreMigrationsDirectory).toBe("/not-rendered/core/drizzle");
|
||||
});
|
||||
|
||||
it("uses a configured directory, and still finds Core's migrations under the platform", () => {
|
||||
const doctorOptions = createDoctorOptions(
|
||||
environment({ EKKLESIA_MODULES_DIR: "/var/lib/ekklesia/modules" }),
|
||||
metadata,
|
||||
);
|
||||
expect(doctorOptions.moduleDiscovery.modulesDirectory).toBe("/var/lib/ekklesia/modules");
|
||||
expect(doctorOptions.moduleDiscovery.modulesDirectoryRequired).toBe(true);
|
||||
expect(doctorOptions.coreMigrationsDirectory).toBe("/not-rendered/core/drizzle");
|
||||
});
|
||||
|
||||
it("hands an unusable value to discovery to report, rather than throwing before a report exists", async () => {
|
||||
const doctorOptions = createDoctorOptions(
|
||||
environment({ EKKLESIA_MODULES_DIR: "relative/private-mount-91aa" }),
|
||||
metadata,
|
||||
);
|
||||
expect(doctorOptions.moduleDiscovery.modulesDirectoryRequired).toBe(true);
|
||||
const report = await diagnoseEkklesia(doctorOptions, dependencies({ discoverModules }));
|
||||
expect(report.checks.find((entry) => entry.id === "modules.discovery")?.status).toBe("failure");
|
||||
expect(JSON.stringify(report)).not.toContain("private-mount-91aa");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user