# Developer + CI entrypoints. # # These targets are the single source of truth for the checks. The Gitea # Actions workflow (.gitea/workflows/ci.yaml) invokes the SAME targets, so # `make ci` locally runs exactly what the pipeline runs — no drift. Until a # self-hosted runner is registered, `make ci` is the gate (see docs/runbooks/ci.md). SLN := register-referentie.slnx COMPOSE := infra/docker-compose.yml # Long-running services with a healthcheck — the smoke polls these for readiness # (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init) # are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md. WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel beheer objecttypen objecten # Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed # into external named volumes via `docker cp` (infra/seed-config.sh) instead of # bind-mounted, because bind mounts don't reach sibling containers on the # containerized CI runner. SEED populates them; run it before every `up`. The # volumes are `external`, so compose won't remove them — CFG_VOLS lists them for # explicit teardown. See docs/runbooks/gitea-actions-gotchas.md. SEED := bash infra/seed-config.sh CFG_VOLS := rr-oz-config rr-nrc-config rr-kc-realms rr-fl-bpmn rr-objecttypen-config rr-objecten-config rr-registerrecord-config # Local-only stack: same services but config is bind-mounted (no seed step), so a # plain `docker compose -f infra/docker-compose.local.yml up` works on any local # engine. This is the no-make / Windows-friendly path. See that file's header. LOCAL_COMPOSE := infra/docker-compose.local.yml OZ_COMPOSE := infra/openzaak/docker-compose.yml OZ_BASE := http://localhost:8000 NRC_COMPOSE := infra/opennotificaties/docker-compose.yml NRC_BASE := http://localhost:8001 KC_COMPOSE := infra/keycloak/docker-compose.yml KC_BASE := http://localhost:8180 FL_COMPOSE := infra/flowable/docker-compose.yml FL_BASE := http://localhost:8090/flowable-rest/service STACK_FILES := -f $(OZ_COMPOSE) -f $(NRC_COMPOSE) # On a rootless Podman dev box, point Docker CLI/Compose at the Podman socket — # but only if that socket exists and DOCKER_HOST isn't already set, so real # Docker hosts and CI runners are left untouched. PODMAN_SOCK := /run/user/$(shell id -u)/podman/podman.sock ifeq ($(wildcard $(PODMAN_SOCK)),$(PODMAN_SOCK)) ifeq ($(origin DOCKER_HOST),undefined) export DOCKER_HOST := unix://$(PODMAN_SOCK) endif endif .PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help ## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions) ## `verify` is the live-stack stage (full stack up once → ACL + notification checks). ci: lint build unit mutation frontend verify ## frontend: install deps and run the Nx lint/test/build for the portals (pnpm + Node required) # Tests run in their own phase, ahead of the build. The @angular/build:unit-test # (Vitest) runner spawns a worker with a hard-coded 60s/90s startup timeout that is # not configurable. When the ~5min production build shares the run-many pool, it # starves that worker of CPU on constrained CI runners and Vitest fails with # "Timeout waiting for worker to respond". Splitting the phases keeps tests off the # heavy build's back so the worker starts well inside its window. frontend: pnpm install --frozen-lockfile pnpm nx run-many -t lint test pnpm nx run-many -t build ## lint: verify formatting (no changes) lint: dotnet format $(SLN) --verify-no-changes # Only pages in mkdocs.yml's nav are published, and mkdocs keeps a build green # when one is missing — so the nav is checked here rather than not at all. python3 infra/check-docs-nav.py ## build: release build build: dotnet build $(SLN) -c Release ## unit: run unit tests (excludes the container-backed Integration lane) # TRX per test project (→ TestResults/) feeds the CI per-service summary (#136); harmless locally. # The CI reporting scripts are stdlib Python with their own assert-based self-checks (#161) — they # ride this lane so a broken job summary is caught by CI rather than by the next red pipeline. unit: dotnet test $(SLN) -c Release --filter "Category!=Integration" --logger trx --results-directory TestResults python3 infra/test_playwright_summary.py python3 infra/test_portal_caddyfiles.py ## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline) # Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore` # makes `make mutation` work from a fresh clone. Each service owns its config + break # threshold (the ratchet, CLAUDE.md §5): each services//stryker-config.json. # Scores never regress below baseline. mutation: dotnet tool restore cd services/acl && dotnet stryker cd services/event-subscriber && dotnet stryker cd services/domain && dotnet stryker cd services/bff && dotnet stryker ## smoke: seed config, bring the whole stack up, wait for health-checked services, tear down # SEED populates the external config volumes first (upstream images used verbatim; # only our acl/bff are built). `up -d --build` starts EVERYTHING. Readiness is # checked by infra/wait-healthy.sh polling the durable, health-checked services # ($(WAIT_SVCS)) via `docker inspect` — portable across docker compose and # podman-compose, and needing no `--wait` flag or host port access. The one-shots # (oz-init, flowable-init) aren't polled; they just need to have run. smoke: $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build bash -c 'WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS); rc=$$?; docker compose -f $(COMPOSE) down --volumes; docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; exit $$rc' ## up: seed config volumes and start the full stack (use instead of bare ## `docker compose up`, which can't self-seed the external config volumes) up: $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build ## down: stop and remove the local stack (incl. the external config volumes) down: docker compose -f $(COMPOSE) down --volumes -docker volume rm -f $(CFG_VOLS) ## local: bring up the bind-mount stack (no seed step) and wait for health ## (Windows / no-make users: run `docker compose -f infra/docker-compose.local.yml up -d --build` directly) local: docker compose -f $(LOCAL_COMPOSE) up -d --build WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) ## verify-local: acceptance check for the local stack (S-B04) — a fresh `make local` completes the ## whole flow (zaaktype seeded + DMN deployed + NRC abonnement) with NO manual seeding. verify-local: bash infra/run-local-flow-check.sh ## local-down: stop and remove the bind-mount stack local-down: docker compose -f $(LOCAL_COMPOSE) down --volumes ## changelog: regenerate CHANGELOG.md from Conventional Commits (git-cliff) changelog: git-cliff --output CHANGELOG.md # ── ZGW verification ─────────────────────────────────────────────────────── # On the single runner CI jobs run sequentially, so the OpenZaak-dependent checks # share ONE full-stack bring-up: the `verify-stack` CI job runs `verify-up` then # `verify-acl` + `verify-nrc` as steps against the same stack (issue #58). The # check logic lives in stack-agnostic runners that reach services by container IP # (gitea-actions-gotchas.md §5/§6); `integration` / `verify-notifications` are local # convenience wrappers that bring up a lighter stack and call the same runners. ## verify-up: bring the FULL stack up and wait for health (CI verify-stack step 1; ## subsumes the old compose-smoke health gate — the DoD "up reaches green" check). verify-up: $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) ## verify-acl: ACL ↔ OpenZaak integration tests against the already-running stack. verify-acl: bash infra/run-acl-integration.sh ## verify-nrc: OpenZaak → NRC notification delivery against the already-running stack. verify-nrc: bash infra/run-notification-check.sh ## verify-projection: OpenZaak → NRC → Event Subscriber → projection-api end-to-end (S-06), ## against the already-running stack. verify-projection: bash infra/run-projection-check.sh ## verify-domain: domain → Flowable → ACL → OpenZaak end-to-end (S-05), against the ## already-running stack. Recreates the acl service to inject the seeded zaaktype URL. verify-domain: bash infra/run-domain-check.sh ## verify-bff: BFF end-to-end (S-07) against the up stack — token validation on self-service ## + anonymous public-safe openbaar register (ADR-0010). verify-bff: bash infra/run-bff-check.sh ## verify-e2e: walking-skeleton Playwright e2e (S-08d) against the up stack — DigiD login → ## submit → confirmation, driven inside the compose network. verify-e2e: bash infra/run-e2e-check.sh ## verify-observability: assert the observability backplane (Grafana + provisioned Tempo & ## Prometheus datasources) is live, against the already-running stack (S-16a). verify-observability: bash infra/run-observability-check.sh ## verify-tracing: assert one connected distributed trace spans the .NET services in Tempo ## (S-16b), against the already-running stack. verify-tracing: bash infra/run-tracing-check.sh ## verify-metrics: assert the services expose /metrics and Prometheus scrapes the golden ## signals (S-16c), against the already-running stack. verify-metrics: bash infra/run-metrics-check.sh ## verify-objecttypen: assert the Objecttypen API is up + its static token authenticates ## (S-18a), against the already-running stack. verify-objecttypen: bash infra/run-objecttypen-check.sh ## verify-objecten: assert the Objecten API is up + its static token authenticates and it ## trusts the Objecttypen API (S-18b), against the already-running stack. verify-objecten: bash infra/run-objecten-check.sh ## verify-registerrecord: assert the RegisterRecord objecttype is registered + published in the ## Objecttypen API (S-18c), against the already-running stack. verify-registerrecord: bash infra/run-registerrecord-check.sh ## verify-objecten-notifications: assert a RegisterRecord write in Objecten is DELIVERED as an ## `objecten` notification via NRC (S-19b-1), against the already-running stack. verify-objecten-notifications: bash infra/run-objecten-notifications-check.sh ## verify: local mirror of the CI verify-stack job — full stack up once, all checks, ## tear down (always). For fast single-concern local iteration use `integration` ## (oz-only) or `verify-notifications` (oz+nrc) instead. verify: $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build @bash -c 'set -e; rc=0; \ WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \ && bash infra/run-acl-integration.sh \ && bash infra/run-notification-check.sh \ && bash infra/run-projection-check.sh \ && bash infra/run-objecten-notifications-check.sh \ && bash infra/run-domain-check.sh \ && bash infra/run-bff-check.sh \ && bash infra/run-e2e-check.sh || rc=$$?; \ docker compose -f $(COMPOSE) down --volumes >/dev/null 2>&1; \ docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; \ exit $$rc' ## integration: local convenience — ACL integration test against a throwaway ## OpenZaak-only stack (fast iteration). CI uses verify-acl on the shared stack. integration: bash infra/run-integration.sh ## openzaak-up: start the OpenZaak stack (migrations run on first start) openzaak-up: $(SEED) oz docker compose -f $(OZ_COMPOSE) up -d ## openzaak-smoke: start OpenZaak, then assert it is up with auth enforced openzaak-smoke: openzaak-up @bash -c 'set -e; \ echo "waiting for OpenZaak to respond..."; \ for i in $$(seq 1 60); do \ code=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/zaken || true); \ [ -n "$$code" ] && [ "$$code" != "000" ] && break; sleep 3; \ done; \ echo "GET /zaken/api/v1/zaken (unauth) -> $$code (expect 403, auth enforced)"; test "$$code" = "403"; \ admin=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/admin/); \ echo "GET /admin/ -> $$admin (expect 302)"; test "$$admin" = "302"; \ root=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/); \ echo "GET /zaken/api/v1/ -> $$root (expect 200)"; test "$$root" = "200"; \ echo "OpenZaak smoke OK"' ## openzaak-seed: bring OpenZaak up and seed the BIG catalogus (idempotent) openzaak-seed: openzaak-up @bash -c 'for i in $$(seq 1 50); do \ c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \ [ "$$c" = "200" ] && break; sleep 3; done; echo "OpenZaak ready ($$c)"' python3 infra/openzaak/seed_catalogus.py ## openzaak-down: stop and remove the OpenZaak stack (wipes data) openzaak-down: docker compose -f $(OZ_COMPOSE) down --volumes -docker volume rm -f rr-oz-config ## verify-notifications: local convenience — OpenZaak → NRC notification delivery ## against a throwaway oz+nrc stack (S-01-c). CI uses verify-nrc on the shared stack. verify-notifications: bash infra/verify-notifications.sh ## stack-up: start OpenZaak + Open Notificaties together (shared network), with ## OpenZaak publishing notifications to NRC (S-01-c). stack-up: $(SEED) oz nrc OZ_NOTIFICATIONS_DISABLED=false docker compose $(STACK_FILES) up -d ## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable stack-smoke: stack-up @bash -c 'set -e; \ echo "waiting for OpenZaak + Open Notificaties..."; \ for i in $$(seq 1 60); do \ oz=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/admin/ || true); \ nrc=$$(curl -s -o /dev/null -w "%{http_code}" $(NRC_BASE)/admin/ || true); \ [ "$$oz" = "302" ] && [ "$$nrc" = "302" ] && break; sleep 3; done; \ z=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/zaken); \ echo "OpenZaak /zaken (unauth) -> $$z (expect 403)"; test "$$z" = "403"; \ echo "OpenZaak /admin/ -> $$oz (expect 302)"; test "$$oz" = "302"; \ echo "Open Notificaties /admin/-> $$nrc (expect 302)"; test "$$nrc" = "302"; \ echo "stack smoke OK"' ## stack-down: stop and remove both stacks (wipes data) stack-down: docker compose $(STACK_FILES) down --volumes -docker volume rm -f rr-oz-config rr-nrc-config ## keycloak-up: start Keycloak with the four imported realms keycloak-up: $(SEED) kc docker compose -f $(KC_COMPOSE) up -d ## keycloak-smoke: start Keycloak, then verify each realm logs in + returns its claim keycloak-smoke: keycloak-up @bash -c 'for i in $$(seq 1 60); do \ c=$$(curl -s -o /dev/null -w "%{http_code}" $(KC_BASE)/realms/digid/.well-known/openid-configuration || true); \ [ "$$c" = "200" ] && break; sleep 3; done; echo "Keycloak ready ($$c)"' python3 infra/keycloak/check_realms.py ## keycloak-down: stop and remove Keycloak keycloak-down: docker compose -f $(KC_COMPOSE) down --volumes -docker volume rm -f rr-kc-realms ## flowable-up: start Flowable (deploys registratie.bpmn on boot) flowable-up: $(SEED) fl docker compose -f $(FL_COMPOSE) up -d ## flowable-smoke: start Flowable, then verify a started instance waits on the external task flowable-smoke: flowable-up @bash -c 'for i in $$(seq 1 80); do \ c=$$(curl -s -o /dev/null -w "%{http_code}" -u rest-admin:test $(FL_BASE)/repository/process-definitions?key=registratie || true); \ [ "$$c" = "200" ] && break; sleep 3; done; echo "Flowable ready ($$c)"' python3 infra/flowable/verify.py ## flowable-down: stop and remove Flowable flowable-down: docker compose -f $(FL_COMPOSE) down --volumes -docker volume rm -f rr-fl-bpmn # ── Kubernetes (single-node Talos) ───────────────────────────────────────────── # The Helm chart in infra/helm/big-reference is a port of infra/docker-compose.yml # (ADR-0033). Full walkthrough: docs/runbooks/kubernetes-talos.md. # TALOS_HOST the address the BROWSER uses — pins Keycloak's issuer and the portals' # OIDC authority. Use `localhost` with `make k8s-portals`: the OIDC # library needs crypto.subtle, which browsers only expose on a secure # context (https, or localhost) — see docs/runbooks/kubernetes-talos.md §5 # K8S_REGISTRY the registry both sides use for this repo's images (see k8s-registry) K8S_NS ?= big K8S_CHART := infra/helm/big-reference K8S_REGISTRY ?= TALOS_HOST ?= # The images built from this repo — compose service name == image name == chart workload. K8S_IMAGES := acl domain bff event-subscriber projection-api self-service openbaar behandel beheer ## k8s-lint: render + schema-check the Helm chart (no cluster needed) k8s-lint: helm lint $(K8S_CHART) helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null ## k8s-drift: fail if compose and the Helm chart describe different stacks # Compose is CI-canonical (ADR-0033) and the chart is a transcription of it; this # compares what each one deploys — workload names and resolved images. Needs # `docker compose` and `helm`, no cluster. k8s-drift: python3 infra/helm/check-drift.py ## k8s-registry: deploy the in-cluster image registry (NodePort 30500) k8s-registry: kubectl apply -f infra/helm/registry.yaml kubectl -n registry rollout status deploy/registry --timeout=180s ## k8s-images: build this repo's images (via compose) and push them to $(K8S_REGISTRY) # `docker save | crane push` rather than `docker push`: the registry speaks plain # HTTP, which the Docker daemon refuses without a root-level insecure-registries # entry, while crane just takes --insecure. Install: see docs/runbooks/kubernetes-talos.md. k8s-images: @command -v crane >/dev/null || { echo "crane not found — see docs/runbooks/kubernetes-talos.md §0" >&2; exit 2; } @test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=" >&2; exit 2; } docker compose -f $(COMPOSE) build $(K8S_IMAGES) @tar=$$(mktemp -t rr-img-XXXX.tar); \ for i in $(K8S_IMAGES); do \ docker save register-referentie/$$i:dev -o $$tar; \ crane push --insecure $$tar $(K8S_REGISTRY)/register-referentie/$$i:dev; \ done; rm -f $$tar ## k8s-seed: create the ConfigMaps the chart mounts (upstream config + bootstrap scripts) k8s-seed: bash infra/helm/seed-configmaps.sh $(K8S_NS) ## k8s-up: seed the config and install/upgrade the release k8s-up: k8s-seed @test -n "$(TALOS_HOST)" || { echo "set TALOS_HOST=" >&2; exit 2; } @test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=" >&2; exit 2; } helm upgrade --install big $(K8S_CHART) -n $(K8S_NS) --create-namespace \ --set host=$(TALOS_HOST) --set images.registry=$(K8S_REGISTRY) $(K8S_SET) kubectl -n $(K8S_NS) get pods ## k8s-reseed: re-run the bootstrap jobs (after a database was wiped, or after ## changing a Job in the chart — Job pod templates are immutable, so a plain ## `helm upgrade` is rejected) k8s-reseed: kubectl -n $(K8S_NS) delete job -l app.kubernetes.io/component=init --ignore-not-found $(MAKE) k8s-up # The projection's schema is created on service start (Projection.ReadModel migrates in a # hosted service), so a wiped database also needs these two restarted — otherwise they keep # writing to a schema-less DB and fail with `relation "processed_notifications" does not exist`. kubectl -n $(K8S_NS) rollout restart deploy/event-subscriber deploy/projection-api kubectl -n $(K8S_NS) rollout status deploy/event-subscriber deploy/projection-api --timeout=180s ## k8s-portals: forward the browser-facing services to localhost (Ctrl-C stops them all) # The portals' OIDC flow needs a *secure context* for crypto.subtle (PKCE), and browsers # only grant that to https or localhost — a NodePort on the VM's IP is neither. Forwarding # to localhost on the same port numbers keeps Keycloak's pinned issuer valid. Deploy with # TALOS_HOST=localhost for this to line up. k8s-portals: @echo "self-service http://localhost:30140 · openbaar :30141 · behandel :30142 · beheer :30143 · keycloak :30180" @trap 'kill 0' INT TERM; \ for f in self-service:30140:80 openbaar:30141:80 behandel:30142:80 beheer:30143:80 keycloak:30180:8080; do \ svc=$${f%%:*}; rest=$${f#*:}; lport=$${rest%%:*}; rport=$${rest#*:}; \ kubectl -n $(K8S_NS) port-forward --address 127.0.0.1 svc/$$svc $$lport:$$rport >/dev/null & \ done; wait ## k8s-down: uninstall the release (database PVCs are kept) k8s-down: helm uninstall big -n $(K8S_NS) ## k8s-purge: uninstall AND drop the namespace, including the database volumes k8s-purge: -helm uninstall big -n $(K8S_NS) kubectl delete namespace $(K8S_NS) --ignore-not-found ## help: list available targets help: @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'