Files
register-referentie/docs/runbooks/gitea-actions-gotchas.md
Niek Otten dda4c58e1c
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
fix(infra): portable health poll instead of compose --wait (refs #30)
`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>
2026-06-25 10:57:52 +02:00

5.6 KiB

Gitea Actions gotchas

Known differences between Gitea Actions (our CI) and a plain local run, and the workarounds we adopted. Referenced by CLAUDE.md §8.7 and §15.

Bind mounts don't reach Compose services on the hosted runner

Symptom. make smoke is green locally but the compose-smoke CI job fails with the OpenZaak init container exiting 1:

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

Migrations run fine; only the step that reads a mounted file fails. The same class of failure hits any service that bind-mounts a workspace path — nrc-init (its data.yaml), flowable-init (the BPMN), keycloak (the realm import dir).

Cause. The ubuntu-latest runner executes the whole job inside a container (docker.gitea.com/runner-images:ubuntu-latest). When the job then runs docker compose ... up, Compose talks to the host's Docker daemon and starts the stack as sibling containers. A relative bind mount such as

volumes:
  - ./openzaak/setup_configuration:/app/setup_configuration:ro

is resolved by Compose to an absolute path inside the job container (/workspace/eho/register-referentie/infra/openzaak/setup_configuration). The daemon then looks for that path on its own host, doesn't find it, and auto-creates an empty directory to mount. The container starts with an empty mount point, so the file appears "missing".

This is the classic Docker-in-Docker / sibling-container bind-mount trap. It does not happen on a runner that executes jobs directly on the host (the previous self-hosted respellion-linux setup), which is why switching to ubuntu-latest exposed it.

Fix: the upstream images are used verbatim (no build); config is streamed into external named volumes with docker cp. infra/seed-config.sh creates a fixed- name volume per asset, runs a throwaway helper container that mounts it, and docker cps the files in. docker cp streams bytes over the Docker API, so it works no matter where the daemon runs (including Docker-in-Docker). The services then mount those volumes:

Asset External volume Mounted by → at
OpenZaak setup_configuration/data.yaml rr-oz-config oz-init/app/setup_configuration
Keycloak realm exports rr-kc-realms keycloak/opt/keycloak/data/import
workflows/registratie.bpmn rr-fl-bpmn flowable-init/work

The volumes are declared external: true with fixed name:s so they resolve identically under docker compose and podman-compose. The seed step (make runs it before every up) recreates them fresh each time; make down / the per-service *-down targets remove them. Open Notificaties needs no config at all — nrc-init runs migrations only.

Two hard constraints drove this design:

  • Use plain docker volume create / docker run / docker cpnot docker compose create, which podman-compose (the local dev runtime) does not implement.
  • docker cp rather than a bind mount of the source dir, because that bind mount is exactly what fails on the containerized runner.

Consequence: bare docker compose up no longer self-seeds — the external volumes must be populated first, so use make up (or make <svc>-up), which seeds then starts. CI uses make smoke, which does the same.

Why not the alternatives.

  • Bake into a (possibly inline) image — clean and portable, but it's a build; rejected here because the goal was to use the upstream images verbatim.
  • Compose configs: with inline content — Compose materialises these as a temp file on the client side and bind-mounts it → same daemon-can't-see-it problem.
  • A self-hosted runner that runs jobs on the host — bind mounts would then work with zero seeding, but it reintroduces a bespoke runner and undoes the move to the hosted ubuntu-latest label.

No bind mounts of these config files remain, so the SELinux :z/:Z relabel flag is no longer needed anywhere in infra/ (named volumes don't need relabeling).

Readiness: a portable health poll, not docker compose up --wait

The smoke does not use docker compose up --wait, for three reasons:

  • podman-compose doesn't implement --wait (unrecognized arguments: --wait), so it would break local dev.
  • A whole-project --wait fails when a one-shot with no service_completed_successfully dependant exitsflowable-init deploys the BPMN and exits 0, which --wait treats as the project failing (symptom: last compose line container infra-flowable-init-1 exited (0)).
  • The containerized CI runner can't reach published host ports, so an external curl localhost:8080/health doesn't work either.

Fix. infra/wait-healthy.sh polls each durable, health-checked service (openzaak nrc-web acl bffWAIT_SVCS in the Makefile) with docker ps + docker inspect '{{.State.Health.Status}}', waiting for healthy. That uses only primitives both docker compose and podman-compose support, reads the in-container healthcheck (no host port needed), and ignores the one-shots (they only need to have run). WAIT_TIMEOUT (default 420 s) covers the cold OpenZaak migrate (~90 s) plus app start.

PostGIS readiness vs. pg_isready

pg_isready reports the server is accepting connections as soon as the TCP port is open — before the postgis/postgis image has finished running its CREATE EXTENSION postgis init scripts. An init container that starts migrating in that window can fail on a missing PostGIS. The db healthchecks therefore add a SELECT PostGIS_Version() probe so dependents wait for the extension, not just the port.