d6b3f9764f4d92e911ce25b8e115b765d7b4822d
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d6b3f9764f |
fix(e2e): bound the Playwright run and make a failed login say why (closes #161) (#165)
## What & why #161 is really two defects, and the second one is why the first was undiagnosable. **A wedged suite consumed the job, and took the post-mortem with it.** Nothing bounded the Playwright run, so CI stopped the job mid-suite — and `if: always()` does not survive that. Run 739's job metadata shows every step after the e2e as a **0-second failure** stamped at the kill: ``` 14 failure 09:48:17 -> 10:14:54 Self-service e2e (Playwright …) 15 failure 10:14:54 -> 10:14:54 verify-stack check summary ← if: always() 16 failure 10:14:54 -> 10:14:54 e2e spec summary ← if: always() 17 failure 10:14:54 -> 10:14:54 Dump container logs on failure ← if: failure() 18 failure 10:14:54 -> 10:14:54 Tear down ← if: always() ``` So the per-spec summary, the container-log dump and the teardown never ran, and the log lost whatever the killed process had buffered — leaving the single `✘` line the issue was filed from. `globalTimeout` now makes Playwright stop and *report*: the JSON report is written and those steps still get their turn. (A `timeout-minutes` on the job would have reproduced the same failure, so there isn't one.) The "~24-minute gap" is that kill, not necessarily a hang — note run 739 shows `run_attempt: 2`, and `concurrency.cancel-in-progress` kills an in-flight run on any re-run or push. **A login that never got its form ate the 90-second test timeout.** Playwright actions auto-wait until the *test* timeout, not `expect.timeout` — so a portal that serves its page but never bootstraps (its `config.json` fetch or the OIDC discovery behind `authorize()` failed; `main.ts` only `console.error`s) spent 90s to report `locator.fill: Test timeout of 90000ms exceeded`: the symptom, not the cause. That is catalogus.spec's 1.8 minutes. Both Keycloak forms are now asserted visible first, with a 20s budget and a message naming the step that never happened. Verified against a real blank-bootstrap portal — the beheer image served with a `config.json` that is not JSON — which fails in **20.2s** with *"the Keycloak login form never appeared — the portal did not reach Keycloak (check its config.json fetch and the OIDC discovery …)"*. **And the summary now says why.** The per-spec table (#136) rendered a verdict icon and nothing else, so even a surviving summary cost a log dive. Failing specs now carry their first error, flattened for a table cell (ANSI stripped, newlines collapsed, `|` escaped, clipped) — shape verified against a real @playwright/test 1.61 failing report, with a stdlib assert self-check on `make unit`. Closes #161 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass; refactor commit follows (login helper dedup). - [x] Conventional Commits referencing the issue (`refs #161`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (untouched). - [x] Docs updated — `docs/runbooks/gitea-actions-gotchas.md` §9. - [x] ADR — not needed: no boundary, dependency or coupling rule touched (test/CI infra only). - [x] Demo note — not applicable: nothing user-visible. ## Notes for reviewers **What this does not do: identify why the beheerder login failed that once.** The evidence to do that was destroyed by defect 2, which is what this PR fixes. The suite ran green here five times today (catalogus.spec 1.1–5.3s each) — but a local box is not the loaded CI runner, so that is weak evidence and I am not claiming the flake is gone. What changes is that the next occurrence is bounded and self-describing: it fails in 20s naming the failing step, the JSON report survives, and the summary prints the error. Please keep #161 in mind rather than treating this as proof. **Two follow-ups I did not pull into this PR:** - *All four portals show a permanently blank page if their startup fetch fails* — `main.ts` does `fetch('config.json').then(bootstrap).catch(console.error)`, one shot, no UI and no recovery. That is a real product gap (the deliberately-broken portal above is exactly what a user would see) and wants its own slice, not a test-infra PR. - `retries: 1` is untouched. CLAUDE.md §15 says flaky tests are fixed rather than retried, but removing retries while a real flake is unexplained would trade a rare red for a frequent one. Worth revisiting once #161 recurs (or doesn't) with the new diagnostics. The login-helper rename (`medewerker-login.ts` → `keycloak-login.ts`, citizen logins routed through `loginBurger`) is its own no-behaviour-change commit: the three citizen specs each duplicated the same three-line login, so guarding the login path once meant routing them through it first.Reviewed-on: #165 |
||
|
|
fff88ca23d |
ci: richer step reports via Gitea 1.27 job summaries (closes #136) (#137)
## What & why Use the standard `$GITHUB_STEP_SUMMARY` (Gitea 1.27 + act_runner 2.0.0) to surface on the run page what was previously buried in logs or download-only artifacts. All five quick wins from #136, **reporting-only** — no job's pass/fail gating changes. Closes #136 ### Items 1. **Mutation scores** — added the `markdown` reporter to each `stryker-config.json`; the `mutation` job concatenates each service's `mutation-report.md` into the summary (`if: always()`). Also reveals where `make mutation` stopped on a ratchet break. 2. **Per-frontend tests** — the 4 apps' `test` targets emit vitest JSON to `test-output/{projectName}.json` (Nx token interpolation); `infra/vitest-summary.py` renders a per-frontend table. 3. **Per-service unit tests** — `make unit` now writes TRX; `infra/trx-summary.py` renders a per-service table (service name derived from the `services/<name>/` path, so `domain` shows, not `big.tests`). 4. **e2e per-spec results** — Playwright writes `playwright-report.json`; `run-e2e-check.sh` copies it out of the container (capturing the exit code first); `infra/playwright-summary.py` renders a per-spec table. Turns a red e2e into a one-glance "which spec". 5. **verify-stack check table** — each live-stack check has an `id`; a final `if: always()` step tabulates each check's ✅/❌/⏭️. Docs: `gitea-actions-gotchas.md` §8 (version requirement + `$GITHUB_STEP_SUMMARY` guard + step-level `always()` note). ### Notes - Every summary write is guarded with `[ -n "${GITHUB_STEP_SUMMARY:-}" ]`, so it no-ops on an unsupported runner / locally. - New helper scripts are stdlib-only Python, matching the existing `infra/*.py` check scripts (no new dependency — a few lines of parsing rather than a test-logger package). - `TestResults/` and `test-output/` gitignored. - This is also the first PR-run exercising the #135 verify-stack fix end to end. ## Verified locally `make unit` (TRX) ✓ · 4 apps' vitest JSON ✓ · ACL Stryker markdown report ✓ · all four parsers + the two summary shell blocks ✓ · `ci.yaml` + `run-e2e-check.sh` syntax ✓. The rendered summaries themselves only appear on the run page — this PR's CI run is the end-to-end check. ## Definition of Done - [x] Each item writes to `$GITHUB_STEP_SUMMARY` (guarded), renders on the run page. - [x] No change to any job's pass/fail gating. - [x] Conventional Commits referencing #136 (one per item + docs). - [ ] CI green; summaries visible on the run. - [x] Runbook note (gotchas §8). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #137 |
||
|
|
849bf4723b |
ci: unstick verify-stack on Gitea 1.27 + act_runner 2.0.0 (closes #134) (#135)
## What & why After the Gitea 1.27 + act_runner 2.0.0 upgrade, `verify-stack` never starts: the run sits in `waiting` forever with no logs for that job, while the other five jobs pass — so `main` stays pending/red (P0). See #134. Closes #134 ### Root cause `verify-stack` was the only job gated by a status-function `if` on top of `needs`: ```yaml verify-stack: needs: [mutation] if: ${{ !cancelled() }} ``` Gitea 1.27 reworked cancellation/aggregation so that `always()`/`cancelled()`-gated `needs` jobs route through a new transitional **`Cancelling`** state + server↔runner **capability negotiation** ("Requires Gitea Runner 2.0.0"). On this 1.27 + 2.0.0 pairing that handshake doesn't resolve, so the job is never dispatched and never leaves `waiting`. Plain jobs (no `if`/`needs`) are unaffected — exactly the observed pattern. It worked pre-upgrade (old runner). ### Fix Drop the `if: ${{ !cancelled() }}`; keep `needs: [mutation]`. Default `if: success()` dispatches normally and still serialises the two memory-heavy jobs (OOM avoidance, #126). **Trade-off:** the `!cancelled()` (added in #127) let verify-stack run even when the mutation ratchet fails. Now a failing mutation skips verify-stack; the fix-and-re-push re-run exercises it, so the signal isn't lost — just deferred to the green-mutation run. If we later want both signals on one run, serialise via a `concurrency` group rather than `needs` + `always()`. Documented as §7 in `docs/runbooks/gitea-actions-gotchas.md`. ## Note on the stuck run Run 582 (the #133 merge) will **not** clear itself and must be force-cancelled from the Actions UI (plain cancel can also stall on this version, gitea#35782). This PR's own run is the first real test of the fix — if `verify-stack` dispatches and runs here, the fix holds. ## Definition of Done - [x] Linked issue (#134). - [x] Conventional Commit referencing the issue. - [ ] CI green — this PR's run is the verification (verify-stack must dispatch). - [x] Runbook updated (gotchas §7). - [ ] Closed by the merging PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #135 |
||
|
|
ac874bf746 |
ci(mutation): make Stryker report upload best-effort (refs #62)
The Gitea artifact backend returns 500 to actions/upload-artifact@v3 (server-side, distinct from the @v4 GHES guard). With if: always() that 500 failed the whole mutation job even though the ratchet passed — red on main and on every PR. Mark the three report uploads continue-on-error: true so the mutation *gate* stays the Stryker ratchet (make mutation's exit code), not the report upload. Documented in gitea-actions-gotchas.md §4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
f9e123dfcb |
docs(infra): tighten gitea-actions-gotchas, add local compose (refs #30)
Restructure for scannability: a shared root-cause intro, a quick-reference table (gotcha → fix → where), and consistent Symptom/Why/Fix sections with tighter prose. Documents infra/docker-compose.local.yml as the no-make/Windows path and drops the now-stale "no bind mounts remain" line (the local compose uses them, which is fine locally). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dda4c58e1c |
fix(infra): portable health poll instead of compose --wait (refs #30)
`make smoke` errored locally because podman-compose doesn't implement
`docker compose up --wait` (`unrecognized arguments: --wait`).
Replace the `--wait` step with infra/wait-healthy.sh, which polls each durable
health-checked service ($(WAIT_SVCS)) via `docker ps` + `docker inspect
'{{.State.Health.Status}}'`. This:
- works on both docker compose (CI) and podman-compose (local) — only plain
docker primitives, no `--wait`;
- reads the in-container healthcheck, so it needs no host port access (the CI
runner can't reach published ports);
- ignores the one-shot init jobs, sidestepping the "--wait fails when a
consumer-less one-shot exits 0" issue (flowable-init).
Verified on podman-compose: wait-healthy.sh reports bff healthy (rc=0); podman
exposes .State.Health.Status (starting -> healthy) and the name filter matches
both `_` and `-` container naming.
Docs: gitea-actions-gotchas.md updated (the two `--wait` sections folded into one
"portable health poll" section).
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> |
||
|
|
6d8e1d0830 |
refactor(infra): bake config via dockerfile_inline, drop Dockerfile files (refs #30)
Replaces the three standalone Dockerfiles (openzaak, opennotificaties, keycloak) with `build.dockerfile_inline` recipes in the compose files, so the config bake has no separate Dockerfile artifacts to maintain. Behaviour is identical: each derived image still COPYies its config in. - oz-init / keycloak / flowable-init: 2-line inline Dockerfiles. - Open Notificaties needs no bake at all now — nrc-init runs migrations only, so all NRC services use the plain base image (removes a whole derived image). Why dockerfile_inline and not `docker cp` into named volumes: docker cp avoids images entirely but needs `docker compose create`, which podman-compose (the local dev runtime) does not implement — it would break `make openzaak-up` etc. locally. dockerfile_inline works on both podman-compose and the CI runner (verified both: oz-init + keycloak inline builds locally; flowable-init inline has been green on CI since run 27). Docs updated: gitea-actions-gotchas.md and 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>
|