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>
146 lines
7.3 KiB
Markdown
146 lines
7.3 KiB
Markdown
# 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 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 |
|
|
| `integration` | `make integration` → `infra/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](gitea-actions-gotchas.md)).
|
|
|
|
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](gitea-actions-gotchas.md).
|
|
|
|
## Mutation testing (the ratchet)
|
|
|
|
The `mutation` job enforces test *strength*, not just coverage (CLAUDE.md §5).
|
|
[Stryker.NET](https://stryker-mutator.io/docs/stryker-net/) is pinned as a local
|
|
dotnet tool (`.config/dotnet-tools.json`), so it runs identically locally and in CI:
|
|
|
|
```bash
|
|
make mutation # dotnet tool restore + dotnet stryker on the ACL
|
|
```
|
|
|
|
Config lives in [`services/acl/stryker-config.json`](../../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](gitea-actions-gotchas.md) (§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:
|
|
|
|
```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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
```
|