How components are built and deployed to ZAD (RIG/Quattro/rijksapps) through GitHub Actions.
All components are deployed to ZAD (RIG/Quattro/rijksapps) via GitHub Actions. Docker images are pushed to GitHub Container Registry (GHCR).
A PR only builds and deploys when it carries the deploy:preview label. Without that label nothing is built, because a preview costs about half of the repository’s runner budget and no required check depends on it. Add the label and the build starts, whether the PR was opened a minute or a month ago:
ghcr.io/minbzk/regelrecht-{component}, tagged pr-{N} and sha-{commit}pr{N} is created on ZADEvery later commit on a labelled PR repeats this. Remove the label and the preview and its images are cleaned up.
Only changed components are rebuilt. Any component can also be forced to build by adding its deploy:<component> label to the PR (for example deploy:editor); those labels are additive and do nothing on their own, since deploy:preview is what opens the gate.
The gate is the presence of the label on the PR, checked on every event, not the kind of event. Before this was opt-in, building seven images and a preview environment for every PR accounted for about half of the repository’s runner time, while nothing tests against the preview (E2E (mocked) runs against mocks). The runner budget goes to the merge queue instead, and a preview is something someone chooses.
Fork PRs never build, labels or not. They have no secrets and a read-only GITHUB_TOKEN, so the push to GHCR could not succeed.
When a PR merges to main, production deployment runs:
regelrecht deployment on ZADdeploy-preview and deploy-production each deploy all components in a single ZAD task. ZAD lets a task give way to a newer task that covers the same deployment, so two tasks for one deployment side by side would push each other aside.
The cleanup-preview job in deploy.yml deletes the pr{N} deployment on ZAD with its GitHub environment and deployments, and asks the ZAD cleanup action to remove the PR’s pr-{N} image tags. No preview keeps running that nobody looks at. Fork PRs are skipped, since they never built anything. All other image cleanup happens nightly, as described below.
scheduled-cleanup.yml runs every night. It removes stale pr{N} ZAD deployments and GitHub environments, and then runs all image cleanup through one script, script/ghcr-cleanup.mjs, in one step. That script removes three kinds of image versions:
pr- tagged versions of closed pull requests, including any the per-PR cleanup missedsha- form, that run nowhere and are older than a weekThe three belong together because they share the same protection, and two cleaners side by side would not know each other’s exceptions:
sha- tag, not on latest. A cleaner that goes by tag shape alone would remove the image under the running deployment. The script therefore checks against what ZAD is running at that moment, and removes nothing at all, of any kind, if it cannot get that list.The ZAD cleanup action inventories GitHub environments, and so misses any ZAD deployment whose environment is already gone. script/prune-orphaned-deployments.sh works the other way round. Afterwards script/check-preview-deployments.sh and script/check-preview-environments.sh establish what is actually left, because the cleanup’s own report says nothing about the outcome.
The reasoning behind each rule is in the header of the script concerned.
Tell a timeout apart from an error first. A message that the wait ran out (“Timed out after 900s waiting for the task; it may still be running”) says only that ZAD took longer than the window. The deployment carries on, and the preview usually comes up a little later. Before the window went from 300 to 900 seconds, failed and successful runs overlapped completely in duration (#1144), so a timeout on its own tells you nothing about the application.
An error with a status or an exception in it is the diagnostic case. Then:
zad logs <deployment> (for example zad logs pr429)ERROR lines. Common causes are migration conflicts, missing environment variables and panics at startupzad deployment delete <deployment>) and re-trigger CI to get a fresh databaseOne failure is harmless: Could not extract URL from result with "status": "superseded" in the JSON below it. ZAD let the task give way to a newer task covering the same deployment (a new push, or the cleanup of a closed PR). The newer task did the work, and re-running only this job is enough.
| Component | Image | Production URL |
|---|---|---|
| Editor | regelrecht-editor | editor.regelrecht.rijks.app |
| Admin | regelrecht-admin | harvester-admin.regelrecht.rijks.app |
| Harvester Worker | regelrecht-harvester-worker | (no web UI) |
| Enrich Worker | regelrecht-enrich-worker | (no web UI) |
| Pipeline API | regelrecht-pipeline-api | (no public URL; reached in-cluster) |
| Lawmaking | regelrecht-lawmaking | lawmaking.regelrecht.rijks.app |
| Demo | regelrecht-demo | demo.regelrecht.rijks.app |
| Docs | regelrecht-docs | docs.regelrecht.rijks.app + regelrecht.rijks.app (landing) |
| PoC portal | regelrecht-poc | poc.regelrecht.rijks.app |
| PoC napp | regelrecht-poc-napp | (internal; reached through the portal at /napp/) |
| Grafana | regelrecht-grafana | grafana.regelrecht.rijks.app |
The docs image also serves /roadmap, a read-only rendering of the werkpakketten in docs/src/content/roadmap/ and the JSON file in docs/src/data/. It is not a component of its own and has no write path: changing the roadmap means editing those files through a pull request, and every werkpakket page links to its own source on GitHub. The landing page links to it from the footer, next to the documentation and research links; it stays out of the main navigation, which covers the landing page’s own sections.
The demo (frontend-demo/) runs at demo.regelrecht.rijks.app as the ZAD component demo in the regelrecht deployment, with only publish-on-web on port 8000. It rolls out with deploy-preview and deploy-production when script/deploy-filters.mjs marks the demo component as changed. That happens for changes to the engine crate and the workspace crates it depends on, the workspace-wide Rust files (packages/Cargo.toml, packages/Cargo.lock, rust-toolchain.toml, schema/), frontend-demo/, packages/frontend-shared/, corpus/demo/, or deploy/nginx/.
The build is the build-demo job in deploy.yml (image-name: minbzk/regelrecht-demo, dockerfile: frontend-demo/Dockerfile, cache-scope: demo). The image is covered by scheduled-cleanup.yml, which checks sha- tags against the running deployment before it deletes anything.
What to check after a change is easiest on a preview (label the PR deploy:preview): that the WASM engine loads (network tab: wasm/pkg/*.wasm served as application/wasm), the slides, the portals of Merijn and Claudia, and one application followed all the way into the case system. The two things in frontend-demo/nginx.conf that can break are the SPA fallback to index.html and the MIME type for .wasm.
The demo needs no backend and no secrets. Its build takes longer than the other frontends because of the Rust-to-WASM step. The wasm-builder stage pins the Rust image to the version in rust-toolchain.toml (a pre-commit test, script/dockerfile-consistency.test.mjs, fails when the two drift) and pins wasm-bindgen-cli to the version in packages/Cargo.lock; the build itself fails loudly when that second pair diverges.
Use zad-cli to manage deployments directly:
Configure ZAD_API_KEY and ZAD_PROJECT_ID in .env.
RIG_API_KEY - API key for ZAD Operations Manager (configured in GitHub repository secrets)GITHUB_TOKEN - used for GHCR image pushes (provided automatically by GitHub Actions)An exploration into transparent, executable legislation, and one of the three projects in the starting selection of the Nederlandse Digitale Dienst.
GitHub repository
How it works
Stay informed
Roadmap (Dutch)
Documentation
Research
Nederlandse Digitale Dienst
Ministry of Economic Affairs and Climate Policy