Files
atomic-design-poc/.github/workflows/ci.yml
T
ehoandClaude Sonnet 5 e7156c5132 feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 21:01:57 +02:00

307 lines
16 KiB
YAML

name: CI
on:
push:
branches: [main]
tags: ['v*']
pull_request:
# Least privilege by default.
permissions:
contents: read
# A newer push to the same ref cancels the in-flight run.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# WP-30: computes which side changed so the jobs below can skip their real work on a
# narrow PR. Deliberately conservative ("skip steps, not jobs" — see each job's `if:`):
# every job below still runs and reports a status even when its side is untouched, just
# with checkout as its only step. This avoids a required-status-check ever waiting on a
# job that never started, at the cost of still paying job-startup overhead on a skip.
# `.github/workflows/**` counts as BOTH sides, so a CI change itself always gets a full run.
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
frontend:
- 'apps/**'
- 'libs/**'
- 'public/**'
- 'e2e/**'
- 'scripts/**'
- 'angular.json'
- 'package*.json'
- 'tsconfig*.json'
- '.storybook*/**'
- 'eslint.config.mjs'
- '.dependency-cruiser*.js'
- '.github/workflows/**'
backend:
- 'backend/**'
- '.github/workflows/**'
# WP-30: split out of `frontend` so lint/format/token failures report in ~1 min instead of
# waiting on the full test:coverage/ng build below — depends on the node_modules cache
# above landing first, else this duplicates a full npm ci for no reason.
lint:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
- run: npm run lint
if: needs.changes.outputs.frontend == 'true'
- run: npm run format:check
if: needs.changes.outputs.frontend == 'true'
- run: npm run check:tokens
if: needs.changes.outputs.frontend == 'true'
frontend:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
# WP-30: on a hit, npm ci is skipped entirely (not just faster) — caching node_modules
# alone doesn't help since npm ci always deletes-then-reinstalls unconditionally.
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
# Bounded-context + atomic-layer boundaries (WP-38, dependency-cruiser).
- run: npm run dep:check
if: needs.changes.outputs.frontend == 'true'
# Showcase snippets must match their real source regions (WP-39, no drift).
- run: npm run gen:snippets && git diff --exit-code apps/ssp/src/app/showcase/snippets.generated.ts
if: needs.changes.outputs.frontend == 'true'
# Runs the full suite (both apps + both shared libraries, WP-67) AND reports coverage
# (WP-46, report-only — no thresholds, so it can't fail on coverage; it still fails on
# a failing test, like `npm test` did).
- run: npm run test:coverage
if: needs.changes.outputs.frontend == 'true'
# --localize builds every configured locale (nl + en, angular.json's i18n block) in one
# pass per app; i18nMissingTranslation:"error" (angular.json) fails this step if either
# app's messages.en.xlf is missing a unit its source (WP-20) or libs/shared gains.
- run: npx ng build ssp --localize && npx ng build behandelportal --localize
if: needs.changes.outputs.frontend == 'true'
# The shipped bundle must stay clean; dev-only advisories are excluded.
- run: npm audit --omit=dev
if: needs.changes.outputs.frontend == 'true'
storybook-a11y:
needs: changes
# Axe runs against every story in the static build; a violation fails the build.
runs-on: ubuntu-latest
# Hard resource ceiling so a runaway test-storybook (one headless Chromium per Jest
# worker) can't OOM the runner host — it fails its own container instead. The real cap
# is `--maxWorkers=2` in test-storybook:ci; this is the belt-and-suspenders guardrail.
# 4g is enough: verified by running this job's exact steps (npm ci, playwright install,
# build-storybook, test-storybook:ci) in `docker run --cpus=2 --memory=4g
# --memory-swap=4g node:24-bookworm` locally — completes clean, no OOM. (An earlier
# unconstrained local RSS measurement of build-storybook alone suggested ~5.8GB was
# needed, but that number reflects what Node/V8 is willing to use when memory is
# plentiful, not what the job actually needs under a real cgroup cap.)
# NB: requires the Gitea act_runner to allow container jobs (docker mode). If the runner
# is host-only, drop this `container:` block and rely on the worker cap alone.
container:
image: node:24-bookworm
options: --cpus=2 --memory=4g --memory-swap=4g
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
# Cache the chromium download across runs; `install --with-deps` then only
# runs the (fast, idempotent) apt deps check on a hit.
- if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
if: needs.changes.outputs.frontend == 'true'
- run: npm run build-storybook
if: needs.changes.outputs.frontend == 'true'
- run: npm run test-storybook:ci
if: needs.changes.outputs.frontend == 'true'
backend:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 10.0.x
- if: needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
if: needs.changes.outputs.backend == 'true'
# Category=Integration (WP-54, OpenZaakIntegrationTests) needs a live OpenZaak — opt-in,
# run manually against backend/openzaak/ (see its README), never in CI.
- run: dotnet test backend/BigRegister.slnx --filter "Category!=Integration"
if: needs.changes.outputs.backend == 'true'
e2e:
needs: changes
# Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh
# runner checkout per run, so there's no bigregister.db (WP-22, gitignored)
# left over from a prior run to leak state in; the backend creates + migrates
# an empty one on this boot, same as a fresh clone always has.
# Playwright's `webServer` (playwright.config.ts) starts BOTH the backend and
# `ng serve`, waits for them, runs the suite, and tears them down — all in the
# one `npm run e2e` process. Do NOT background them as separate steps: a `&`
# process from one Actions step is dead by the next step, so `wait-on` hung
# forever (the 2-hour e2e hang).
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 10.0.x
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
- run: npm run e2e
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
semgrep:
needs: changes
# SAST for both sides — replaces CodeQL, which is GitHub-only (its analyze step uploads
# SARIF to GitHub's code-scanning API) and can't run on this Gitea instance. Semgrep OSS
# is a plain CLI: no account, no external platform API. Findings print in the job log.
# Installed via the runner's preinstalled python3/pip — NOT `setup-python` (its Python
# download failed on this runner) and NOT a job `container:` (this act_runner times out
# pulling the base runner image for container jobs). `--break-system-packages` survives
# PEP-668; pip drops `semgrep` on PATH. `--ignore-installed` is required because some of
# semgrep's deps (e.g. PyJWT) are already present as apt-managed packages, which pip cannot
# uninstall ("RECORD file not found") — this flag installs fresh without uninstalling, so it
# never touches the Debian copies. Don't drop it.
# WP-30: initial findings triaged (dependabot cooldown, npm min-release-age, GH Actions
# pinned to SHA, 2 nosemgrep'd ReDoS false positives on non-attacker-controlled input) —
# `--error` below makes this a real blocking gate, not report-only.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- run: python3 -m pip install --break-system-packages --ignore-installed semgrep
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
# p/default = curated cross-language security (covers JS/TS); p/csharp = the backend.
# Anonymous registry fetch; --metrics=off disables telemetry (not `auto`, which uploads
# project metadata).
- run: semgrep scan --config p/default --config p/csharp --metrics=off --error
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
api-client-drift:
needs: changes
# The committed typed client must match the backend OpenAPI doc.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
# 8.0 for the bundled NSwag runtime, 10.0 to build/emit the spec.
dotnet-version: |
8.0.x
10.0.x
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
- run: npm run gen:api
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
- run: git diff --exit-code libs/shared/src/infrastructure/api-client.ts backend/swagger.json
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'