Add deploy/Dockerfile and .dockerignore (ADR 0044): one image for the
api service and every operator-run CLI, built from an already-assembled
workspace (the platform plus whatever modules/<id> the context holds).
- node:24.18.0-trixie-slim pinned by index digest; pnpm via Corepack at
the root packageManager version.
- Build stage: frozen install, modules:validate, migrations:validate,
build. Runtime: production-only node_modules plus every dist/, core
migrations, and each module directory, in the repository layout the
API and CLIs resolve paths from; root-owned, run as node.
- NODE_ENV=production and EKKLESIA_API_HOST=0.0.0.0 inside the image
only; the platform default stays 127.0.0.1. Configuration comes only
from the environment; no .env enters the context or any layer.
- HEALTHCHECK on /api/v1/health/ready with Node's fetch; OCI labels for
source, revision, and version.
- CI builds the image without modules: build only, no credentials, no
push.
- deploy/README.md documents the image, the read-only production
posture (verified), init: true, and the exact CLI commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The security document claimed the CI workflow "is least privilege" because it
declares `permissions: contents: read`. That is not true on the server this
repository actually uses.
The inventoried server is Gitea 1.24.2, and its official comparison page lists
`permissions` / `jobs.<job_id>.permissions`, `concurrency`, and
`jobs.<job_id>.timeout-minutes` under unsupported workflow syntax — verified
against https://docs.gitea.com/1.24/usage/actions/comparison rather than assumed.
On this server those keys are inert, so the branch cannot claim the in-job
GITEA_TOKEN is read-only, and cannot claim YAML-enforced timeout or concurrency
behavior. Job-token permission controls, repository/owner defaults, and
server-side maximum-permission clamping arrive in the 1.26 feature line
(https://docs.gitea.com/1.26/usage/actions/token-permissions).
The document now separates what is actually true of this repository — full-SHA
action pins, frozen-lockfile install, no pull_request_target, no referenced
secret, `persist-credentials: false` on every checkout, and the declared
`permissions` block — from what the server enforces. It also states precisely
what `persist-credentials: false` proves: checkout leaves no credential in
`.git/config` for a later step, which is not the same as removing or reducing
the job token. The CI blockers are recorded: upgrade to a reviewed supported
release, configure repository/owner defaults and a maximum permission, prove
against the real server what the token cannot do, move timeout/cancellation/
teardown into the trusted provisioner and reaper, and finish the disposable-guest
proof before opening the real pull request.
The workflow keeps every declaration — they are already correct for a supported
Gitea release and for GitHub Actions — and gains a comment saying plainly that
this server ignores them, so no future reader mistakes them for enforcement.
Nothing about runners, proxies, firewalls, or deployment is asserted here.
Co-Authored-By: Claude Opus 5 <[email protected]>
`actions/checkout` defaults to `persist-credentials: true`, which writes the job
token into `.git/config` and leaves it there for every later step. Nothing in
this workflow pushes, so the credential has no use after checkout — but a pull
request can edit this file, and any step it adds runs in the same workspace.
All three jobs now check out with `persist-credentials: false`. The rest of the
workflow's posture is unchanged and was re-verified: top-level
`permissions: contents: read`, both actions pinned by full commit SHA,
`pnpm install --frozen-lockfile`, no `pull_request_target`, and no secrets
referenced anywhere.
This is the one property from the Pass 5 runner-isolation requirements that is
repository-local and statically verifiable. The rest — whether pull-request jobs
run on a disposable guest, what Docker daemon they reach, and which runners this
repository can select — depends on the actual Gitea and runner hosts and is not
claimed here.
Co-Authored-By: Claude Opus 5 <[email protected]>