ci(infra): Gitea Actions CI pipeline + full-stack compose smoke (closes #30) #50

Merged
not merged 12 commits from ci/30-gitea-actions-ci into main 2026-06-25 12:34:44 +00:00
Contributor

Summary

Establishes the Gitea Actions CI pipeline for slice S-00-c and the full development stack that its compose-smoke job exercises.

CI workflow — .gitea/workflows/ci.yaml

Four jobs — lint, build, unit, compose-smoke — on the hosted ubuntu-latest runner (switched from the self-hosted respellion-linux label). Each job invokes the matching make target, so make ci locally runs exactly what CI runs (no drift). compose-smoke dumps container logs and tears the stack down on failure. uses: are absolute, tag-pinned URLs per CLAUDE.md §8.7/§15.

Full development stack — infra/docker-compose.yml

Consolidates OpenZaak, Open Notificaties (NRC), Keycloak, Flowable and our ACL + BFF into one stack. Upstream images are used verbatim and pinned (OpenZaak 1.28.2, Open Notificaties 1.16.1, Keycloak 26.1, PostGIS 17-3.5); only ACL and BFF are built from this repo.

Config delivery without bind mounts (the core problem this slice solved)

The ubuntu-latest runner executes the job inside a container, so docker compose starts the stack as sibling containers via the host daemon. A workspace bind mount then resolves to a path the daemon can't see and is mounted empty — which is why oz-init kept failing with data.yaml does not exist. Fix:

  • infra/seed-config.sh streams config into external named volumes with docker cp (which works in Docker-in-Docker because it streams over the API): rr-oz-configsetup_configuration/data.yaml, rr-kc-realms ← the four realm exports, rr-fl-bpmnregistratie.bpmn. It uses plain docker create/cp (not docker compose create, which podman-compose lacks), so the same mechanism works on the CI runner and on a local podman-compose box.
  • Open Notificaties needs no confignrc-init runs migrations only (the OZ↔NRC notification wiring is deferred to S-06), which also avoids NRC's setup_configuration aborting with "No steps enabled" on an empty config.

Portable readiness — infra/wait-healthy.sh

Polls each durable service's healthcheck via docker ps + docker inspect '{{.State.Health.Status}}'. This replaces docker compose up --wait (not implemented by podman-compose) and an external curl localhost (the containerized runner can't reach published host ports), and it sidesteps --wait failing when a consumer-less one-shot (flowable-init) exits 0.

Hardening

  • PostGIS-aware db healthcheck — waits for SELECT PostGIS_Version(), not just an open TCP port, so init containers don't race the extension setup.
  • Pinned image tags (no :latest drift); on-failure log capture in CI.

Per-service stacks

infra/{openzaak,opennotificaties,keycloak,flowable}/docker-compose.yml were updated to the same mechanism (pure upstream images, external config volumes, migrations-only nrc-init, PostGIS healthcheck) so local make <svc>-up / -smoke stay consistent with the consolidated stack.

New service build

services/acl/Dockerfile + .dockerignore — multi-stage .NET 10 build. Without these a fresh checkout cannot build the ACL image and compose-smoke fails immediately.

Docs

  • docs/runbooks/gitea-actions-gotchas.md (new) — the DinD bind-mount trap, the docker cp-into-volumes fix, the portable health poll, and PostGIS readiness.
  • docs/runbooks/ci.md — rewritten for the ubuntu-latest runner and the pipeline.
  • docs/runbooks/openzaak.md — config-via-volume and pinned tag.

Verification

CI (ubuntu-latest), run 32 / commit dda4c58:

Job Result
lint
build
unit
compose-smoke

Local (rootless podman-compose):

Check Result
make smoke full stack up, openzaak / nrc-web / acl / bff all healthy, clean teardown

Notes & follow-ups

  • Bare docker compose up no longer self-seeds the external config volumes — use make up (seeds, then starts). Documented in the gotchas runbook.
  • OZ↔NRC notification wiring (setup_configuration steps for NRC) lands in S-06; nrc-init is migrations-only until then.

closes #30

🤖 Generated with Claude Code

## Summary Establishes the Gitea Actions CI pipeline for slice **S-00-c** and the full development stack that its `compose-smoke` job exercises. ### CI workflow — `.gitea/workflows/ci.yaml` Four jobs — `lint`, `build`, `unit`, `compose-smoke` — on the hosted **`ubuntu-latest`** runner (switched from the self-hosted `respellion-linux` label). Each job invokes the matching `make` target, so `make ci` locally runs exactly what CI runs (no drift). `compose-smoke` dumps container logs and tears the stack down on failure. `uses:` are absolute, tag-pinned URLs per CLAUDE.md §8.7/§15. ### Full development stack — `infra/docker-compose.yml` Consolidates OpenZaak, Open Notificaties (NRC), Keycloak, Flowable and our ACL + BFF into one stack. **Upstream images are used verbatim and pinned** (OpenZaak `1.28.2`, Open Notificaties `1.16.1`, Keycloak `26.1`, PostGIS `17-3.5`); only ACL and BFF are built from this repo. ### Config delivery without bind mounts (the core problem this slice solved) The `ubuntu-latest` runner executes the job **inside a container**, so `docker compose` starts the stack as **sibling containers** via the host daemon. A workspace bind mount then resolves to a path the daemon can't see and is mounted **empty** — which is why `oz-init` kept failing with `data.yaml does not exist`. Fix: - **`infra/seed-config.sh`** streams config into **external named volumes** with `docker cp` (which works in Docker-in-Docker because it streams over the API): `rr-oz-config` ← `setup_configuration/data.yaml`, `rr-kc-realms` ← the four realm exports, `rr-fl-bpmn` ← `registratie.bpmn`. It uses plain `docker create`/`cp` (not `docker compose create`, which **podman-compose** lacks), so the same mechanism works on the CI runner and on a local podman-compose box. - **Open Notificaties needs no config** — `nrc-init` runs migrations only (the OZ↔NRC notification wiring is deferred to S-06), which also avoids NRC's `setup_configuration` aborting with "No steps enabled" on an empty config. ### Portable readiness — `infra/wait-healthy.sh` Polls each durable service's healthcheck via `docker ps` + `docker inspect '{{.State.Health.Status}}'`. This replaces `docker compose up --wait` (not implemented by podman-compose) and an external `curl localhost` (the containerized runner can't reach published host ports), and it sidesteps `--wait` failing when a consumer-less one-shot (`flowable-init`) exits 0. ### Hardening - PostGIS-aware db healthcheck — waits for `SELECT PostGIS_Version()`, not just an open TCP port, so init containers don't race the extension setup. - Pinned image tags (no `:latest` drift); on-failure log capture in CI. ### Per-service stacks `infra/{openzaak,opennotificaties,keycloak,flowable}/docker-compose.yml` were updated to the same mechanism (pure upstream images, external config volumes, migrations-only `nrc-init`, PostGIS healthcheck) so local `make <svc>-up` / `-smoke` stay consistent with the consolidated stack. ### New service build `services/acl/Dockerfile` + `.dockerignore` — multi-stage .NET 10 build. Without these a fresh checkout cannot build the ACL image and `compose-smoke` fails immediately. ### Docs - **`docs/runbooks/gitea-actions-gotchas.md`** (new) — the DinD bind-mount trap, the `docker cp`-into-volumes fix, the portable health poll, and PostGIS readiness. - `docs/runbooks/ci.md` — rewritten for the `ubuntu-latest` runner and the pipeline. - `docs/runbooks/openzaak.md` — config-via-volume and pinned tag. ## Verification **CI (`ubuntu-latest`), run 32 / commit `dda4c58`:** | Job | Result | |---|---| | `lint` | ✅ | | `build` | ✅ | | `unit` | ✅ | | `compose-smoke` | ✅ | **Local (rootless podman-compose):** | Check | Result | |---|---| | `make smoke` | ✅ full stack up, `openzaak` / `nrc-web` / `acl` / `bff` all healthy, clean teardown | ## Notes & follow-ups - Bare `docker compose up` no longer self-seeds the external config volumes — use **`make up`** (seeds, then starts). Documented in the gotchas runbook. - OZ↔NRC notification wiring (`setup_configuration` steps for NRC) lands in **S-06**; `nrc-init` is migrations-only until then. closes #30 🤖 Generated with [Claude Code](https://claude.com/claude-code)
not added 1 commit 2026-06-24 07:29:02 +00:00
ci(infra): ACL Dockerfile + full compose stack for smoke test (refs #30)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
CI / compose-smoke (pull_request) Has been cancelled
eaca611842
Adds the ACL multi-stage Dockerfile and .dockerignore, and expands
infra/docker-compose.yml from the BFF-only stub to the full development
stack (OpenZaak, NRC, Keycloak, Flowable, ACL, BFF).  Without these
files a fresh checkout cannot satisfy `make smoke`'s `docker compose
up --build --wait` step, so `make ci` could never go green.

`make lint && make build && make unit` verified green locally.
`make smoke` requires Docker Compose v2 (`--wait` flag); on this dev box
only podman-compose is available — smoke will be verified on the
respellion-linux CI runner once it is registered (see docs/runbooks/ci.md).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
not added 1 commit 2026-06-24 07:32:48 +00:00
ci(infra): switch runner label to ubuntu-latest (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 1m27s
CI / build (pull_request) Successful in 48s
CI / unit (pull_request) Successful in 47s
CI / compose-smoke (pull_request) Failing after 3m47s
f32fc4e8c0
Self-hosted respellion-linux runner not required — Gitea's hosted
ubuntu-latest runner has Docker + Compose v2 out of the box, so
make smoke works without any manual registration step.

Updates docs/runbooks/ci.md to reflect the new runner label and
removes the act_runner self-hosted setup as the primary path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
not requested review from eho 2026-06-24 07:33:48 +00:00
not added 1 commit 2026-06-24 07:46:43 +00:00
fix(infra): pin OpenZaak/NRC image tags; add smoke log capture on failure (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 55s
CI / build (pull_request) Successful in 44s
CI / unit (pull_request) Successful in 46s
CI / compose-smoke (pull_request) Failing after 1m27s
8528664660
latest bumped to OpenZaak 1.29.0 (2026-06-18) and open-notificaties
updated (2026-06-22), breaking oz-init in compose-smoke.  Pin all four
compose files to stable patch releases:

  open-zaak:            1.28.2  (was :latest -> 1.29.0)
  open-notificaties:    1.16.1  (was :latest)

Tags are still overridable via OPENZAAK_TAG / OPENNOTIFICATIES_TAG env vars.

Also adds two if: failure() steps to the compose-smoke CI job: one that
dumps the last 100 lines of oz-init / nrc-init / acl / bff logs, and one
that tears the stack down cleanly, so future failures are self-diagnosing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
not added 1 commit 2026-06-24 09:55:21 +00:00
fix(infra): harden oz-db healthcheck and raise compose-up timeout (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 52s
CI / build (pull_request) Successful in 44s
CI / unit (pull_request) Successful in 45s
CI / compose-smoke (pull_request) Failing after 1m53s
88de47d1bb
Three root-cause fixes for the oz-init CI failure:

1. Smoke timeout: add --wait-timeout 300 to `docker compose up --wait`
   so CI has 5 minutes instead of the 60-second default in older Compose
   v2 releases (migrations alone take 50 s locally).

2. PostGIS race: the old healthcheck used pg_isready which only checks
   TCP connectivity — it passes before the postgis/postgis init scripts
   have run SELECT PostGIS_Version(). The new check adds a psql probe so
   oz-init does not start until PostGIS is actually available.

3. Remove :z from volume mounts: the SELinux re-label flag is
   Podman/Fedora-specific and a no-op (or unexpected) under Docker on
   ubuntu-latest; plain :ro is correct for both runtimes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
not added 1 commit 2026-06-24 11:07:02 +00:00
fix(infra): bake config into images so compose-smoke passes on CI (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 40s
CI / unit (pull_request) Successful in 45s
CI / compose-smoke (pull_request) Failing after 3m31s
9ff7937055
Root cause of the compose-smoke failure (found in the runner logs):

  oz-init-1 | CommandError: Yaml file
              `/app/setup_configuration/data.yaml` does not exist.

The ubuntu-latest runner runs the job inside a container, so
`docker compose up` starts the stack as SIBLING containers via the host
daemon. A relative bind mount (./openzaak/setup_configuration) resolves to
a path inside the job container that the daemon can't see, so Docker mounts
an empty dir and the init container can't find data.yaml. The same trap hit
nrc-init (data.yaml), flowable-init (the BPMN) and keycloak (realm import).

Fix: bake the assets into small derived images instead of bind-mounting:
  - infra/openzaak/Dockerfile        -> register-referentie/openzaak:dev
  - infra/opennotificaties/Dockerfile-> register-referentie/opennotificaties:dev
  - infra/keycloak/Dockerfile        -> register-referentie/keycloak:dev
  - flowable-init: build.dockerfile_inline bakes workflows/registratie.bpmn

Base versions stay build args (OPENZAAK_TAG / OPENNOTIFICATIES_TAG), so the
pinning is unchanged. Applied to both the consolidated compose and the
per-service composes, so local Podman and CI use one mechanism — no bind
mounts, no SELinux `:z`, no world-readable requirement.

Verified locally: `podman build` of the OpenZaak and BPMN images produces
the file at the expected in-container path.

Docs: docs/runbooks/gitea-actions-gotchas.md explains the DinD bind-mount
trap and the bake fix; openzaak.md and ci.md point at it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-24 11:18:40 +00:00
fix(infra): nrc-init runs migrations only, not setup_configuration (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 49s
CI / build (pull_request) Successful in 43s
CI / unit (pull_request) Successful in 47s
CI / compose-smoke (pull_request) Failing after 3m34s
12049a0f35
With OpenZaak now coming up, nrc-init ran for the first time and failed:

  nrc-init-1 | CommandError: No steps enabled, aborting.

NRC's setup_configuration/data.yaml is intentionally empty ({}) — the
OZ<->NRC wiring is deferred to S-06 — but /setup_configuration.sh runs
`manage.py setup_configuration` regardless, and NRC 1.16.1 aborts when no
steps are enabled. (This was masked until now: oz-init failed first, so
openzaak never became healthy and nrc-init, which waits on it, never ran.)

The documented intent is "init runs migrations only", so nrc-init now runs
`manage.py migrate` directly instead of /setup_configuration.sh, and the
dead RUN_SETUP_CONFIG env is dropped from the NRC services. nrc-web still
migrates + creates the superuser itself via /start.sh.

Also:
- Makefile: bump compose `--wait-timeout` 300 -> 420. The serial
  oz-db -> oz-init -> openzaak(healthy) -> nrc-init -> nrc-web(healthy)
  chain runs ~260 s on the runner; 420 s gives comfortable headroom.
- ci.yaml: widen the on-failure log dump to oz-init, openzaak, nrc-init,
  nrc-web, flowable-init, keycloak, acl, bff for full diagnosability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 07:03:41 +00:00
fix(infra): smoke waits on durable services, not the whole project (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 50s
CI / compose-smoke (pull_request) Successful in 4m52s
a0aa22c80b
Run 28 got the full stack healthy but `compose-smoke` still failed. The last
compose line before the error was:

  container infra-flowable-init-1 exited (0)

`docker compose up --wait` treats a service that exits as a failure of the
"stay running" condition unless something depends on it via
`service_completed_successfully`. oz-init/nrc-init are fine (openzaak/nrc-web
depend on them), but flowable-init deploys the BPMN and exits 0 with no
dependant, so whole-project `--wait` failed the instant it finished — even
though everything else was healthy and nrc-init now exits 0.

Smoke now:
  1. `up -d` starts the full stack (one-shots run + deploy as before), then
  2. `up -d --wait <WAIT_SVCS>` waits only for the durable health-checked
     services (openzaak nrc-web acl bff).

Also drops the external `curl localhost:8080/health`: the containerized CI
runner can't reach published host ports at localhost, and each service's
healthcheck already runs inside its container — so `--wait` succeeding IS the
smoke. Documented in docs/runbooks/gitea-actions-gotchas.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 07:32:39 +00:00
refactor(infra): bake config via dockerfile_inline, drop Dockerfile files (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 46s
CI / compose-smoke (pull_request) Successful in 4m7s
6d8e1d0830
Replaces the three standalone Dockerfiles (openzaak, opennotificaties,
keycloak) with `build.dockerfile_inline` recipes in the compose files, so the
config bake has no separate Dockerfile artifacts to maintain. Behaviour is
identical: each derived image still COPYies its config in.

- oz-init / keycloak / flowable-init: 2-line inline Dockerfiles.
- Open Notificaties needs no bake at all now — nrc-init runs migrations only,
  so all NRC services use the plain base image (removes a whole derived image).

Why dockerfile_inline and not `docker cp` into named volumes: docker cp avoids
images entirely but needs `docker compose create`, which podman-compose (the
local dev runtime) does not implement — it would break `make openzaak-up` etc.
locally. dockerfile_inline works on both podman-compose and the CI runner
(verified both: oz-init + keycloak inline builds locally; flowable-init inline
has been green on CI since run 27).

Docs updated: gitea-actions-gotchas.md and openzaak.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 08:22:18 +00:00
refactor(infra): use upstream images verbatim, seed config via docker cp (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 49s
CI / build (pull_request) Successful in 44s
CI / unit (pull_request) Successful in 44s
CI / compose-smoke (pull_request) Successful in 4m15s
b349dff496
Drops the inline-build images for the upstream services. The compose now
references the published images directly (openzaak/open-zaak,
openzaak/open-notificaties, keycloak, curl, flowable-rest) with no build for
them, and the config they need is streamed into external named volumes by
infra/seed-config.sh:

  rr-oz-config  -> oz-init     /app/setup_configuration   (data.yaml)
  rr-kc-realms  -> keycloak    /opt/keycloak/data/import   (realm exports)
  rr-fl-bpmn    -> flowable-init /work                     (registratie.bpmn)

How: the seeder creates each volume, `docker create`s a throwaway helper that
mounts it, `docker cp`s the files in, and removes it. docker cp streams over the
Docker API, so it works in Docker-in-Docker (the CI runner) where bind mounts
mount empty. It uses plain `docker create`/`cp` — NOT `docker compose create`,
which podman-compose (local dev) lacks. `external: true` fixed names keep the
volumes identical across docker compose and podman-compose.

Consequence: bare `docker compose up` no longer self-seeds, so use `make up`
(seeds then starts). Every `*-up` target seeds first; `*-down` removes the
external volume. acl/bff are still built (they're our apps, not upstream images).

Verified end-to-end on podman-compose: `make keycloak-up` seeds rr-kc-realms,
the upstream Keycloak mounts it, and --import-realm imports all four realms
(digid realm returns 200). Seeder runs in ~2s.

Docs updated: gitea-actions-gotchas.md, ci.md, openzaak.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 08:57:56 +00:00
fix(infra): portable health poll instead of compose --wait (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 48s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 44s
CI / compose-smoke (pull_request) Successful in 3m56s
dda4c58e1c
`make smoke` errored locally because podman-compose doesn't implement
`docker compose up --wait` (`unrecognized arguments: --wait`).

Replace the `--wait` step with infra/wait-healthy.sh, which polls each durable
health-checked service ($(WAIT_SVCS)) via `docker ps` + `docker inspect
'{{.State.Health.Status}}'`. This:

- works on both docker compose (CI) and podman-compose (local) — only plain
  docker primitives, no `--wait`;
- reads the in-container healthcheck, so it needs no host port access (the CI
  runner can't reach published ports);
- ignores the one-shot init jobs, sidestepping the "--wait fails when a
  consumer-less one-shot exits 0" issue (flowable-init).

Verified on podman-compose: wait-healthy.sh reports bff healthy (rc=0); podman
exposes .State.Health.Status (starting -> healthy) and the name filter matches
both `_` and `-` container naming.

Docs: gitea-actions-gotchas.md updated (the two `--wait` sections folded into one
"portable health poll" section).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not changed title from ci(infra): ACL Dockerfile + full compose stack for make smoke (closes #30) to ci(infra): Gitea Actions CI pipeline + full-stack compose smoke (closes #30) 2026-06-25 09:19:09 +00:00
not added 1 commit 2026-06-25 10:00:34 +00:00
feat(infra): add bind-mount local compose for no-make/Windows dev (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 51s
CI / compose-smoke (pull_request) Successful in 3m59s
e87113da24
Adds infra/docker-compose.local.yml: the same full stack as the canonical
infra/docker-compose.yml, but the three config inputs (OpenZaak data.yaml,
Keycloak realms, Flowable BPMN) are bind-mounted from the repo instead of
streamed into external volumes by seed-config.sh.

Bind mounts are valid here because a local daemon (Docker Desktop on Windows/
macOS, or rootless Podman on Linux) can see the working directory — the seed
dance only exists for the containerized CI runner, where it can't. So this file
runs with a plain `docker compose up`: no make, no seed step, no bash.

  docker compose -f infra/docker-compose.local.yml up -d --build
  docker compose -f infra/docker-compose.local.yml up -d --build --wait  # Docker Desktop

Linux/macOS convenience wrappers `make local` / `make local-down` added too.
Verified on podman: Keycloak boots from this file and imports the bind-mounted
realms (digid realm returns 200). docs/runbooks/ci.md documents the Windows path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 12:00:53 +00:00
docs(infra): tighten gitea-actions-gotchas, add local compose (refs #30)
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 41s
CI / unit (pull_request) Successful in 49s
CI / compose-smoke (pull_request) Successful in 4m0s
f9e123dfcb
Restructure for scannability: a shared root-cause intro, a quick-reference
table (gotcha → fix → where), and consistent Symptom/Why/Fix sections with
tighter prose. Documents infra/docker-compose.local.yml as the no-make/Windows
path and drops the now-stale "no bind mounts remain" line (the local compose
uses them, which is fine locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not merged commit 5a83216395 into main 2026-06-25 12:34:44 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: eho/register-referentie#50