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

115 lines
5.6 KiB
Markdown

# 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
```yaml
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 cp`s 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 cp`**not**
`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 exits** — `flowable-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 bff` — `WAIT_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.