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'