CI / lint (pull_request) Successful in 1m20s
CI / k8s (pull_request) Successful in 8s
CI / build (pull_request) Successful in 1m5s
CI / unit (pull_request) Successful in 1m33s
CI / frontend (pull_request) Successful in 3m9s
CI / mutation (pull_request) Successful in 6m53s
CI / verify-stack (pull_request) Successful in 9m32s
`make k8s-lint` existed since the chart landed but nothing ran it, so the chart had no automated coverage at all. A `k8s` job now runs it plus `make k8s-drift` on every push and PR: no cluster, ~20s, and it catches the two failure modes the chart is actually exposed to — a values typo that renders invalid YAML, and a change made to one stack but not the other. helm is installed as its pinned static binary (the URL the Talos runbook already gives developers) rather than via a marketplace action: nothing extra to vet. The `k8s` targets stay out of `make ci` on purpose — helm is optional for anyone not deploying to Kubernetes — which is the one place local and CI now differ, noted in docs/runbooks/ci.md.
318 lines
15 KiB
YAML
318 lines
15 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# Supersede stale runs: a new push to the same branch/PR cancels the previous run, so the runner's
|
|
# concurrency slots aren't spent on commits nobody is waiting for (refs #127).
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Self-hosted runner — see docs/runbooks/ci.md for the runner setup.
|
|
# `uses:` are absolute, tag-pinned URLs (CLAUDE.md §8.7 / §15).
|
|
|
|
# Each job calls a `make` target — the same one developers run locally
|
|
# (`make ci`). The Makefile is the single source of truth; see docs/runbooks/ci.md.
|
|
|
|
jobs:
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
- uses: https://github.com/actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '10.0.x'
|
|
# Cache the NuGet package store so each .NET job restores from disk, not the network. There are
|
|
# no lock files (so setup-dotnet's built-in cache doesn't apply); key on the project files. @v3
|
|
# avoids the GHES guard that breaks @v4 on Gitea (gitea-actions-gotchas.md); cache is best-effort
|
|
# — a miss just restores from the network. See issue #73.
|
|
- uses: https://github.com/actions/cache@v3
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
|
restore-keys: |
|
|
nuget-${{ runner.os }}-
|
|
- run: make lint
|
|
|
|
# The Helm chart's only automated gate: it renders and schema-checks the whole
|
|
# stack, and checks it still describes the same stack as the compose file
|
|
# (ADR-0033). No cluster involved — see docs/runbooks/kubernetes-talos.md.
|
|
k8s:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
# helm as its pinned static binary rather than a marketplace action: one URL,
|
|
# the same one the Talos runbook §0 gives a developer, and no third-party
|
|
# action to vet (CLAUDE.md §13). The drift check also needs `docker compose`,
|
|
# which the runner already has (see docs/runbooks/ci.md).
|
|
- name: Install helm
|
|
run: |
|
|
mkdir -p "$HOME/.local/bin"
|
|
curl -sSL https://get.helm.sh/helm-v3.16.4-linux-amd64.tar.gz \
|
|
| tar xz -O linux-amd64/helm > "$HOME/.local/bin/helm"
|
|
chmod +x "$HOME/.local/bin/helm"
|
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
- run: make k8s-lint
|
|
- run: make k8s-drift
|
|
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
- uses: https://github.com/actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '10.0.x'
|
|
- uses: https://github.com/actions/cache@v3
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
|
restore-keys: |
|
|
nuget-${{ runner.os }}-
|
|
- run: make build
|
|
|
|
unit:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
- uses: https://github.com/actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '10.0.x'
|
|
- uses: https://github.com/actions/cache@v3
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
|
restore-keys: |
|
|
nuget-${{ runner.os }}-
|
|
- run: make unit
|
|
# Job summary (#136): a per-service pass/fail table from the TRX `make unit` wrote.
|
|
- name: Unit test summary
|
|
if: always()
|
|
run: |
|
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
|
python3 infra/trx-summary.py TestResults >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
|
frontend:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
- uses: https://github.com/pnpm/action-setup@v4
|
|
with:
|
|
version: 11
|
|
- uses: https://github.com/actions/setup-node@v4
|
|
with:
|
|
node-version: '24'
|
|
cache: 'pnpm'
|
|
- run: make frontend
|
|
# Job summary (#136): a per-frontend (app) pass/fail table from the vitest JSON each app wrote.
|
|
- name: Frontend test summary
|
|
if: always()
|
|
run: |
|
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
|
python3 infra/vitest-summary.py test-output >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
mutation:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
- uses: https://github.com/actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '10.0.x'
|
|
- uses: https://github.com/actions/cache@v3
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
|
restore-keys: |
|
|
nuget-${{ runner.os }}-
|
|
- run: make mutation
|
|
# Job summary (#136): render each service's Stryker Markdown report on the run page (Gitea
|
|
# 1.27 $GITHUB_STEP_SUMMARY). `if: always()` so a ratchet break still reports — and because
|
|
# `make mutation` stops at the first break, the summary also shows exactly where it stopped.
|
|
# Guarded so it no-ops on a runner/server without summary support. Strips the report's UTF-8 BOM.
|
|
- name: Mutation score summary
|
|
if: always()
|
|
run: |
|
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
|
{
|
|
echo "## 🧬 Mutation testing"
|
|
echo
|
|
for svc in acl event-subscriber domain bff; do
|
|
echo "### $svc"
|
|
echo
|
|
report=$(ls services/"$svc"/StrykerOutput/*/reports/mutation-report.md 2>/dev/null | sort | tail -1)
|
|
if [ -n "$report" ]; then
|
|
sed '1s/^\xef\xbb\xbf//' "$report"
|
|
else
|
|
echo "_No report — \`make mutation\` stopped before \`$svc\` (earlier ratchet break)._"
|
|
fi
|
|
echo
|
|
done
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
# Publish the Stryker HTML reports. `if: always()` uploads them even when the
|
|
# ratchet fails — that is exactly when you want to inspect the survivors.
|
|
# `continue-on-error` keeps the upload best-effort: the mutation *gate* is the
|
|
# ratchet (make mutation's exit code), not the report, so a Gitea artifact-backend
|
|
# 500 must not fail the job (gitea-actions-gotchas.md §4). Glob handles Stryker's
|
|
# non-deterministic StrykerOutput/<timestamp>/ dir. Pinned @v3: @v4's bundled
|
|
# @actions/artifact hard-aborts on non-github.com (GHES guard) — see the runbook.
|
|
- uses: https://github.com/actions/upload-artifact@v3
|
|
if: always()
|
|
continue-on-error: true
|
|
with:
|
|
name: acl-mutation-report
|
|
path: services/acl/StrykerOutput/**/reports/mutation-report.html
|
|
if-no-files-found: warn
|
|
- uses: https://github.com/actions/upload-artifact@v3
|
|
if: always()
|
|
continue-on-error: true
|
|
with:
|
|
name: event-subscriber-mutation-report
|
|
path: services/event-subscriber/StrykerOutput/**/reports/mutation-report.html
|
|
if-no-files-found: warn
|
|
- uses: https://github.com/actions/upload-artifact@v3
|
|
if: always()
|
|
continue-on-error: true
|
|
with:
|
|
name: domain-mutation-report
|
|
path: services/domain/StrykerOutput/**/reports/mutation-report.html
|
|
if-no-files-found: warn
|
|
- uses: https://github.com/actions/upload-artifact@v3
|
|
if: always()
|
|
continue-on-error: true
|
|
with:
|
|
name: bff-mutation-report
|
|
path: services/bff/StrykerOutput/**/reports/mutation-report.html
|
|
if-no-files-found: warn
|
|
|
|
# One stage for every check that needs the live stack. Booting OpenZaak once (instead
|
|
# of once per job) is the cheapest layout (issue #58). No setup-dotnet: the ACL test runs
|
|
# in a built image and everything reaches services by container IP. Needs Docker + egress
|
|
# (base images, nuget, selectielijst.openzaak.nl).
|
|
#
|
|
# `needs: [mutation]` is NOT a data dependency — it serialises the two memory-heavy jobs so
|
|
# they never co-schedule now the runner has capacity >1. A concurrent Stryker run + full-stack
|
|
# bring-up + Playwright browser on one host is what OOMs the e2e (commit d5e5fa2, #126). The
|
|
# light .NET/frontend jobs have no `needs`, so they still parallelise up to runner capacity.
|
|
#
|
|
# No `if: ${{ !cancelled() }}` here (removed in #134): on Gitea 1.27 + act_runner 2.0.0, a job
|
|
# gated by a status-function `if` (always()/cancelled()) on top of `needs` routes through the new
|
|
# transitional "Cancelling" state + capability negotiation and never leaves `waiting` — it's never
|
|
# dispatched (gitea-actions-gotchas.md §7). Default `if: success()` dispatches normally. Cost: a
|
|
# failing mutation ratchet now skips verify-stack instead of running it anyway; the fix-and-re-push
|
|
# re-run exercises verify-stack, so we still get the signal.
|
|
verify-stack:
|
|
needs: [mutation]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
# Bring the full stack up + wait for health — this also is the DoD "compose up
|
|
# reaches green health" smoke (it replaces the old compose-smoke job).
|
|
# Each check carries an `id` so the summary step below can report its per-check outcome (#136).
|
|
# A failed check skips the rest (no step `if:`), so the table shows exactly where it stopped.
|
|
- name: Bring up the full stack & wait for health
|
|
id: up
|
|
run: make verify-up
|
|
- name: Observability backplane (Grafana + Tempo + Prometheus datasources)
|
|
id: obs
|
|
run: OBS_TIMEOUT=180 make verify-observability
|
|
- name: Objecttypen API up + token authenticates
|
|
id: objecttypen
|
|
run: OBJECTTYPEN_TIMEOUT=120 make verify-objecttypen
|
|
- name: Objecten API up + token authenticates + trusts Objecttypen
|
|
id: objecten
|
|
run: OBJECTEN_TIMEOUT=120 make verify-objecten
|
|
- name: RegisterRecord objecttype registered + published
|
|
id: registerrecord
|
|
run: REGISTERRECORD_TIMEOUT=120 make verify-registerrecord
|
|
- name: ACL ↔ OpenZaak integration tests
|
|
id: acl
|
|
run: make verify-acl
|
|
- name: OpenZaak → NRC notification delivery
|
|
id: nrc
|
|
run: make verify-nrc
|
|
- name: OpenZaak → NRC → Event Subscriber → projection-api
|
|
id: projection
|
|
run: make verify-projection
|
|
- name: Objecten → NRC notification delivery
|
|
id: objecten_nrc
|
|
run: make verify-objecten-notifications
|
|
- name: Domain → Flowable → ACL → OpenZaak
|
|
id: domain
|
|
run: make verify-domain
|
|
- name: BFF → Keycloak + domain + projection
|
|
id: bff
|
|
run: make verify-bff
|
|
- name: Distributed traces reach Tempo (one connected trace across services)
|
|
id: tracing
|
|
run: TRACING_TIMEOUT=120 make verify-tracing
|
|
- name: Golden-signal metrics scraped by Prometheus (/metrics on every service)
|
|
id: metrics
|
|
run: METRICS_TIMEOUT=120 make verify-metrics
|
|
- name: Self-service e2e (Playwright, login → submit → success)
|
|
id: e2e
|
|
run: make verify-e2e
|
|
# Job summary (#136): a pass/fail table of every live-stack check, so a red verify-stack shows
|
|
# which check failed at a glance. `if: always()` (step-level — safe on runner 2.0.0, unlike the
|
|
# job-level status-function `if` of #134) so it renders even after a check fails.
|
|
- name: verify-stack check summary
|
|
if: always()
|
|
env:
|
|
UP: ${{ steps.up.outcome }}
|
|
OBS: ${{ steps.obs.outcome }}
|
|
OBJECTTYPEN: ${{ steps.objecttypen.outcome }}
|
|
OBJECTEN: ${{ steps.objecten.outcome }}
|
|
REGISTERRECORD: ${{ steps.registerrecord.outcome }}
|
|
OBJECTEN_NOTIFICATIONS: ${{ steps.objecten_nrc.outcome }}
|
|
ACL: ${{ steps.acl.outcome }}
|
|
NRC: ${{ steps.nrc.outcome }}
|
|
PROJECTION: ${{ steps.projection.outcome }}
|
|
DOMAIN: ${{ steps.domain.outcome }}
|
|
BFF: ${{ steps.bff.outcome }}
|
|
TRACING: ${{ steps.tracing.outcome }}
|
|
METRICS: ${{ steps.metrics.outcome }}
|
|
E2E: ${{ steps.e2e.outcome }}
|
|
run: |
|
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
|
icon() { case "$1" in success) echo "✅";; failure) echo "❌";; skipped) echo "⏭️";; cancelled) echo "🚫";; *) echo "❔ ${1:-—}";; esac; }
|
|
{
|
|
echo "## 🔌 verify-stack checks"
|
|
echo
|
|
echo "| Check | Result |"
|
|
echo "| ----- | :----: |"
|
|
echo "| Bring up + health | $(icon "$UP") |"
|
|
echo "| Observability backplane | $(icon "$OBS") |"
|
|
echo "| Objecttypen API + token | $(icon "$OBJECTTYPEN") |"
|
|
echo "| Objecten API + token | $(icon "$OBJECTEN") |"
|
|
echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |"
|
|
echo "| Objecten → NRC | $(icon "$OBJECTEN_NOTIFICATIONS") |"
|
|
echo "| ACL ↔ OpenZaak | $(icon "$ACL") |"
|
|
echo "| OpenZaak → NRC | $(icon "$NRC") |"
|
|
echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |"
|
|
echo "| Domain → Flowable → ACL → OpenZaak | $(icon "$DOMAIN") |"
|
|
echo "| BFF → Keycloak + domain + projection | $(icon "$BFF") |"
|
|
echo "| Distributed traces (Tempo) | $(icon "$TRACING") |"
|
|
echo "| Golden-signal metrics (Prometheus) | $(icon "$METRICS") |"
|
|
echo "| Self-service e2e (Playwright) | $(icon "$E2E") |"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
# Job summary (#136): per-spec Playwright results, from the JSON report run-e2e-check.sh copied
|
|
# out of the e2e container. Turns a red e2e into a one-glance "which spec" instead of a log dive.
|
|
- name: e2e spec summary
|
|
if: always()
|
|
run: |
|
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
|
python3 infra/playwright-summary.py tests/e2e/playwright-report.json >> "$GITHUB_STEP_SUMMARY"
|
|
# Log dump must precede teardown (which removes the containers).
|
|
- name: Dump container logs on failure
|
|
if: failure()
|
|
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten objecten-celery registerrecord-init tempo prometheus grafana 2>&1 || true
|
|
- name: Tear down
|
|
if: always()
|
|
run: make down
|