Files
register-referentie/docs/runbooks/ci.md
Niek Otten 4474585606
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
ci(acl): run the ACL integration test in CI inside the compose network (closes #55) (refs #46)
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>
2026-06-29 12:28:43 +02:00

7.3 KiB

CI runbook — Gitea Actions

Status: active. The workflow .gitea/workflows/ci.yaml runs on Gitea's hosted ubuntu-latest runner — no self-hosted runner required. make ci is still the local gate — it runs the exact same checks (the workflow calls the same make targets).

The pipeline

.gitea/workflows/ci.yaml runs on every push and pull request to main. Each job calls a make target — the single source of truth for the checks, so local and CI cannot drift:

Job Target Needs
lint make lintdotnet format … --verify-no-changes .NET 10 SDK
build make builddotnet build … -c Release .NET 10 SDK
unit make unitdotnet test … -c Release --filter "Category!=Integration" .NET 10 SDK
mutation make mutationdotnet tool restoredotnet stryker (ACL); uploads the HTML report as an artifact .NET 10 SDK
integration make integrationinfra/run-integration.sh: OpenZaak up → seed a published BIG zaaktype + run Acl.IntegrationTests as containers inside the compose network → tear down container engine + egress (base images, nuget, selectielijst.openzaak.nl)
compose-smoke make smoke → seed config volumes → up -d (full stack) → up --wait durable services → down container engine + compose v2

The integration job needs no setup-dotnet. dotnet runs inside the test image, and both the seed and the test join the OpenZaak network and reach it by container IP — so the runner never has to reach a published port (see gitea-actions-gotchas.md §5).

All uses: references are absolute, tag-pinned URLs (https://github.com/actions/checkout@v4, https://github.com/actions/setup-dotnet@v4) per CLAUDE.md §8.7 and §15 — Gitea Actions resolves them from GitHub.

compose-smoke runs on a containerized runner. Workspace bind mounts do not reach the sibling containers Compose starts, so config/assets are streamed into external named volumes via docker cp (infra/seed-config.sh), and the upstream images are used verbatim (no build). If you add a service that needs a repo file at runtime, seed it the same way — don't bind-mount it. Note: bare docker compose up no longer self-seeds; use make up. See gitea-actions-gotchas.md.

Mutation testing (the ratchet)

The mutation job enforces test strength, not just coverage (CLAUDE.md §5). Stryker.NET is pinned as a local dotnet tool (.config/dotnet-tools.json), so it runs identically locally and in CI:

make mutation     # dotnet tool restore + dotnet stryker on the ACL

Config lives in services/acl/stryker-config.json. It runs in solution mode against Acl.slnx, mutating the two projects under test (Acl.Application, Acl.Infrastructure); Acl.Api has no tests and is skipped.

Baseline (the ratchet): the ACL is the first service with branching logic, so it sets the repo-wide baseline. Observed score 95%; enforced break threshold 90% (one-mutant headroom over the ~20-mutant surface). Stryker exits non-zero — failing the job — when the score drops below break. Per §5 the baseline only moves up, and only as a slice's stated outcome; never lower it. New services add their own mutation run as they gain logic.

The HTML report is written to services/acl/StrykerOutput/<timestamp>/reports/ (git-ignored); open it to see survived vs. killed mutants.

In CI the mutation job publishes that report as the acl-mutation-report artifact (download it from the run's summary page). The upload step uses if: always(), so the report is available even when the ratchet fails — which is exactly when you want to inspect the survivors. It is the repo's first use of actions/upload-artifact, pinned to @v3: @v4 refuses to run on Gitea (its @actions/artifact v2 library blocks any non-github.com server as "GHES"), while @v3 speaks the artifact protocol Gitea implements. See gitea-actions-gotchas.md §4 (§15).

Running the stack locally without make (Windows / Docker Desktop)

make and the bash helpers assume a Unix shell. To bring the whole stack up on a machine without them (e.g. Windows + Docker Desktop), use the local compose file, which bind-mounts the config instead of seeding volumes — so it needs no make, no seed step, and no bash:

docker compose -f infra/docker-compose.local.yml up -d --build          # any engine
docker compose -f infra/docker-compose.local.yml up -d --build --wait    # Docker Desktop (Compose v2)
docker compose -f infra/docker-compose.local.yml down --volumes

On Linux/macOS the same thing is wrapped as make local / make local-down.

infra/docker-compose.local.yml mirrors the canonical infra/docker-compose.yml but swaps the external config volumes for bind mounts — valid locally because a local daemon can see the working directory (the seed/volume dance only exists for the containerized CI runner). Keep the two files in sync.

Running CI locally (make ci)

Until the runner exists, run the full pipeline yourself before pushing:

make ci          # lint + build + unit + mutation + smoke — the fast pipeline lanes
make lint        # or a single stage
make mutation    # Stryker.NET ratchet on the ACL
make smoke       # compose up --wait, curl /health, tear down
make integration # ACL ↔ real OpenZaak (its own CI job; not part of `make ci`)

make integration is a separate, heavier lane (it stands the OpenZaak stack up and seeds a published zaaktype), so it is not folded into make ci. Run it before pushing changes that touch the ACL gateway or the OpenZaak seed. See ADR-0006.

Prerequisites: .NET 10 SDK, a container engine with Compose v2, and curl.

On a rootless Podman box (the default dev setup here), the smoke target needs the Podman API socket and a Compose provider:

systemctl --user enable --now podman.socket            # start the API socket
ln -sf "$(command -v podman)" ~/.local/bin/docker      # docker -> podman shim
# install Docker Compose v2 into ~/.local/bin as `docker-compose` (the provider)

The Makefile auto-points DOCKER_HOST at /run/user/$(id -u)/podman/podman.sock when that socket exists and DOCKER_HOST is unset, so make smoke "just works" locally while leaving real Docker hosts / CI runners untouched.

Runner: ubuntu-latest

All jobs run on Gitea's hosted ubuntu-latest runner — no self-hosted runner setup is required. The hosted runner ships with Docker and Docker Compose v2, so make smoke (docker compose … up --wait) works without extra configuration.

If Gitea's hosted runners are unavailable and a self-hosted fallback is needed, register an act_runner with the ubuntu-latest label:

VER=0.2.11
curl -fsSL -o /usr/local/bin/act_runner \
  "https://dl.gitea.com/act_runner/${VER}/act_runner-${VER}-linux-amd64"
chmod +x /usr/local/bin/act_runner

act_runner register --no-interactive \
  --instance https://git.labs.respellion.tech \
  --token <REGISTRATION_TOKEN> \
  --name respellion-ci-1 \
  --labels "ubuntu-latest:docker://node:20-bookworm"

act_runner daemon