ci/175-deploy-on-merge
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc036d53d5 |
ci(deploy): deploy the stack to Talos on merge to main (refs #175)
CI / k8s (pull_request) Successful in 5s
CI / lint (pull_request) Successful in 1m25s
CI / build (pull_request) Successful in 1m22s
CI / unit (pull_request) Successful in 1m24s
CI / frontend (pull_request) Successful in 1m45s
CI / mutation (pull_request) Successful in 3m1s
CI / verify-stack (pull_request) Successful in 6m22s
The chart has been deployable by hand since #25 and linted in CI since #168; this makes a merged PR actually ship it to the lab server's Talos VM. Neither the Kubernetes API nor the in-cluster registry is publicly reachable, so the job forwards 6443, 30500 and 30141 over the same SSH hop into the Fedora host that the Gitea-runner pipeline uses. That splits the registry into two names for one store: images are pushed through the tunnel to localhost:30500, and the node pulls them from its own NodePort — the address its registry-mirror patch trusts over plain HTTP. It deploys with `make k8s-reseed` rather than `make k8s-up`: the bootstrap Jobs are idempotent, and deleting them first is what stops a changed Job template from wedging `helm upgrade`. The nine deployments are then rolled explicitly, because `dev` is a mutable tag and helm sees an unchanged pod template. PR CI is the merge gate, so this workflow does not re-run the checks. Deploys queue instead of cancelling: a `helm upgrade` killed half-way leaves the release in `pending-upgrade` and needs unwedging by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9d7e8e5b65 |
ci(k8s): gate the Helm chart in CI + a compose↔chart drift check (closes #168) (#171)
CI / k8s (push) Successful in 5s
CI / lint (push) Successful in 1m27s
CI / build (push) Successful in 1m22s
CI / unit (push) Successful in 1m12s
CI / frontend (push) Successful in 2m7s
CI / mutation (push) Successful in 3m9s
CI / verify-stack (push) Successful in 6m20s
## What & why The Helm chart landed in #167 with two gaps written into ADR-0033: `make k8s-lint` existed but no CI job ran it, and *"a second deployment description to keep in step with compose — nothing enforces that today; a drift check belongs in CI (follow-up)"*. Both are closed here. **`make k8s-drift`** (`infra/helm/check-drift.py`, stdlib only) compares what each stack actually deploys rather than diffing two files that differ by design: workload names and resolved container images, taken from `docker compose config --format json` and a rendered chart. The six differences that exist today are declared in `DEVIATIONS` with the reason each was forced — the four `*-init` Django services folded into their web pods, and the two bootstrap Jobs compose runs from the host — so only a *new* difference fails. **A `k8s` CI job** runs `k8s-lint` then `k8s-drift` on every push and PR. No cluster, no marketplace action: helm is fetched as the pinned static binary the Talos runbook already gives developers. Closes #168 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation — the red commit reports all six real differences; the green commit declares them. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #168`). - [x] CI green — awaiting the run on this PR (`make k8s-lint` and `make k8s-drift` pass locally). - [x] `docker compose up` unaffected — no service, image or compose file is touched. - [x] Docs updated — `docs/runbooks/ci.md` (job table + the one place local and CI now differ), `docs/runbooks/kubernetes-talos.md` §7/§"not ported", and ADR-0033's cost note. - [x] No ADR needed: no new dependency (python stdlib, and helm/docker were already prerequisites of the `k8s-*` targets), no boundary moved, no §8 rule bent. - [x] Not user-visible, so no demo note. ## Notes for reviewers Verified by hand that both drift classes fail the check, not just that it passes today: - bumping `OPENZAAK_TAG` in compose alone → reports `openzaak` and `oz-celery` with both image strings; - adding a workload to `values.yaml` alone → reports it by name. Deliberate limits (there is a `ponytail:` note in the script): - **Names and images only**, as sets — no per-workload env, ports or volumes. Those differ by design in four documented places, so comparing them would mean re-encoding every deviation field by field for very little more signal. - **The three observability workloads are rendered with `enabled=true`** by the check, even though both stacks default them off, so their images can't drift unwatched. - **`k8s-lint`/`k8s-drift` are not in `make ci`**, to avoid making `helm` a hard prerequisite for everyone. That is now the only local/CI difference; it's called out in `docs/runbooks/ci.md`. Follow-ups filed while reviewing the chart, not addressed here: #169 (the published docs omit every ADR after 0010 and all runbooks but `ci.md`) and #170 (the production-posture ADR #25 asked for — secrets are still plain text in `values.yaml`).Reviewed-on: #171 |
||
|
|
d49443353e |
refactor(ci): one verify-stack stage for all live-stack checks (closes #58) (refs #46 #56)
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> |
||
|
|
a256db1a23 |
arch(infra): ADR-0007 + runbooks for the OZ→NRC notification wiring (refs #56)
CI / lint (pull_request) Successful in 52s
CI / build (pull_request) Successful in 40s
CI / unit (pull_request) Successful in 49s
CI / mutation (pull_request) Successful in 1m35s
CI / integration (pull_request) Successful in 3m24s
CI / notifications (pull_request) Successful in 3m14s
CI / compose-smoke (pull_request) Successful in 4m2s
Records the wiring decision (AC-delegated auth, required celery-beat) and the two non-obvious gotchas: single-label hosts aren't URL-valid (reach services by IP) and abonnement callbacks must enforce auth. Documents the new notifications CI job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4474585606 |
ci(acl): run the ACL integration test in CI inside the compose network (closes #55) (refs #46)
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> |
||
|
|
3829cb0b68 |
ci(acl): keep the integration lane local-only; document the runner gap (refs #46)
The hosted Gitea runner starts the OpenZaak stack as sibling containers via the host daemon, so a process on the runner can't reach the published ports — the seed and dotnet test get Connection refused on localhost:8000. Drop the (non-working) integration CI job; make integration stays the local / host-runner gate. Document the limitation in gitea-actions-gotchas.md §5 and the CI runbook, and track running it inside the compose network in #55. ADR-0006 updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
855a5565fe |
ci(acl): run the ACL integration test as a Gitea Actions job (refs #46)
CI / integration (pull_request) Failing after 5m16s
CI / lint (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 46s
CI / mutation (pull_request) Successful in 1m37s
CI / build (pull_request) Successful in 41s
CI / compose-smoke (pull_request) Successful in 4m11s
New integration job: setup-dotnet + make integration (stack up, OZ_PUBLISH=1 seed, Integration-category tests, tear down), with on-failure log dump + teardown like compose-smoke. Documents the job and the new make target in the CI runbook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f3dd31925 |
fix(ci): pin upload-artifact to @v3 — @v4 refuses to run on Gitea (refs #47)
The artifact step failed the mutation job: upload-artifact@v4 bundles
@actions/artifact v2, which hard-aborts on any non-github.com server ("not
supported on GHES"), even though Gitea 1.25 stores artifacts fine. @v3 uses the
older protocol Gitea speaks and has no GHES guard — a drop-in swap (same inputs).
Document it as gotcha §4 and correct the CI runbook note.
Refs #47.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
347713766e |
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> |
||
|
|
7ecc184111 |
arch(acl): ADR-0005 adopt Stryker.NET for mutation testing (refs #47)
Record the decision to adopt Stryker.NET (pinned local tool, solution mode on Acl.slnx) and to set the first repo-wide mutation baseline on the ACL: observed 95%, enforced break threshold 90%. Document the ratchet, local run, and report location in the CI runbook; add the ADR to the docs nav. Proposed in #51 (adr-proposal). Refs #47. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e87113da24 |
feat(infra): add bind-mount local compose for no-make/Windows dev (refs #30)
Adds infra/docker-compose.local.yml: the same full stack as the canonical infra/docker-compose.yml, but the three config inputs (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are bind-mounted from the repo instead of streamed into external volumes by seed-config.sh. Bind mounts are valid here because a local daemon (Docker Desktop on Windows/ macOS, or rootless Podman on Linux) can see the working directory — the seed dance only exists for the containerized CI runner, where it can't. So this file runs with a plain `docker compose up`: no make, no seed step, no bash. docker compose -f infra/docker-compose.local.yml up -d --build docker compose -f infra/docker-compose.local.yml up -d --build --wait # Docker Desktop Linux/macOS convenience wrappers `make local` / `make local-down` added too. Verified on podman: Keycloak boots from this file and imports the bind-mounted realms (digid realm returns 200). docs/runbooks/ci.md documents the Windows path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b349dff496 |
refactor(infra): use upstream images verbatim, seed config via docker cp (refs #30)
Drops the inline-build images for the upstream services. The compose now references the published images directly (openzaak/open-zaak, openzaak/open-notificaties, keycloak, curl, flowable-rest) with no build for them, and the config they need is streamed into external named volumes by infra/seed-config.sh: rr-oz-config -> oz-init /app/setup_configuration (data.yaml) rr-kc-realms -> keycloak /opt/keycloak/data/import (realm exports) rr-fl-bpmn -> flowable-init /work (registratie.bpmn) How: the seeder creates each volume, `docker create`s a throwaway helper that mounts it, `docker cp`s the files in, and removes it. docker cp streams over the Docker API, so it works in Docker-in-Docker (the CI runner) where bind mounts mount empty. It uses plain `docker create`/`cp` — NOT `docker compose create`, which podman-compose (local dev) lacks. `external: true` fixed names keep the volumes identical across docker compose and podman-compose. Consequence: bare `docker compose up` no longer self-seeds, so use `make up` (seeds then starts). Every `*-up` target seeds first; `*-down` removes the external volume. acl/bff are still built (they're our apps, not upstream images). Verified end-to-end on podman-compose: `make keycloak-up` seeds rr-kc-realms, the upstream Keycloak mounts it, and --import-realm imports all four realms (digid realm returns 200). Seeder runs in ~2s. Docs updated: gitea-actions-gotchas.md, ci.md, openzaak.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0aa22c80b |
fix(infra): smoke waits on durable services, not the whole project (refs #30)
Run 28 got the full stack healthy but `compose-smoke` still failed. The last
compose line before the error was:
container infra-flowable-init-1 exited (0)
`docker compose up --wait` treats a service that exits as a failure of the
"stay running" condition unless something depends on it via
`service_completed_successfully`. oz-init/nrc-init are fine (openzaak/nrc-web
depend on them), but flowable-init deploys the BPMN and exits 0 with no
dependant, so whole-project `--wait` failed the instant it finished — even
though everything else was healthy and nrc-init now exits 0.
Smoke now:
1. `up -d` starts the full stack (one-shots run + deploy as before), then
2. `up -d --wait <WAIT_SVCS>` waits only for the durable health-checked
services (openzaak nrc-web acl bff).
Also drops the external `curl localhost:8080/health`: the containerized CI
runner can't reach published host ports at localhost, and each service's
healthcheck already runs inside its container — so `--wait` succeeding IS the
smoke. Documented in docs/runbooks/gitea-actions-gotchas.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9ff7937055 |
fix(infra): bake config into images so compose-smoke passes on CI (refs #30)
Root cause of the compose-smoke failure (found in the runner logs):
oz-init-1 | CommandError: Yaml file
`/app/setup_configuration/data.yaml` does not exist.
The ubuntu-latest runner runs the job inside a container, so
`docker compose up` starts the stack as SIBLING containers via the host
daemon. A relative bind mount (./openzaak/setup_configuration) resolves to
a path inside the job container that the daemon can't see, so Docker mounts
an empty dir and the init container can't find data.yaml. The same trap hit
nrc-init (data.yaml), flowable-init (the BPMN) and keycloak (realm import).
Fix: bake the assets into small derived images instead of bind-mounting:
- infra/openzaak/Dockerfile -> register-referentie/openzaak:dev
- infra/opennotificaties/Dockerfile-> register-referentie/opennotificaties:dev
- infra/keycloak/Dockerfile -> register-referentie/keycloak:dev
- flowable-init: build.dockerfile_inline bakes workflows/registratie.bpmn
Base versions stay build args (OPENZAAK_TAG / OPENNOTIFICATIES_TAG), so the
pinning is unchanged. Applied to both the consolidated compose and the
per-service composes, so local Podman and CI use one mechanism — no bind
mounts, no SELinux `:z`, no world-readable requirement.
Verified locally: `podman build` of the OpenZaak and BPMN images produces
the file at the expected in-container path.
Docs: docs/runbooks/gitea-actions-gotchas.md explains the DinD bind-mount
trap and the bake fix; openzaak.md and ci.md point at it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f32fc4e8c0 |
ci(infra): switch runner label to ubuntu-latest (refs #30)
Self-hosted respellion-linux runner not required — Gitea's hosted ubuntu-latest runner has Docker + Compose v2 out of the box, so make smoke works without any manual registration step. Updates docs/runbooks/ci.md to reflect the new runner label and removes the act_runner self-hosted setup as the primary path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
d4a89e6e62 | ci: Gitea Actions pipeline + runner runbook (refs #30) (#37) |