Files
register-referentie/docs/runbooks/gitea-actions-gotchas.md
Niek Otten f9e123dfcb
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
docs(infra): tighten gitea-actions-gotchas, add local compose (refs #30)
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>
2026-06-25 14:00:51 +02:00

4.8 KiB

Gitea Actions gotchas

How our CI (Gitea Actions on the hosted ubuntu-latest runner) differs from a local run, and the workarounds in this repo. Referenced by CLAUDE.md §8.7/§15.

One root cause sits under most of this: the runner executes the job inside a container, so when a step runs docker compose up, Compose starts the stack as sibling containers on the host's daemon. Anything that assumes the job and those containers share a filesystem — or a localhost — breaks.

Gotcha Fix Lives in
Bind-mounted config arrives empty docker cp config into external volumes infra/seed-config.sh
docker compose up --wait is unsupported / flaky poll health with docker inspect infra/wait-healthy.sh
pg_isready passes before PostGIS is ready add a PostGIS_Version() probe the db healthchecks

1. Bind mounts don't reach the containers

Symptom — green locally, but compose-smoke fails with:

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 trap hits nrc-init, flowable-init, and keycloak.

Why — a relative bind mount like ./openzaak/setup_configuration:/app/... is resolved by Compose to a path inside the job container (/workspace/.../setup_configuration). The daemon then looks for that path on its own host, doesn't find it, and mounts an empty directory. (It works on a runner that executes jobs on the host — which is why moving to ubuntu-latest exposed it.)

Fix — use the upstream images verbatim (no build) and stream config into external named volumes with docker cp, which copies over the Docker API and so works wherever the daemon runs. infra/seed-config.sh creates each volume, mounts it in a throwaway helper, and copies the files in:

Asset Volume Mounted at
OpenZaak data.yaml rr-oz-config oz-init:/app/setup_configuration
Keycloak realms rr-kc-realms keycloak:/opt/keycloak/data/import
registratie.bpmn rr-fl-bpmn flowable-init:/work

The volumes are external: true with fixed names, so they resolve identically under docker compose and podman-compose. make seeds before every up; make down removes them. (Open Notificaties needs nothing — nrc-init migrates only.)

Consequence — bare docker compose up can't self-seed external volumes:

  • CI / Linux / macOS: make up or make smoke (seed, then start).
  • No-make / Windows: infra/docker-compose.local.yml — a twin stack that bind-mounts the config instead. Bind mounts are fine locally because a local daemon can see your working directory, so docker compose -f infra/docker-compose.local.yml up -d just works.

Why not the obvious alternatives

  • Bake config into an image (incl. an inline Dockerfile) — docker compose up would then work unaided, but it's a build; we wanted the upstream images as-is.
  • Compose configs: with inline content — Compose writes a client-side temp file and bind-mounts it, hitting the exact same problem.
  • A host-executing runner — bind mounts would work with zero seeding, but it reintroduces a self-hosted runner and undoes the move to ubuntu-latest.

2. Readiness: poll health, don't use --wait

docker compose up --wait looks ideal but fails us three ways:

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

Fixinfra/wait-healthy.sh polls each durable service (openzaak nrc-web acl bff, listed as WAIT_SVCS in the Makefile) with docker ps + docker inspect '{{.State.Health.Status}}' until it reports healthy. It uses only primitives both runtimes support, reads the in-container healthcheck (no host port needed), and ignores the one-shots (they only need to have run). WAIT_TIMEOUT defaults to 420 s — enough for the cold OpenZaak migrate (~90 s) plus app start.


3. pg_isready passes before PostGIS is ready

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