refactor(ci): one verify-stack stage for all live-stack checks (closes #58) (refs #46 #56)
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 40s
CI / unit (pull_request) Successful in 48s
CI / mutation (pull_request) Successful in 1m36s
CI / verify-stack (pull_request) Successful in 4m37s

On the single self-hosted runner CI jobs run sequentially, so booting OpenZaak once
beats once-per-job. Replace the integration + notifications + compose-smoke jobs with
one verify-stack job that brings the full stack up once and runs, as clearly-named
steps: health (make verify-up, the DoD smoke) → ACL ↔ OpenZaak (verify-acl) →
OpenZaak → NRC delivery (verify-nrc) → teardown (always) + log dump on failure.

The check logic moves into stack-agnostic runners (run-acl-integration.sh,
run-notification-check.sh) that operate on whatever stack is already up, reaching
services by container IP. The local single-concern wrappers (make integration oz-only,
make verify-notifications oz+nrc) keep working by delegating to the same runners, so
nothing is duplicated. make ci now runs the consolidated 'verify' stage.

Verified locally: make verify boots the full stack once, ACL integration passes and
the NRC notification is delivered, then tears down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 16:48:38 +02:00
parent a256db1a23
commit d49443353e
10 changed files with 228 additions and 186 deletions

37
infra/run-acl-integration.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
#
# Run the ACL integration tests (Category=Integration) against the OpenZaak that is
# ALREADY running — works for any stack: oz-only (`make integration`), the standalone
# oz+nrc stack, or the full compose stack (the CI `verify-stack` job). Seeds a
# published BIG zaaktype (idempotent), then builds + runs the test image on the stack
# network, reaching OpenZaak by container IP (a single-label host isn't URL-valid;
# the runner can't reach published ports — see gitea-actions-gotchas.md §5/§6).
#
# Does NOT manage the stack lifecycle: the caller owns bring-up + teardown. Plain
# docker primitives only (docker/podman-portable). See ADR-0006.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/.." && pwd)"
# The OpenZaak API container, matched across compose projects + docker/podman naming
# (`<project>[-_]openzaak[-_]<n>`); the delimiters exclude oz-db / oz-redis / oz-init.
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
[ -n "$oz" ] || { echo "ERROR: no running OpenZaak container found — bring the stack up first" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$oz")"
oz_base="http://$oz_ip:8000"
echo ">> OpenZaak at $oz_base on network $net"
echo ">> seeding a published BIG zaaktype (idempotent)"
sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 \
python:3-slim python /seed.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> building the integration test image"
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
echo ">> running the ACL integration tests (inside the network)"
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration

View File

@@ -1,21 +1,14 @@
#!/usr/bin/env bash
#
# Run the ACL ↔ real-OpenZaak integration test (S-04a / #46) end to end.
# Local convenience: run the ACL integration test against a throwaway OpenZaak-only
# stack (fast iteration on the ACL gateway). Brings OpenZaak up, runs the shared
# stack-agnostic check (infra/run-acl-integration.sh), then always tears down.
#
# Everything that talks to OpenZaak runs *inside* the compose network and reaches
# it by service name (http://openzaak:8000) — the hosted CI runner can't reach the
# stack's published ports (sibling containers) and bind mounts don't reach the
# daemon either (gitea-actions-gotchas.md §1/§5). So we use only plain docker
# primitives (run / create / cp / build) — portable across docker compose (CI) and
# podman-compose (local), exactly like infra/seed-config.sh. See ADR-0006.
#
# Steps: bring OpenZaak up → wait for it healthy → seed a PUBLISHED BIG zaaktype
# (a seed container on the network) → build + run the test container on the network
# → always tear down.
# CI does not use this — there the full stack is brought up once and the same runner
# is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0006.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/.." && pwd)"
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
cleanup() {
@@ -29,40 +22,13 @@ bash "$here/seed-config.sh" oz
docker compose -f "$OZ_COMPOSE" up -d
echo ">> waiting for the OpenZaak API container to be healthy"
# Match the API container under both docker compose (openzaak-openzaak-1) and
# podman-compose (openzaak_openzaak_1) naming; the regex excludes oz-db / oz-redis.
api=""
for _ in $(seq 1 140); do
api="$(docker ps -q --filter 'name=openzaak[-_]openzaak' | head -1)"
if [ -n "$api" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$api" 2>/dev/null || true)"
[ "$status" = "healthy" ] && break
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
if [ -n "$oz" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$oz" 2>/dev/null || true)" = healthy ]; then
break
fi
sleep 3
done
[ -n "$api" ] || { echo "ERROR: OpenZaak API container never appeared" >&2; exit 1; }
[ "${status:-}" = "healthy" ] || { echo "ERROR: OpenZaak not healthy (status=${status:-none})" >&2; exit 1; }
[ -n "${oz:-}" ] || { echo "ERROR: OpenZaak never came up" >&2; exit 1; }
# The network the API container is attached to — joined by the seed + test below.
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$api" | head -1)"
# Reach OpenZaak by container IP, not by the service name. OpenZaak echoes its
# request Host into the self-referential URLs it returns, then validates those URLs
# with Django's URLValidator — which rejects a single-label host like `openzaak`
# ("Voer een geldige URL in") while accepting an IPv4 literal (and `localhost`).
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$api")"
oz_base="http://${oz_ip}:8000"
echo ">> OpenZaak healthy on network $net at $oz_base"
echo ">> seeding a published BIG zaaktype (OZ_PUBLISH=1, inside the network)"
sid="$(docker create --network "$net" \
-e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 \
python:3-slim python /seed_catalogus.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed_catalogus.py"
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> building the integration test image"
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
echo ">> running the integration tests (inside the network)"
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration
bash "$here/run-acl-integration.sh"

72
infra/run-notification-check.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
#
# Verify the OpenZaak → NRC notification path against an ALREADY-RUNNING oz+nrc stack
# (the standalone stack via `make verify-notifications`, or the full compose stack in
# the CI `verify-stack` job). Seeds a published BIG zaaktype (idempotent), registers
# an abonnement to a webhook sink, creates a zaak, and asserts the sink receives the
# `zaken`/`create` notification. All in-network, reaching services by container IP
# (single-label hosts aren't URL-valid; the runner can't reach published ports).
#
# Does NOT manage the stack lifecycle (the caller owns bring-up + teardown), but it
# cleans up the throwaway sink/driver it creates. Plain docker primitives only.
# See ADR-0007.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SINK_AUTH="Bearer notification-sink-token"
cleanup() { docker rm -f rr-nsink rr-nverify >/dev/null 2>&1 || true; }
trap cleanup EXIT
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
[ -n "$oz" ] && [ -n "$nrc" ] || { echo "ERROR: OpenZaak and/or NRC not running — bring the stack up first" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"
echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip"
echo ">> seeding a published BIG zaaktype (idempotent)"
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
python:3-slim python /seed.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> starting the webhook sink"
docker rm -f rr-nsink >/dev/null 2>&1 || true
sink="$(docker create --network "$net" --name rr-nsink -e "EXPECTED_AUTH=$SINK_AUTH" \
python:3-slim python /sink.py)"
docker cp "$here/notification-sink.py" "$sink:/sink.py" >/dev/null
docker start "$sink" >/dev/null
sleep 1
sink_ip="$(ip rr-nsink)"
echo ">> sink at $sink_ip:9000"
echo ">> registering abonnement + creating a zaak"
docker rm -f rr-nverify >/dev/null 2>&1 || true
drv="$(docker create --network "$net" --name rr-nverify \
-e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
-e "SINK_CALLBACK=http://$sink_ip:9000/" -e "SINK_AUTH=$SINK_AUTH" \
python:3-slim python /driver.py)"
docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
docker start -a "$drv"
zaak_url="$(docker logs rr-nverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
docker rm -f rr-nverify >/dev/null
[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
zaak_uuid="${zaak_url##*/}"
echo ">> zaak created: $zaak_url"
echo ">> waiting for the notification to reach the sink"
for _ in $(seq 1 30); do
if docker logs rr-nsink 2>&1 | grep -q "$zaak_uuid"; then
echo "OK — NRC delivered the zaken notification for zaak $zaak_uuid to the sink"
docker logs rr-nsink 2>&1 | grep "$zaak_uuid" | tail -1 | cut -c1-300
exit 0
fi
sleep 2
done
echo "FAIL — the sink never received a notification for zaak $zaak_uuid" >&2
echo "--- sink log ---" >&2; docker logs rr-nsink 2>&1 | tail -8 >&2
exit 1

View File

@@ -1,36 +1,29 @@
#!/usr/bin/env bash
#
# Verify the OpenZaak → Open Notificaties (NRC) notification path end to end (S-01-c,
# #56): a zaak created in OpenZaak is published to NRC and delivered to a subscriber.
# Local convenience: verify the OpenZaak → NRC notification path against a throwaway
# oz+nrc stack. Brings both up (notifications enabled), runs the shared stack-agnostic
# check (infra/run-notification-check.sh), then always tears down.
#
# Everything that talks to OpenZaak/NRC runs *inside* the compose network and reaches
# them by container IP — the hosted CI runner can't reach published ports (sibling
# containers, gitea-actions-gotchas.md §5) and OpenZaak/NRC reject a single-label host
# in URLs (Django URLValidator). Plain docker primitives only (docker/podman-portable),
# like infra/seed-config.sh and infra/run-integration.sh. See ADR-0007.
# CI does not use this — there the full stack is brought up once and the same runner
# is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0007.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
NRC_COMPOSE="$here/opennotificaties/docker-compose.yml"
SINK_AUTH="Bearer notification-sink-token"
cleanup() {
docker rm -f rr-nsink rr-nverify >/dev/null 2>&1 || true
docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" down --volumes >/dev/null 2>&1 || true
docker volume rm -f rr-oz-config rr-nrc-config >/dev/null 2>&1 || true
}
trap cleanup EXIT
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
wait_healthy() { # name-regex -> echoes container id
local re="$1" cid status
wait_healthy() { # name-regex
local re="$1" cid
for _ in $(seq 1 140); do
cid="$(docker ps -q --filter "name=$re" | head -1)"
if [ -n "$cid" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)"
[ "$status" = healthy ] && { echo "$cid"; return 0; }
if [ -n "$cid" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" = healthy ]; then
return 0
fi
sleep 3
done
@@ -42,52 +35,7 @@ bash "$here/seed-config.sh" oz nrc
OZ_NOTIFICATIONS_DISABLED=false docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" up -d
echo ">> waiting for OpenZaak + NRC to be healthy"
oz="$(wait_healthy 'openzaak[-_]openzaak')" || { echo "ERROR: OpenZaak not healthy" >&2; exit 1; }
nrc="$(wait_healthy 'nrc-web')" || { echo "ERROR: NRC not healthy" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"
echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip"
wait_healthy '[-_]openzaak[-_]' || { echo "ERROR: OpenZaak not healthy" >&2; exit 1; }
wait_healthy 'nrc-web' || { echo "ERROR: NRC not healthy" >&2; exit 1; }
echo ">> seeding a published BIG zaaktype"
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
python:3-slim python /seed.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> starting the webhook sink"
docker rm -f rr-nsink >/dev/null 2>&1 || true
sink="$(docker create --network "$net" --name rr-nsink -e "EXPECTED_AUTH=$SINK_AUTH" \
python:3-slim python /sink.py)"
docker cp "$here/notification-sink.py" "$sink:/sink.py" >/dev/null
docker start "$sink" >/dev/null
sleep 1
sink_ip="$(ip rr-nsink)"
echo ">> sink at $sink_ip:9000"
echo ">> registering abonnement + creating a zaak"
docker rm -f rr-nverify >/dev/null 2>&1 || true
drv="$(docker create --network "$net" --name rr-nverify \
-e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
-e "SINK_CALLBACK=http://$sink_ip:9000/" -e "SINK_AUTH=$SINK_AUTH" \
python:3-slim python /driver.py)"
docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
docker start -a "$drv"
zaak_url="$(docker logs rr-nverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
docker rm -f rr-nverify >/dev/null
[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
zaak_uuid="${zaak_url##*/}"
echo ">> zaak created: $zaak_url"
echo ">> waiting for the notification to reach the sink"
for _ in $(seq 1 30); do
if docker logs rr-nsink 2>&1 | grep -q "$zaak_uuid"; then
echo "OK — NRC delivered the zaken notification for zaak $zaak_uuid to the sink"
docker logs rr-nsink 2>&1 | grep "$zaak_uuid" | tail -1 | cut -c1-300
exit 0
fi
sleep 2
done
echo "FAIL — the sink never received a notification for zaak $zaak_uuid" >&2
echo "--- sink log ---" >&2; docker logs rr-nsink 2>&1 | tail -8 >&2
exit 1
bash "$here/run-notification-check.sh"