docs: propose ADR 0047, a published harness every module runs in CI
Proposal only: no implementation in this PR. It is step 2 of the module CI plan. The implementation PR follows once the decision is accepted.
Why
Signage reached production with three defects that appear only when it runs inside Ekklesia in a real browser:
every heartbeat was refused with a 400;
a display that failed one request at start-up never recovered without a reload;
a blob leak exhausted the browser.
Its tests stubbed the host, and it had no CI. The platform smoke runs only the template. Each module repository would otherwise copy deployment scripts that then drift.
What it decides
A versioned harness image, ekklesia-module-harness:<platform version>, built from the release tag and pushed by the platform's release CI (v* tags only, never overwritten). Modules pin it by digest.
What it does:
assembles the module into that platform;
runs the platform's own gates (boundaries, modules:validate, migrations:validate, build);
builds the image, starts the split reference deployment, and explicitly migrates a fresh database;
mounts every declared UI surface in headless Chromium: the primary UI, the display as a paired device, and contributions.
What fails it:
console errors, unhandled rejections, unexpected 4xx/5xx responses, and refused requests;
a 2-minute display soak: remounts, offline state, realtime health, live object URLs, heap growth;
generic no-dead-ends injections: the first request to each start-up route, and the first realtime connection, are refused, and the display must recover without a reload.
Both ends of the range: a module tests the lowest and newest platform versions in compatibility.platform. Both are pinned, with a lockfile per version that the harness can regenerate.
Extra smoke steps go in ekklesia.harness.json, beside the manifest. This is a deliberate deviation from "in their manifest": manifest versions are strict, host-validated hard cutovers (ADR 0033), and test steps have no runtime meaning.
Later: the module-trust check. Recorded table, bucket and egress access, compared with what the manifest declares. Egress needs a manifest declaration that doesn't exist yet, and is a separate decision.
The template workflow stays short: one docker run of the harness, by digest.
Gitea 1.24 findings behind the "docker run, not a reusable workflow" choice
Gitea 1.24's runner can call a reusable workflow from another repository by cloning it, but that support is thinly documented and has known problems in practice (forum, reported failures).
Actions and reusable workflows from private repositories arrive only with Gitea 1.26 (go-gitea/gitea#32562).
Absolute uses: URLs for actions are supported on 1.24 (Gitea 1.24 comparison). A composite action would still only wrap the same docker run.
An image digest is equally immutable and reviewable, and works unchanged on 1.24, 1.26 and GitHub Actions.
Numbering
This ADR takes 0047 because it is written first. The display-presence/duplicate-pages ADR and the device-lifecycle ADR discussed in M0 become 0048 and 0049.
## docs: propose ADR 0047, a published harness every module runs in CI
**Proposal only: no implementation in this PR.** It is step 2 of the module CI plan. The implementation PR follows once the decision is accepted.
### Why
Signage reached production with three defects that appear only when it runs **inside** Ekklesia in a real browser:
- every heartbeat was refused with a `400`;
- a display that failed one request at start-up never recovered without a reload;
- a blob leak exhausted the browser.
Its tests stubbed the host, and it had no CI. The platform smoke runs only the template. Each module repository would otherwise copy deployment scripts that then drift.
### What it decides
- **A versioned harness image**, `ekklesia-module-harness:<platform version>`, built from the release tag and pushed by the platform's release CI (`v*` tags only, never overwritten). Modules pin it **by digest**.
- **What it does:**
- assembles the module into that platform;
- runs the platform's own gates (boundaries, `modules:validate`, `migrations:validate`, build);
- builds the image, starts the split reference deployment, and explicitly migrates a fresh database;
- mounts **every declared UI surface** in headless Chromium: the primary UI, the display as a paired device, and contributions.
- **What fails it:**
- console errors, unhandled rejections, unexpected 4xx/5xx responses, and refused requests;
- a 2-minute display soak: remounts, offline state, realtime health, live object URLs, heap growth;
- generic **no-dead-ends** injections: the first request to each start-up route, and the first realtime connection, are refused, and the display must recover without a reload.
- **Both ends of the range:** a module tests the lowest and newest platform versions in `compatibility.platform`. Both are pinned, with a lockfile per version that the harness can regenerate.
- **Extra smoke steps** go in `ekklesia.harness.json`, beside the manifest. This is a deliberate deviation from "in their manifest": manifest versions are strict, host-validated hard cutovers (ADR 0033), and test steps have no runtime meaning.
- **Later: the module-trust check.** Recorded table, bucket and egress access, compared with what the manifest declares. Egress needs a manifest declaration that doesn't exist yet, and is a separate decision.
- **The template workflow** stays short: one `docker run` of the harness, by digest.
### Gitea 1.24 findings behind the "`docker run`, not a reusable workflow" choice
- Gitea 1.24's runner can call a reusable workflow from another repository by cloning it, but that support is thinly documented and has known problems in practice ([forum](https://forum.gitea.com/t/reusable-workflows-usability/10155), [reported failures](https://forum.gitea.com/t/simple-reusable-workflow-example-not-working/11960)).
- Actions and reusable workflows from **private** repositories arrive only with Gitea 1.26 ([go-gitea/gitea#32562](https://github.com/go-gitea/gitea/pull/32562)).
- Absolute `uses:` URLs for actions are supported on 1.24 ([Gitea 1.24 comparison](https://docs.gitea.com/1.24/usage/actions/comparison)). A composite action would still only wrap the same `docker run`.
- An image digest is equally immutable and reviewable, and works unchanged on 1.24, 1.26 and GitHub Actions.
### Numbering
This ADR takes 0047 because it is written first. The display-presence/duplicate-pages ADR and the device-lifecycle ADR discussed in M0 become 0048 and 0049.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Approve in principle. Please add before accepting:
Trust boundary: the harness needs the Docker socket (effectively root on the runner). First-party modules may run on the shared runner; untrusted third-party modules only on an isolated runner (separate VM or sandboxed/rootless Docker). State this in the decision.
Expected errors: expected refusals and 4xx (injected failures, pre-pairing 401) are declared in ekklesia.harness.json; anything undeclared fails.
Flakiness policy: document the soak and heap thresholds; no automatic retries that mask failures; flaky checks get quarantined with an issue.
Cleanup guarantee: the harness tears down everything it starts, even on failure or cancellation, with labels that allow a sweep of leftovers.
Optional: a nightly run against platform main as an early warning.
Resources: document runner requirements (memory; one harness job at a time).
Approve in principle. Please add before accepting:
1. **Trust boundary:** the harness needs the Docker socket (effectively root on the runner). First-party modules may run on the shared runner; untrusted third-party modules only on an isolated runner (separate VM or sandboxed/rootless Docker). State this in the decision.
2. **Expected errors:** expected refusals and 4xx (injected failures, pre-pairing 401) are declared in ekklesia.harness.json; anything undeclared fails.
3. **Flakiness policy:** document the soak and heap thresholds; no automatic retries that mask failures; flaky checks get quarantined with an issue.
4. **Cleanup guarantee:** the harness tears down everything it starts, even on failure or cancellation, with labels that allow a sweep of leftovers.
5. **Optional:** a nightly run against platform main as an early warning.
6. Resources: document runner requirements (memory; one harness job at a time).
Signage reached production with three defects that appear only inside
Ekklesia in a real browser, and no CI that ran it there. The ADR
proposes:
- a versioned harness image, ekklesia-module-harness:<platform version>,
built and pushed by the platform's release CI;
- what the harness does: assemble a module into that platform, validate
and build it, migrate a disposable database, start the split reference
deployment, and mount every declared UI surface in headless Chromium;
- what fails it: console errors, unexpected 4xx/5xx, unhandled
rejections, a display soak, and generic no-dead-ends injections;
- testing at the lowest and newest platform versions of a module's
range;
- extra smoke steps in ekklesia.harness.json, beside the manifest
rather than in it;
- a later data-access and egress check against declarations;
- a short template workflow that calls the harness by digest with one
`docker run`. Not a reusable workflow: thinly supported on Gitea
1.24, and private sources only arrive in 1.26.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A job the runner kills never runs its own cleanup trap, and the
platform's CI leaked a whole migration stack that way. The harness
follows the platform's three-layer rule:
- everything it starts is named and labeled as a CI stack;
- it cleans up in a trap;
- the template workflow sweeps stale CI stacks before running it, and
removes its own in a final if: always() step.
The sweep ships in the harness image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
docs: propose ADR 0047, a published harness every module runs in CI
Proposal only: no implementation in this PR. It is step 2 of the module CI plan. The implementation PR follows once the decision is accepted.
Why
Signage reached production with three defects that appear only when it runs inside Ekklesia in a real browser:
400;Its tests stubbed the host, and it had no CI. The platform smoke runs only the template. Each module repository would otherwise copy deployment scripts that then drift.
What it decides
ekklesia-module-harness:<platform version>, built from the release tag and pushed by the platform's release CI (v*tags only, never overwritten). Modules pin it by digest.modules:validate,migrations:validate, build);compatibility.platform. Both are pinned, with a lockfile per version that the harness can regenerate.ekklesia.harness.json, beside the manifest. This is a deliberate deviation from "in their manifest": manifest versions are strict, host-validated hard cutovers (ADR 0033), and test steps have no runtime meaning.docker runof the harness, by digest.Gitea 1.24 findings behind the "
docker run, not a reusable workflow" choiceuses:URLs for actions are supported on 1.24 (Gitea 1.24 comparison). A composite action would still only wrap the samedocker run.Numbering
This ADR takes 0047 because it is written first. The display-presence/duplicate-pages ADR and the device-lifecycle ADR discussed in M0 become 0048 and 0049.
🤖 Generated with Claude Code
Approve in principle. Please add before accepting:
f005893c13to3827841205