The hosted Gitea runner starts the OpenZaak stack as sibling containers via the host daemon, so a process on the runner can't reach the published ports — the seed and dotnet test get Connection refused on localhost:8000. Drop the (non-working) integration CI job; make integration stays the local / host-runner gate. Document the limitation in gitea-actions-gotchas.md §5 and the CI runbook, and track running it inside the compose network in #55. ADR-0006 updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.1 KiB
CI runbook — Gitea Actions
Status: active. The workflow
.gitea/workflows/ci.yamlruns on Gitea's hostedubuntu-latestrunner — no self-hosted runner required.make ciis still the local gate — it runs the exact same checks (the workflow calls the samemaketargets).
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 lint → dotnet format … --verify-no-changes |
.NET 10 SDK |
build |
make build → dotnet build … -c Release |
.NET 10 SDK |
unit |
make unit → dotnet test … -c Release --filter "Category!=Integration" |
.NET 10 SDK |
mutation |
make mutation → dotnet tool restore → dotnet stryker (ACL); uploads the HTML report as an artifact |
.NET 10 SDK |
compose-smoke |
make smoke → seed config volumes → up -d (full stack) → up --wait durable services → down |
container engine + compose v2 |
make integrationis not a hosted-runner job yet. The ACL ↔ real-OpenZaak test (make integration, ADR-0006) passes locally and on a host-executing runner, but a process on the hosted runner can't reach the stack's published ports (sibling containers — see gitea-actions-gotchas.md §5). Run it inside the compose network to gate it in CI — tracked in #55.
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-smokeruns on a containerized runner. Workspace bind mounts do not reach the sibling containers Compose starts, so config/assets are streamed into external named volumes viadocker 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: baredocker compose upno longer self-seeds; usemake 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 integrationis a separate, heavier lane (it stands the OpenZaak stack up and seeds a published zaaktype), so it is not folded intomake 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