All checks were successful
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 47s
CI / mutation (pull_request) Successful in 1m31s
CI / integration (pull_request) Successful in 3m43s
CI / compose-smoke (pull_request) Successful in 4m1s
The hosted runner can't reach the stack's published ports (sibling containers), so run the seed and the test as containers joined to the OpenZaak network, reaching it by container IP — a single-label host like 'openzaak' isn't URL-valid for OpenZaak's own URLValidator, but an IPv4 literal is. Code is delivered via image build / docker cp (bind mounts don't reach the daemon either). - infra/run-integration.sh: up -> wait healthy (docker inspect) -> seed published zaaktype (python container on the net) -> build + run the test image on the net -> always tear down. Plain docker primitives only (portable docker/podman). - services/acl/Dockerfile.integration: builds + runs Acl.IntegrationTests; dotnet lives in the image, so the CI job needs only Docker (no setup-dotnet). - make integration now delegates to the script; re-added the Gitea Actions job. Supersedes the local-only gap documented earlier; #55 is no longer needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
160 lines
7.7 KiB
Markdown
160 lines
7.7 KiB
Markdown
# 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 |
|
|
| `upload-artifact@v4` fails ("not supported on GHES") | pin `@v3` | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
**Fix** — `infra/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.
|
|
|
|
---
|
|
|
|
## 4. `actions/upload-artifact@v4` refuses to run on Gitea
|
|
|
|
**Symptom** — the `mutation` job's `make mutation` step passes (95% score), but the
|
|
upload step right after it fails the job:
|
|
|
|
```
|
|
::error::@actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
|
|
are not currently supported on GHES.
|
|
❌ Failure - Main https://github.com/actions/upload-artifact@v4
|
|
```
|
|
|
|
**Why** — `upload-artifact@v4` bundles `@actions/artifact` v2, which inspects the
|
|
server URL and **hard-aborts on anything that isn't `github.com`**, treating Gitea
|
|
as an unsupported GitHub Enterprise Server. The check fires regardless of whether
|
|
the Gitea server can actually store artifacts (1.24+ can). It is the *action*, not
|
|
the server, that refuses.
|
|
|
|
**Fix** — pin **`actions/upload-artifact@v3`** (and `download-artifact@v3` if ever
|
|
needed). v3 uses the older artifact protocol that Gitea implements, and has no GHES
|
|
guard. Inputs are the same (`name`, `path`, `if-no-files-found`), so it is a drop-in
|
|
swap. Do **not** bump to `@v4` until act_runner advertises github.com-compatible
|
|
artifact support.
|
|
|
|
---
|
|
|
|
## 5. A runner process can't reach a service container's published port
|
|
|
|
**Symptom** — green locally, but a CI step that runs *on the runner* and talks to a
|
|
compose service over `localhost` fails. The ACL integration test's seed died with:
|
|
|
|
```
|
|
OpenZaak ready (000)
|
|
urllib.error.URLError: <urlopen error [Errno 111] Connection refused>
|
|
make: *** [Makefile:114: integration] Error 1
|
|
```
|
|
|
|
OpenZaak was demonstrably up — uwsgi had been serving for ~2 minutes — yet
|
|
`curl`/`urllib` to `localhost:8000` from the runner were refused the whole time.
|
|
|
|
**Why** — the same sibling-container split as §1. Compose starts the stack via the
|
|
host daemon, so `ports: ["8000:8000"]` publishes to the *daemon host*, not to the job
|
|
container. From the runner, `localhost:8000` has nothing listening. (`make smoke`
|
|
sidesteps this by polling readiness via `docker inspect` (§2), never a service port.)
|
|
|
|
**Fix** — don't talk to service ports from the runner. Either check state via `docker
|
|
inspect` (health), or run the client **inside the compose network** so it reaches the
|
|
service by name (`http://openzaak:8000`). For a test/seed that needs the repo's own
|
|
code, deliver it via a **built image** (not a bind mount — §1), then
|
|
`docker run --network <stack>_cg …`.
|
|
|
|
**Applied** — `make integration` (the ACL ↔ real-OpenZaak test, ADR-0006) does
|
|
exactly this: `infra/run-integration.sh` runs the seed and the test as containers on
|
|
the OpenZaak network and reaches it by **container IP** (a single-label service name
|
|
like `openzaak` isn't URL-valid — OpenZaak echoes the request host into the URLs it
|
|
returns and then rejects them with Django's `URLValidator`; an IPv4 literal passes).
|