Files
register-referentie/docs/runbooks/ci.md
Niek Otten 347713766e
Some checks failed
CI / lint (pull_request) Successful in 52s
CI / build (pull_request) Successful in 41s
CI / unit (pull_request) Successful in 47s
CI / mutation (pull_request) Failing after 1m50s
CI / compose-smoke (pull_request) Successful in 3m55s
ci(acl): publish the Stryker HTML report as a CI artifact (refs #47)
Add an upload-artifact step to the mutation job so the ACL mutation report is
downloadable from the run summary. `if: always()` uploads it even when the
ratchet fails — exactly when the survivors matter. A glob handles Stryker's
timestamped output directory. First use of actions/upload-artifact (@v4, pinned);
Gitea 1.25.x supports it. Document it in the CI runbook.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:21:26 +02:00

6.4 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 .NET 10 SDK
mutation make mutationdotnet tool restoredotnet 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

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, @v4); Gitea ≥1.24 supports v4 and this instance runs 1.25.x. If a future Gitea/runner combination can't store artifacts, treat it as a known gap per §15 and record it in gitea-actions-gotchas.md rather than blocking merges on it.

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 — what the pipeline runs
make lint        # or a single stage
make mutation    # Stryker.NET ratchet on the ACL
make smoke       # compose up --wait, curl /health, tear down

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