Compare commits

..

1 Commits

Author SHA1 Message Date
202ee34ef3 chore: remove bootstrap scripts from main
The tools/seed-gitea.sh seeder + README reached main via an earlier chore
commit, but the agreed decision is to manage Gitea entirely with the tea
CLI and not keep bespoke scripts in the repo. Remove them; the backlog
they seeded already exists in Gitea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:40:05 +02:00
26 changed files with 13 additions and 1207 deletions

View File

@@ -1,24 +0,0 @@
---
name: ADR proposal
about: Propose a decision that needs recording before coding (CLAUDE.md §14)
title: "ADR: "
labels:
- type:adr-proposal
---
**Decision to be made:**
**Context / forces:** <!-- what makes this non-obvious; constraints, trade-offs -->
**Options considered:**
1.
2.
**Proposed option + why:**
**Consequences:** <!-- what becomes easier/harder; what we commit to -->
**Coupling rules touched (CLAUDE.md §8):** <!-- none, or which and why -->
> On acceptance, the ADR file (`docs/architecture/adr-NNNN-title.md`, Nygard
> template) lands in the PR that implements the decision.

View File

@@ -1,21 +0,0 @@
---
name: Bug
about: Something behaves incorrectly
title: ""
labels:
- type:bug
---
**What happened:**
**What you expected:**
**Steps to reproduce:**
1.
2.
**Environment:** <!-- branch/commit, OS, container engine, anything relevant -->
**Logs / evidence:**
**Suspected area:** <!-- e.g. area:bff, area:acl — add the matching area label -->

View File

@@ -1,31 +0,0 @@
---
name: Slice (user story)
about: A backlog slice — independently demoable, encodes the Definition of Done
title: "S-NN · "
labels:
- type:slice
---
**Outcome:** <!-- one sentence; user-visible if possible -->
**Acceptance:**
<!-- Gherkin scenarios or testable assertions -->
-
**Touches:** <!-- services and folders -->
**Out of scope:** <!-- explicit non-goals -->
## Definition of Done
- [ ] This linked Gitea issue exists and is on the right milestone.
- [ ] Failing test written and committed first (`test(scope): … (refs #NN)`).
- [ ] Implementation makes the test pass (`feat(scope): … (refs #NN)`).
- [ ] Refactor commit follows if structure improved.
- [ ] Conventional Commit messages referencing this issue.
- [ ] All Gitea Actions CI jobs green (or `make ci` green while no runner exists).
- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes.
- [ ] Docs touched if behaviour, contracts, or operations changed.
- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made.
- [ ] Demo note appended to `docs/demo-script.md` if the slice is user-visible.
- [ ] This issue closed by the merging PR (`closes #NN`).

View File

@@ -1,23 +0,0 @@
<!-- Title: Conventional Commit style, e.g. feat(bff): … (closes #NN) -->
## What & why
<!-- Summary of the change and the slice/bug it addresses. -->
Closes #
## Definition of Done
- [ ] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation.
- [ ] Implementation makes the test pass; refactor commit if structure improved.
- [ ] Conventional Commits referencing the issue (`refs #NN`).
- [ ] CI green — all Gitea Actions jobs (or `make ci` green while no runner exists).
- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes.
- [ ] Docs updated if behaviour, contracts, or operations changed.
- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made.
- [ ] Demo note in `docs/demo-script.md` if user-visible.
## Notes for reviewers
<!-- Anything that helps review: trade-offs, follow-ups, known gaps. -->

View File

@@ -1,50 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
# 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: respellion-linux
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: make lint
build:
runs-on: respellion-linux
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: make build
unit:
runs-on: respellion-linux
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: make unit
compose-smoke:
runs-on: respellion-linux
steps:
- uses: https://github.com/actions/checkout@v4
- run: make smoke

4
.gitignore vendored
View File

@@ -22,10 +22,6 @@ node_modules/
dist/ dist/
.angular/ .angular/
# Python / MkDocs
.venv/
site/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View File

@@ -1,20 +0,0 @@
# Changelog
All notable changes to this project. Generated from Conventional Commits by git-cliff.
## Unreleased
### CI
- Gitea Actions pipeline + runner runbook (refs #30) (#37)
### Chores
- Add idempotent Gitea backlog seeder
- Remove bootstrap scripts from main (#35)
### Documentation
- Split S-00 into sub-slices (refs #1) (#33)
### Features
- Placeholder BFF + /health endpoint (closes #28) (#34)
- Containerize BFF + compose-up smoke (closes #29) (#36)

112
Makefile
View File

@@ -1,112 +0,0 @@
# 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 := services/bff/Bff.slnx
COMPOSE := infra/docker-compose.yml
HEALTH_URL := http://localhost:8080/health
OZ_COMPOSE := infra/openzaak/docker-compose.yml
OZ_BASE := http://localhost:8000
NRC_COMPOSE := infra/opennotificaties/docker-compose.yml
NRC_BASE := http://localhost:8001
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 smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down help
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
ci: lint build unit smoke
## lint: verify formatting (no changes)
lint:
dotnet format $(SLN) --verify-no-changes
## build: release build
build:
dotnet build $(SLN) -c Release
## unit: run unit tests
unit:
dotnet test $(SLN) -c Release
## smoke: compose up (wait for healthy), curl /health, then tear down
smoke:
docker compose -f $(COMPOSE) up -d --build --wait
bash -c 'curl -fsS $(HEALTH_URL); rc=$$?; docker compose -f $(COMPOSE) down --volumes; exit $$rc'
## down: stop and remove the local stack
down:
docker compose -f $(COMPOSE) down --volumes
## changelog: regenerate CHANGELOG.md from Conventional Commits (git-cliff)
changelog:
git-cliff --output CHANGELOG.md
## openzaak-up: start the OpenZaak stack (migrations run on first start)
openzaak-up:
docker compose -f $(OZ_COMPOSE) up -d
## openzaak-smoke: start OpenZaak, then assert it is up with auth enforced
openzaak-smoke:
docker compose -f $(OZ_COMPOSE) up -d
@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
## stack-up: start OpenZaak + Open Notificaties together (shared network)
stack-up:
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
## help: list available targets
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'

View File

@@ -41,32 +41,22 @@ For the architecture rationale, see [docs/PRD.md §3](docs/PRD.md) and [docs/arc
**Prerequisites** **Prerequisites**
- .NET 10 SDK (for `make lint/build/unit`) - Docker Engine (or Docker Desktop) with Compose v2
- A container engine with Compose v2 — Docker, or rootless Podman (see [docs/runbooks/ci.md](docs/runbooks/ci.md) for the Podman + Compose-provider setup) - ~8 GB free RAM, ~10 GB free disk
- `make`, `curl`, `git` - Bash or PowerShell
- ~4 GB free RAM, ~5 GB free disk (grows as services land)
**Clone** **Bring the stack up**
```bash ```bash
git clone git@git.labs.respellion.tech:eho/register-referentie.git git clone https://gitea.respellion.local/respellion/register-reference.git
cd register-referentie cd register-reference
cp .env.example .env # edit if you change ports
docker compose -f infra/docker-compose.yml up -d
``` ```
**Wired today (Iteration 0):** only the placeholder BFF exists so far. Get to green in under 10 minutes — run the full check gate, or just the running service: Health checks should be green within ~3 minutes on a developer machine. If something fails, see [docs/runbooks/local-startup.md](docs/runbooks/local-startup.md).
```bash **Default URLs**
make ci # lint + build + unit + container smoke — the CI gate
```
```bash
docker compose -f infra/docker-compose.yml up -d --build --wait
curl http://localhost:8080/health # -> Healthy
```
`--wait` exits non-zero unless the container reports healthy, so it doubles as the compose-up smoke test. The remaining services and the URLs below land in later slices.
**Target service URLs** *(most land in later slices)*
| Service | URL | | Service | URL |
|---|---| |---|---|
@@ -74,7 +64,7 @@ curl http://localhost:8080/health # -> Healthy
| Openbaar register | http://localhost:4201 | | Openbaar register | http://localhost:4201 |
| Behandel-portal | http://localhost:4202 | | Behandel-portal | http://localhost:4202 |
| Beheer-portal | http://localhost:4203 | | Beheer-portal | http://localhost:4203 |
| BFF | http://localhost:8080 | | BFF | http://localhost:5000 |
| OpenZaak | http://localhost:8000 | | OpenZaak | http://localhost:8000 |
| Open Notificaties | http://localhost:8001 | | Open Notificaties | http://localhost:8001 |
| Flowable | http://localhost:8080 | | Flowable | http://localhost:8080 |
@@ -83,12 +73,10 @@ curl http://localhost:8080/health # -> Healthy
Test credentials, BSNs, and personas: see [docs/synthetic-data.md](docs/synthetic-data.md). Test credentials, BSNs, and personas: see [docs/synthetic-data.md](docs/synthetic-data.md).
**Build the docs site** **Re-seed synthetic data**
```bash ```bash
python3 -m venv .venv && .venv/bin/pip install mkdocs-material ./tools/seed.sh # or pwsh ./tools/seed.ps1
.venv/bin/mkdocs serve # live preview at http://localhost:8000
.venv/bin/mkdocs build # static site in ./site
``` ```
--- ---

View File

@@ -1,45 +0,0 @@
# git-cliff configuration — generates CHANGELOG.md from Conventional Commits.
# Run via `make changelog`. See https://git-cliff.org.
[changelog]
header = """
# Changelog
All notable changes to this project. Generated from Conventional Commits by git-cliff.\n
"""
body = """
{% if version %}\
## {{ version }} — {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## Unreleased
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}\
- {{ commit.message | upper_first }}{% if commit.breaking %} **[BREAKING]**{% endif %}
{% endfor %}\
{% endfor %}\n
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
protect_breaking_commits = true
tag_pattern = "v[0-9]*"
# CalVer tags: YYYY.MM.PATCH
filter_commits = false
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Tests" },
{ message = "^ci", group = "CI" },
{ message = "^build", group = "Build" },
{ message = "^arch", group = "Architecture" },
{ message = "^chore", group = "Chores" },
{ message = ".*", group = "Other" },
]

View File

@@ -1,58 +0,0 @@
# ADR-0001: Loose coupling to upstream Common Ground modules
- **Status:** Accepted
- **Date:** 2026-06-03
- **Deciders:** Respellion engineering
- **Template note:** This is the first ADR and doubles as the worked example of
the Nygard template. Copy its shape for new ADRs (`adr-NNNN-title.md`).
## Context
This reference application orchestrates several upstream Common Ground modules —
OpenZaak (ZGW APIs), Open Notificaties (NRC), Objecten/Objecttypen, Flowable,
Keycloak. Each is an independently developed, independently deployed peer. The
temptation in a demo is to reach straight into a peer's database or couple to its
internal schema to move faster. That coupling is exactly what makes Common Ground
landscapes brittle and un-upgradeable in practice.
We need a stance, recorded up front, on how our services may talk to these peers.
## Decision
**We integrate with upstream modules only through their documented public APIs, and
we isolate that integration behind explicit anti-corruption boundaries.**
Concretely (mirrors CLAUDE.md §8):
1. The **ACL** is the only code that talks to ZGW APIs; no other service constructs
ZGW URLs.
2. The **Workflow Client** is the only code that talks to Flowable; BPMN models hold
no OpenZaak knowledge.
3. **Portals talk only to the BFF** — never directly to a backend or a peer module.
4. **No direct database access across services or to any peer.** Each service owns
its schema; the Read Projection is a rebuildable derived artefact.
5. **Idempotency at every event boundary** (the Event Subscriber tolerates duplicate
and out-of-order NRC events).
Bending any of these is an ADR-worthy moment (CLAUDE.md §14): stop and open an
`adr-proposal` issue first.
## Consequences
**Positive**
- Upstream modules can be upgraded or swapped behind their APIs without rippling
through our services.
- Coupling is visible and minimal — anti-corruption code lives in one named place.
- The architecture teaches the Common Ground pattern by enforcing it.
**Negative / costs**
- More indirection: a translation layer (ACL, Workflow Client) instead of direct
calls. Accepted — it's the point.
- Eventual consistency across aggregates must be designed for, not assumed away.
**Follow-up**
- Each integration slice that touches a boundary references this ADR; new boundary
decisions get their own ADR.

View File

@@ -1,53 +0,0 @@
# ADR-0002: BIG catalogus design and OpenZaak seeding
- **Status:** Accepted
- **Date:** 2026-06-03
- **Deciders:** Respellion engineering
- **Relates to:** S-01 (#2)
## Context
S-01 needs a reproducible `BIG` catalogus in OpenZaak with a **lean** `BIG-registratie`
zaaktype (only schema-mandatory fields) plus a `bsn` eigenschap, and a JWT client that
can list zaaktypen. We had to decide *how* to provision this idempotently at startup.
Findings from the OpenZaak image (`openzaak/open-zaak:latest`):
- `setup_configuration` (run by the init container) is declarative and idempotent, with
steps for **JWT secrets** and **applicaties** (`vng_api_common_credentials`,
`vng_api_common_applicaties`) — but **no step for catalogi/zaaktypen**.
- Catalogus/zaaktype/eigenschap can only be created through the **ZTC REST API**.
- Publishing a zaaktype requires ≥1 roltype, ≥1 resultaattype and ≥2 statustypen.
## Decision
1. **Provision the JWT client declaratively** via `infra/openzaak/setup_configuration/data.yaml`:
a `JWTSecret` (`big-reference-seed` / dev secret) and an `Applicatie` with
`heeft_alle_autorisaties: true`. Idempotent, runs in the init container.
2. **Seed the catalogus/zaaktype/eigenschap via the ZTC API** with an idempotent,
stdlib-only script (`infra/openzaak/seed_catalogus.py`, `make openzaak-seed`). It mints
a ZGW JWT from the provisioned client and matches existing objects (by `domein` /
`identificatie` / `naam`, querying `status=alles` so concepts are seen) before creating.
3. **Keep the zaaktype a CONCEPT (not published).** Publishing pulls in roltypen,
statustypen and resultaattypen, which go beyond "schema-mandatory"; those arrive with
the workflow/zaak slices that actually need a published type. Listing uses `status=alles`.
4. **Disable outbound notifications** (`NOTIFICATIONS_DISABLED=true`) until Open Notificaties
(NRC) lands in S-01-c — otherwise every ZTC write 500s trying to notify.
5. **Fixed dev values:** RSIN `517439943` (elfproef-valid test value); the JWT secret is
dev-only and documented as such.
## Consequences
- **Reproducible & version-robust:** the API-driven seed doesn't depend on fixture PKs or
a catalogi `setup_configuration` step that may change between versions.
- **Teaches the pattern:** the seed talks to OpenZaak exactly the way the ACL will later —
through the documented ZGW API, with a JWT (ADR-0001).
- The seed is a script, but a **data loader is explicitly anticipated** (PRD §8); it lives
under `infra/openzaak/`, not as ad-hoc tooling.
- **Follow-ups:** re-enable notifications when NRC is up (S-01-c); publish the zaaktype (add
the related types) when a slice needs to create real zaken; pin the OpenZaak image tag.
## Alternatives considered
- **Fully declarative in `data.yaml`** — rejected: no catalogi/zaaktype step exists.
- **Django `loaddata` fixture** — rejected: brittle, tied to model PKs and the exact image
version; bypasses the API the rest of the system uses.

View File

@@ -1,52 +0,0 @@
# Working with Gitea: issues, milestones, PRs
Gitea is the **system of record** (CLAUDE.md §7). `BACKLOG.md` is a human-readable
mirror of the active milestone — when they disagree, Gitea wins.
## Issues
- Open issues from the templates in `.gitea/ISSUE_TEMPLATE/`:
- **Slice** — a backlog user story (`S-NN · …`), encodes the Definition of Done.
- **Bug** — a defect.
- **ADR proposal** — a decision to record before coding (CLAUDE.md §14).
- Every issue gets `type:*` plus the relevant `area:*` label(s), and is assigned to
its iteration **milestone** (`Iteration N — …`).
- Splitting a slice that grew too big: see CLAUDE.md §13 and the "How to split a
slice" section of `BACKLOG.md`.
## Branches & commits
- Trunk-based: short-lived branches off `main`, squash-merged. Never push to `main`.
- Branch name: `<type>/<issue-number>-<slug>`, e.g. `feat/28-bff-health`.
- Conventional Commits, each referencing its issue: `feat(bff): … (refs #28)`.
- TDD order: the `test(...)` red commit precedes the `feat(...)` green commit.
## Pull requests
- Open with `tea pr create` (the Gitea CLI) or the web UI; the body uses
`.gitea/PULL_REQUEST_TEMPLATE.md` and its DoD checklist.
- The merging PR closes its issue via `closes #NN` in the squash-commit body.
Work that isn't finished (e.g. CI green pending a runner) uses `refs #NN` and the
issue stays open.
- A PR needs a linked issue. Don't open one without it.
## CI gate
Until a self-hosted `respellion-linux` runner is registered, `make ci` is the gate
(it runs the same checks the workflow does). See [runbooks/ci.md](runbooks/ci.md).
## CLI cheatsheet (`tea`)
```bash
tea issues list --state open # backlog
tea issue create --title "S-NN · …" --labels type:slice,area:bff --milestone "Iteration 1 — Walking Skeleton"
tea pr create --base main --head <branch> --title "…" --description "… closes #NN"
tea pr list # open PRs
tea pr merge <n> --style squash # merge (after review)
```
## Changelog & releases
`CHANGELOG.md` is generated from commits by `git-cliff` (`make changelog`), refreshed
on tag. Versioning is **CalVer** `YYYY.MM.PATCH`; releases are published via Gitea
Releases.

View File

@@ -1,23 +0,0 @@
# register-referentie
A reference application demonstrating Respellion's Common Ground architecture
pattern. Quality and architectural clarity over feature throughput — every commit
should teach.
## Where to go
- **[Product Requirements](PRD.md)** — what we're building and why.
- **[ADR-0001: Loose coupling](architecture/adr-0001-loose-coupling.md)** — the
non-negotiable integration stance; the template for future ADRs.
- **[Working in Gitea](gitea-workflow.md)** — issues, milestones, branches, PRs.
- **[CI runbook](runbooks/ci.md)** — the pipeline and the `make ci` local gate.
## Quickstart
See the repository `README.md`. In short: clone, then either run the checks with
`make ci`, or bring the BFF up with
`docker compose -f infra/docker-compose.yml up -d --build --wait` and
`curl http://localhost:8080/health`.
> This site is built with MkDocs Material (`mkdocs build`). It grows with the
> backlog; sections appear as their slices land.

View File

@@ -1,104 +0,0 @@
# CI runbook — Gitea Actions
> **Status: no runner yet → run CI locally with `make ci`.** The workflow
> `.gitea/workflows/ci.yaml` is in place, but the pipeline cannot go green until a
> self-hosted `respellion-linux` runner is registered against the Gitea instance.
> Until then, **`make ci` is the gate** — it runs the exact same checks locally
> (the workflow calls the same `make` targets). Issue **#30 (S-00-c)** stays open
> until CI is verified green on a runner.
## The pipeline
`.gitea/workflows/ci.yaml` runs on every push and pull request to `main`. Each job
calls a `make` target — the **single source of truth** for the checks, so local
and CI cannot drift:
| Job | Target | Needs |
|---|---|---|
| `lint` | `make lint``dotnet format … --verify-no-changes` | .NET 10 SDK |
| `build` | `make build``dotnet build … -c Release` | .NET 10 SDK |
| `unit` | `make unit``dotnet test … -c Release` | .NET 10 SDK |
| `compose-smoke` | `make smoke` → compose up `--wait``curl /health``down` | container engine + compose v2 |
All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`,
`https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea
Actions resolves them from GitHub.
## Running CI locally (`make ci`)
Until the runner exists, run the full pipeline yourself before pushing:
```bash
make ci # lint + build + unit + smoke — what the pipeline runs
make lint # or a single stage
make smoke # compose up --wait, curl /health, tear down
```
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
the Podman API socket and a Compose provider:
```bash
systemctl --user enable --now podman.socket # start the API socket
ln -sf "$(command -v podman)" ~/.local/bin/docker # docker -> podman shim
# install Docker Compose v2 into ~/.local/bin as `docker-compose` (the provider)
```
The Makefile auto-points `DOCKER_HOST` at `/run/user/$(id -u)/podman/podman.sock`
when that socket exists and `DOCKER_HOST` is unset, so `make smoke` "just works"
locally while leaving real Docker hosts / CI runners untouched.
## Runner: `respellion-linux`
The single self-hosted runner label this repo targets is **`respellion-linux`**
(declared here per §15). It is intended to run **co-located on the Gitea server**
(`git.labs.respellion.tech` / `46.224.220.37`) so CI is durable and independent of
any developer machine.
### Host prerequisites
The runner executes jobs in **host mode** (see registration below), so the host
must have, on `PATH`:
- .NET 10 SDK (or let `setup-dotnet` install it into the runner tool cache)
- A container engine with Compose v2 — Docker, or Podman with the Docker-compatible
socket and the `docker-compose` provider (as configured on the dev box)
- `curl`
### Install & register `act_runner` (on the Gitea server)
```bash
# 1. Install the binary (pick the version matching the Gitea release line)
VER=0.2.11
curl -fsSL -o /usr/local/bin/act_runner \
"https://dl.gitea.com/act_runner/${VER}/act_runner-${VER}-linux-amd64"
chmod +x /usr/local/bin/act_runner
# 2. Obtain a registration token from the Gitea UI:
# Site Administration → Actions → Runners → "Create new Runner" (instance-level)
# (or Repo → Settings → Actions → Runners for a repo-scoped runner)
# 3. Register with the respellion-linux label in HOST execution mode.
# The ":host" suffix means jobs run directly on the host shell, so
# `docker compose` in compose-smoke uses the host engine (no docker-in-docker).
act_runner register --no-interactive \
--instance https://git.labs.respellion.tech \
--token <REGISTRATION_TOKEN> \
--name respellion-ci-1 \
--labels "respellion-linux:host"
# 4. Run it (foreground to verify, then install as a systemd service)
act_runner daemon
```
Verify in the Gitea UI (Actions → Runners) that `respellion-ci-1` shows **Idle**,
then re-run the `CI` workflow; all four jobs should pass.
## Security note
A self-hosted runner in **host mode** executes workflow code directly on the Gitea
server host. Anyone who can push a workflow can run code there. This is acceptable
for a **private lab** instance with trusted contributors. For anything
internet-facing, switch to container/VM isolation (`--labels "respellion-linux:docker://..."`)
or a dedicated runner host, and gate workflow runs on approval for outside PRs.

View File

@@ -1,44 +0,0 @@
# Open Notificaties (NRC) runbook
The Open Notificaties stack (`infra/opennotificaties/docker-compose.yml`) is a lean
adaptation of the upstream dev compose: PostGIS db, redis (also the Celery broker), a
one-shot migrate-init, the API, and a celery worker. It shares the **`cg`** Docker
network with the OpenZaak stack so the two can reach each other by service name.
NRC is published on host **:8001** (OpenZaak holds :8000).
## Quick test (`make`) — both platforms together
```bash
make stack-up # OpenZaak + Open Notificaties on the shared network
make stack-smoke # start both + assert reachable (OZ 403/302/200, NRC 302)
make stack-down # stop + wipe both
```
`make stack-smoke` runs `docker compose -f infra/openzaak/... -f infra/opennotificaties/... up -d`
and asserts:
| Check | Expected |
|---|---|
| OpenZaak `GET /zaken/api/v1/zaken` (no JWT) | 403 (auth enforced) |
| OpenZaak `GET /admin/` | 302 |
| Open Notificaties `GET /admin/` | 302 |
NRC admin UI: <http://localhost:8001/admin/> (dev superuser **admin / admin**).
## Notification wiring is deferred to S-06
Both platforms are **up and reachable**, but OpenZaak→NRC notification *delivery* is not
wired yet, and OpenZaak still runs with `NOTIFICATIONS_DISABLED=true`. The bidirectional
auth wiring (NRC `setup_configuration`: Services + Authorization to OpenZaak's
Autorisaties API + JWT secrets + Kanalen + Abonnementen; OpenZaak's NotificationConfig)
lands with **S-06 (Event Subscriber)** — the slice that actually consumes events. NRC's
`setup_configuration/data.yaml` is intentionally minimal (migrations only) until then.
This matches S-01's acceptance, which asks only that the platforms *come up in compose*
and a health check confirms them reachable.
## Prerequisites
Same rootless-Podman setup as the rest of the repo — see [ci.md](ci.md) and
[openzaak.md](openzaak.md). `systemctl --user start podman.socket` once per session.

View File

@@ -1,75 +0,0 @@
# OpenZaak runbook
The OpenZaak stack (`infra/openzaak/docker-compose.yml`) is a lean adaptation of the
upstream open-zaak dev compose: PostGIS db, redis, a one-shot init that runs
migrations, the OpenZaak API, and a celery worker.
## Quick test (`make`)
```bash
make openzaak-up # start the stack (first run pulls images + migrates: 1-3 min)
make openzaak-smoke # start + assert it's up with auth enforced (403/302/200)
make openzaak-seed # start + seed the BIG catalogus (idempotent)
make openzaak-down # stop and wipe data
```
## Seed the BIG catalogus
`make openzaak-seed` brings the stack up and runs `infra/openzaak/seed_catalogus.py`,
which creates (idempotently, via the ZTC API):
- catalogus **BIG**
- a lean **BIG-REGISTRATIE** zaaktype (concept; only schema-mandatory fields)
- a **bsn** eigenschap on it
then confirms the JWT client can list it. See **ADR-0002** for the design (why the
zaaktype stays a concept, why notifications are disabled, why the API not a fixture).
**JWT client** (provisioned declaratively by `setup_configuration/data.yaml`, **dev only**):
| | |
|---|---|
| client_id | `big-reference-seed` |
| secret | `insecure-dev-secret-change-me` |
| authorizations | `heeft_alle_autorisaties` (all) |
The seed mints a ZGW JWT (HS256) from these and calls `/catalogi/api/v1/...`.
`make openzaak-smoke` polls until the API responds, then asserts:
| Check | Expected |
|---|---|
| `GET /zaken/api/v1/zaken` (no JWT) | **403**`PermissionDenied` ZGW fout (auth enforced) |
| `GET /admin/` | **302** — admin login redirect |
| `GET /zaken/api/v1/` | **200** — ZGW API schema root |
> **403, not 401.** OpenZaak's ZGW APIs return `403 PermissionDenied` for a missing
> or invalid JWT. The S-01 acceptance text says "401" — that's inaccurate; 403 is the
> correct auth-enforced response.
The admin UI is at <http://localhost:8000/admin/>; the dev superuser is **admin /
admin** (from the compose env — dev only).
## Prerequisites (rootless Podman)
Same setup as the rest of the repo (see [ci.md](ci.md)):
```bash
systemctl --user start podman.socket # the Docker-API socket the shim talks to
```
The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so the
`make openzaak-*` targets work without extra env.
## Notes
- **Not in `make ci`.** The OpenZaak smoke is a separate, heavier check (large image
pull + migrations); it is intentionally kept out of `make ci` so the core gate
stays fast. Run `make openzaak-smoke` when you touch the OpenZaak stack.
- **Notifications disabled.** `NOTIFICATIONS_DISABLED=true` — otherwise ZTC writes 500
trying to notify. Open Notificaties is now up (see [opennotificaties.md](opennotificaties.md)),
but OZ→NRC delivery wiring + re-enabling lands with **S-06**.
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
resultaattypen — beyond the lean seed). List with `?status=alles`.
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
a known-good tag (ADR-0002 follow-up).

View File

@@ -1,19 +0,0 @@
# Local development stack. Grows service-by-service with each slice.
# S-00-b: the placeholder BFF with a /health check.
#
# docker compose -f infra/docker-compose.yml up -d --build --wait
# curl http://localhost:8080/health # -> Healthy
services:
bff:
build:
context: ../services/bff
dockerfile: Dockerfile
image: register-referentie/bff:dev
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s

View File

@@ -1,91 +0,0 @@
# Open Notificaties (NRC) stack (S-01-c). Lean adaptation of the upstream dev compose:
# db (PostGIS) + redis (also the Celery broker) + migrate-init + the API + a celery worker.
#
# Shares the `cg` network with the OpenZaak stack so the two can reach each other.
# Run BOTH together (NRC needs OpenZaak's Autorisaties API for auth wiring):
#
# docker compose -f infra/openzaak/docker-compose.yml -f infra/opennotificaties/docker-compose.yml up -d
# curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8001/admin/ # -> 302
#
# NRC is published on host :8001 (OpenZaak holds :8000).
services:
nrc-db:
image: docker.io/postgis/postgis:17-3.5
environment:
POSTGRES_USER: opennotificaties
POSTGRES_PASSWORD: opennotificaties
POSTGRES_DB: opennotificaties
command: postgres -c max_connections=300
volumes:
- nrc-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
interval: 5s
timeout: 3s
retries: 10
networks: [cg]
nrc-redis:
image: docker.io/library/redis:7
networks: [cg]
nrc-init:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: &nrc-env
DJANGO_SETTINGS_MODULE: nrc.conf.docker
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
DB_HOST: nrc-db
DB_NAME: opennotificaties
DB_USER: opennotificaties
DB_PASSWORD: opennotificaties
IS_HTTPS: "no"
ALLOWED_HOSTS: "*"
CACHE_DEFAULT: nrc-redis:6379/0
CACHE_AXES: nrc-redis:6379/0
CELERY_BROKER_URL: redis://nrc-redis:6379/1
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
DISABLE_2FA: "true"
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
RUN_SETUP_CONFIG: "true"
command: /setup_configuration.sh
volumes:
- ./setup_configuration:/app/setup_configuration:ro,z
depends_on:
nrc-db:
condition: service_healthy
nrc-redis:
condition: service_started
networks: [cg]
nrc-web:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: *nrc-env
healthcheck:
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
ports:
- "8001:8000"
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
nrc-celery:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: *nrc-env
command: /celery_worker.sh
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
volumes:
nrc-db:
networks:
cg:

View File

@@ -1,5 +0,0 @@
# Open Notificaties setup_configuration.
# Stage 1 (this commit): intentionally minimal — the init container runs
# migrations; no steps enabled yet. The OpenZaak<->NRC notification wiring
# (Services, Authorization, JWT, Kanalen) is added next. See ADR-0002 / S-01-c.
{}

View File

@@ -1,95 +0,0 @@
# OpenZaak stack (S-01). Lean adaptation of the upstream open-zaak dev compose:
# db (PostGIS) + redis + a one-shot init (migrations) + the API + a celery worker.
# Dropped from upstream for leanness: nginx, celery-beat, flower, OTEL.
#
# docker compose -f infra/openzaak/docker-compose.yml up -d --wait
# curl -i http://localhost:8000/zaken/api/v1/zaken # -> 401 (auth required)
#
# NOTE: image pinned to a tag (not :latest) once a known-good tag is chosen; see
# the catalogus-design ADR. Using a tag var with a sensible default for now.
services:
oz-db:
image: docker.io/postgis/postgis:17-3.5
environment:
POSTGRES_USER: openzaak
POSTGRES_PASSWORD: openzaak
POSTGRES_DB: openzaak
command: postgres -c max_connections=300
volumes:
- oz-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak"]
interval: 5s
timeout: 3s
retries: 10
networks: [cg]
oz-redis:
image: docker.io/library/redis:7
networks: [cg]
oz-init:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
environment: &oz-env
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
DB_HOST: oz-db
DB_NAME: openzaak
DB_USER: openzaak
DB_PASSWORD: openzaak
IS_HTTPS: "no"
ALLOWED_HOSTS: "*"
CACHE_DEFAULT: oz-redis:6379/0
CACHE_AXES: oz-redis:6379/0
CELERY_BROKER_URL: redis://oz-redis:6379/1
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
DISABLE_2FA: "true"
# Notifications go to Open Notificaties (NRC), which arrives in S-01-c.
# Until then, disable outbound notifications so writes don't 500.
NOTIFICATIONS_DISABLED: "true"
OPENZAAK_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
RUN_SETUP_CONFIG: "true"
command: /setup_configuration.sh
volumes:
# :z relabels for SELinux; the dir/file must be world-readable for the
# container user (rootless Podman uid mapping). See docs/runbooks/openzaak.md.
- ./setup_configuration:/app/setup_configuration:ro,z
depends_on:
oz-db:
condition: service_healthy
oz-redis:
condition: service_started
networks: [cg]
openzaak:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
environment: *oz-env
healthcheck:
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
ports:
- "8000:8000"
depends_on:
oz-init:
condition: service_completed_successfully
networks: [cg]
oz-celery:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
environment: *oz-env
command: /celery_worker.sh
depends_on:
oz-init:
condition: service_completed_successfully
networks: [cg]
volumes:
oz-db:
networks:
cg:

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env python3
"""Idempotent seed of the BIG catalogus into OpenZaak via the ZTC API.
Creates (if absent):
- catalogus "BIG"
- a lean "BIG-registratie" zaaktype (only schema-mandatory fields)
- a "bsn" eigenschap on that zaaktype
- then publishes the zaaktype.
Auth uses the JWT client provisioned by setup_configuration (see ADR-0002).
Stdlib only — no pip deps. Re-running is safe (matches existing by identifier).
"""
import base64, hashlib, hmac, json, os, sys, time, urllib.error, urllib.request
BASE = os.environ.get("OZ_BASE", "http://localhost:8000")
CLIENT_ID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
ZTC = f"{BASE}/catalogi/api/v1"
RSIN = "517439943" # elfproef-valid test RSIN
def token():
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
hdr = {"alg": "HS256", "typ": "JWT"}
pl = {"iss": CLIENT_ID, "iat": int(time.time()), "client_id": CLIENT_ID,
"user_id": "seed", "user_representation": "seed"}
seg = b64(json.dumps(hdr, separators=(",", ":")).encode()) + b"." + \
b64(json.dumps(pl, separators=(",", ":")).encode())
sig = b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())
return (seg + b"." + sig).decode()
def api(method, path, body=None):
url = path if path.startswith("http") else f"{ZTC}{path}"
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method, headers={
"Authorization": "Bearer " + token(),
"Content-Type": "application/json",
"Accept": "application/json",
})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read() or "null")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or "null")
def find(path):
status, body = api("GET", path)
if status != 200:
sys.exit(f"GET {path} -> {status}: {body}")
return body.get("results", [])
def main():
# 1. Catalogus
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
if existing:
cat = existing[0]
print(f"skip catalogus BIG ({cat['url']})")
else:
st, cat = api("POST", "/catalogussen", {
"domein": "BIG", "rsin": RSIN,
"contactpersoonBeheerNaam": "BIG Beheer",
})
if st != 201:
sys.exit(f"create catalogus -> {st}: {cat}")
print(f"create catalogus BIG ({cat['url']})")
# 2. Zaaktype (concept)
# status=alles so concept zaaktypen are matched too (else we'd duplicate).
zts = [z for z in find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
if z.get("identificatie") == "BIG-REGISTRATIE"]
if zts:
zt = zts[0]
print(f"skip zaaktype BIG-REGISTRATIE ({zt['url']}) concept={zt.get('concept')}")
else:
st, zt = api("POST", "/zaaktypen", {
"identificatie": "BIG-REGISTRATIE",
"omschrijving": "BIG-registratie",
"vertrouwelijkheidaanduiding": "openbaar",
"doel": "Registratie van een zorgprofessional in het BIG-register",
"aanleiding": "Aanvraag tot registratie",
"indicatieInternOfExtern": "extern",
"handelingInitiator": "indienen",
"onderwerp": "BIG-registratie",
"handelingBehandelaar": "behandelen",
"doorlooptijd": "P30D",
"opschortingEnAanhoudingMogelijk": False,
"verlengingMogelijk": False,
"publicatieIndicatie": False,
"productenOfDiensten": [],
"referentieproces": {"naam": "BIG-registratie"},
"catalogus": cat["url"],
"besluittypen": [],
"gerelateerdeZaaktypen": [],
"beginGeldigheid": "2026-01-01",
"versiedatum": "2026-01-01",
"verantwoordelijke": RSIN,
})
if st != 201:
sys.exit(f"create zaaktype -> {st}: {json.dumps(zt, indent=2)}")
print(f"create zaaktype BIG-REGISTRATIE ({zt['url']})")
# 3. bsn eigenschap (only addable while concept)
eigs = [e for e in find(f"/eigenschappen?zaaktype={zt['url']}&status=alles")
if e.get("naam") == "bsn"]
if eigs:
print("skip eigenschap bsn")
elif zt.get("concept", True):
st, eig = api("POST", "/eigenschappen", {
"naam": "bsn",
"definitie": "Burgerservicenummer van de zorgprofessional",
"zaaktype": zt["url"],
"specificatie": {"groep": "aanvrager", "formaat": "tekst",
"lengte": "9", "kardinaliteit": "1", "waardenverzameling": []},
})
if st != 201:
sys.exit(f"create eigenschap -> {st}: {json.dumps(eig, indent=2)}")
print("create eigenschap bsn")
else:
print("warn zaaktype already published; cannot add bsn eigenschap")
# Intentionally NOT published. Publishing requires roltypen, resultaattypen
# and statustypen, which go beyond the "lean / schema-mandatory" zaaktype this
# slice asks for; they arrive with the workflow/zaak slices. See ADR-0002.
# 4. Verify the JWT client can list the zaaktype (concepts included).
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
names = [z.get("identificatie") for z in zaaktypen]
print(f"zaaktypen in BIG: {names}")
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
print("OK — BIG catalogus seeded (BIG-REGISTRATIE concept + bsn eigenschap)")
if __name__ == "__main__":
main()

View File

@@ -1,22 +0,0 @@
# OpenZaak setup_configuration (idempotent, declarative).
# Provisions the JWT client the seed + ACL use to call OpenZaak's APIs.
# Dev-only credentials — not for production.
#
# Steps come from vng_api_common.contrib.setup_configuration (see ADR-0002).
vng_api_common_credentials_config_enable: true
vng_api_common_credentials:
items:
- identifier: big-reference-seed
secret: insecure-dev-secret-change-me
vng_api_common_applicaties_config_enable: true
vng_api_common_applicaties:
items:
# uuid must be given explicitly as a string (the step's auto-default is a
# UUID object that fails its own validation).
- uuid: "11111111-1111-4111-8111-111111111111"
client_ids:
- big-reference-seed
label: BIG reference seed client
heeft_alle_autorisaties: true

View File

@@ -1,41 +0,0 @@
site_name: register-referentie
site_description: Reference application for Respellion's Common Ground architecture pattern
docs_dir: docs
theme:
name: material
features:
- navigation.sections
- navigation.top
- content.code.copy
palette:
- scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
toggle:
icon: material/brightness-4
name: Switch to light mode
nav:
- Home: index.md
- Product Requirements: PRD.md
- Architecture:
- "ADR-0001: Loose coupling": architecture/adr-0001-loose-coupling.md
- Working in Gitea: gitea-workflow.md
- Runbooks:
- CI: runbooks/ci.md
markdown_extensions:
- admonition
- toc:
permalink: true
- pymdownx.superfences
# Many docs referenced by PRD.md land in later slices; don't fail the build on them.
validation:
nav:
omitted_files: warn
links:
not_found: warn

View File

@@ -1,3 +0,0 @@
**/bin
**/obj
**/*.user

View File

@@ -1,30 +0,0 @@
# Multi-stage build for the placeholder BFF (.NET 10).
# Build context is services/bff (see infra/docker-compose.yml).
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Restore first (cached unless the csproj changes).
COPY Bff.Api/Bff.Api.csproj Bff.Api/
RUN dotnet restore Bff.Api/Bff.Api.csproj
# Then build + publish.
COPY Bff.Api/ Bff.Api/
RUN dotnet publish Bff.Api/Bff.Api.csproj -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
# curl is used by the container HEALTHCHECK / compose healthcheck.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=3s --start-period=10s --retries=5 \
CMD curl -fsS http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "Bff.Api.dll"]