Deployment¶
Environments¶
| Environment | URL | Branch |
|---|---|---|
| Production | https://cpsv-editor.open-regels.nl | main |
| Acceptance | https://acc.cpsv-editor.open-regels.nl | acc |
All changes go to acc first. After acceptance testing, they are merged to main for production deployment.
CI/CD pipeline¶
Seven workflows live in .github/workflows/. Two deploy the application; the
rest audit it, scan it, record what was released, or clean up after a pull
request.
| Workflow name | File | Job (check) | Runs on | Purpose |
|---|---|---|---|---|
| Deploy ACC (orange-beach) | azure-static-web-apps-orange-beach-0574c2a03.yml |
changes, Build and deploy ACC |
pushes to acc; pull requests into acc |
Lint, test, build and deploy acceptance |
| Deploy PROD (white-sky) | azure-static-web-apps-white-sky-02b674303.yml |
Build and deploy PROD |
pushes to main; pull requests into main |
Lint, test, build and deploy production |
| Supply-chain audit | zizmor.yml |
audit |
every pull request; pushes to acc/main |
Workflow analysis, Renovate config, lockfile sync, formatting, pin truth |
| Semgrep | semgrep.yml |
scan |
every pull request; pushes to acc/main |
Semgrep Code and Supply Chain over the source and the npm tree |
| Close preview environments | close-preview-environments.yml |
Close ACC staging environment, Close PROD staging environment |
every pull request closed into acc or main |
Deletes the pull request's Static Web Apps preview |
| Dependency audit | dependency-audit.yml |
dependency-audit |
daily at 05:17 UTC; on demand; pull requests touching the audit itself | npm audit of both acc and main |
| Release SBOM | sbom.yml |
release-sbom |
pushes to main; on demand; pull requests touching the SBOM tooling |
CycloneDX SBOM of the released version |
Every job runs on ubuntu-24.04 rather than ubuntu-latest, so a change of
Ubuntu release arrives as a diff in this repository instead of silently under
every job at once. That label pins the release, not the image โ GitHub
rebuilds ubuntu-24.04 about weekly and a hosted runner cannot be pinned to a
digest โ which the repository's SECURITY-PIPELINE.md records as an accepted
risk.
The deploy jobs are named for the environment they deploy. Required checks
match by job name, so Build and deploy ACC can be required on acc without
ambiguity, and renaming it means updating the ruleset in the same change. For
the same reason the daily audit's job is dependency-audit, not audit: a
second job called audit would make the required context ambiguous โ one
passing and one failing check under one name, which no ruleset can satisfy.
Which changes deploy¶
A push to acc or main that changes only documentation does not deploy.
paths-ignore on the push trigger covers docs/**, .claude/** and
**/*.md. That is a denylist on purpose: an allowlist would mean enumerating
every path that affects the build, and anything forgotten from it would
silently skip a deploy, which is worse than one unnecessary preview.
Pull requests are filtered differently in each deploy workflow:
- ACC has no path filter on its
pull_requesttrigger. The workflow always starts, and achangesjob matches the pull request's files against the same list; a documentation-only pull request skips the build. A workflow its trigger filters out never starts and reports no check at all, so a required check would wait forever โ whereas a job skipped by its ownif:reports success. The fallback is deliberately fail-safe: the build runs wheneverchangesdid not succeed, so a failed lookup means a full build, never a free pass. - PROD keeps
paths-ignoreon both triggers and has nochangesjob. Nothing is required onmain, so a check that never reports blocks nothing.
What a deploy runs¶
checkout (persist-credentials: false)
โ
setup-node (node-version-file: .nvmrc โ 24.20.0) โ npm ci
โ
npm run lint โ npm run test:ci โ a failure here blocks the deploy
โ
Build: npm run build
with VITE_BACKEND_URL, VITE_BUILD_SHA, VITE_BUILD_RUN
fails if dist/index.html is missing
โ
Azure/static-web-apps-deploy
app_location: '/dist', skip_app_build: true (uploads, builds nothing)
โ
https://cpsv-editor.open-regels.nl (main)
https://acc.cpsv-editor.open-regels.nl (acc)
The install that is tested is the install that ships. The bundle is built
on the runner, from the tree npm ci installed and on the Node the tests just
ran on, and the deploy action uploads dist/ as it is. The dist/index.html
check exists because the deploy action, given an empty or wrong directory,
uploads it and reports success. Before v2026.09.7 the action built the bundle
itself, with Oryx inside its own container, on a Node the repository never
chose; production moved from Oryx's Node 22.22.0 to the .nvmrc-pinned 24.20.0
with this release. Renovate's nvm manager maintains .nvmrc.
Every uses: reference is pinned to a commit digest rather than a tag, each
job declares least-privilege permissions:, and the checkout step does not
persist a git credential into the workspace. See
Supply-Chain Pinning for why, and for
what that hardening deliberately does not cover. The deploy action's container
image, staticappsclient:stable, still floats and cannot be pinned; it now
only uploads, but still receives the deploy token and the built artifact.
Dependency audit and release SBOM¶
Two workflows answer questions about dependencies rather than about a commit.
Dependency audit runs daily, because a new advisory lands against code that
has not changed. It audits both acc and main โ Dependabot watches only
the default branch, acc โ reading each branch's lockfile with
npm audit --package-lock-only, so it installs nothing. It fails on a high or
critical advisory in production dependencies; moderate, low and dev-only
advisories are listed but do not fail it. scripts/audit-tree.mjs groups the
report by advisory rather than by package, because one advisory on a widely used
package otherwise reads as dozens of findings. It exits 2 when the audit could
not run at all, and that is handled with the same weight as a finding, never as
a clean tree. Results go to the run summary and to one tracking issue the
workflow opens, updates and closes.
Release SBOM covers the fact that a promotion to main is a release here
โ there are no tags. scripts/write-sbom.mjs writes
docs/sbom/<name>-<version>.cdx.json: CycloneDX, production dependencies only,
from the lockfile alone. It has three modes: write (npm run sbom, a release
step), --check (strict, where the release is cut) and --verify-release
(what a promotion asserts: a missing document fails, drift only warns). The
workflow uploads the document as an artifact, kept 90 days โ the maximum on a
public repository โ so the committed copy is the durable one.
Supply-chain guards around the install¶
- Lockfile sync. The
auditjob runs a step named Lockfile matches package.json โnpm ci --dry-run --ignore-scriptsโ so a lockfile that disagrees withpackage.jsonfails under its own name. Its stated limit: it checks the pull request's merge commit against that base, so a pull request green against a stale base and merged into a moved one is not caught. Merge dependency pull requests one at a time, rebasing each onto the mergedacc. - Cooldown. The root
.npmrcsetsmin-release-age=14, so npm will not resolve a version published less than 14 days ago โ covering lock-file maintenance and workstation installs, which Renovate's own 14-day cooldown cannot reach. npm 11.10 or newer honours it (Node 24.20.0 bundles 11.19);npm ciignores it by design, so CI is neither blocked nor protected by it. - Majors. Renovate never offers an npm package's
X.0.0, so the earliest a major can arrive is its first patch, and majors wait for Dependency Dashboard approval. The Ubuntu 26.04 runner major is deferred by a Renovate rule that records its reason:ubuntu-lateststill resolves to 24.04.
Pull request workflow¶
acc is protected by the acc supply-chain gate ruleset: it requires a pull
request and three passing checks โ audit, scan and Build and deploy ACC โ
with no bypass actors. A direct git push origin acc is rejected outright โ
including for releases, and including for the repository owner. main requires
a pull request but no status checks, as decided in #131.
- Create a feature branch from
acc. - Make changes and test locally (
npm run test:ci,npm run lint,npm run check-format). - Push the branch and open a pull request targeting
acc. - The pull request runs
audit,scanandBuild and deploy ACC; the last produces a Static Web Apps preview deployment, or is skipped โ and passes โ when the pull request changes only documentation. All three must be green to merge. - Merge with a merge commit. It is the only merge method the repository allows: changelog entries cite commits by SHA, and squashing or rebasing would orphan every citation.
- Merging is the push to
acc, which triggers the acceptance deploy. Verify behaviour on the ACC environment. - Open a pull request from
acctomainfor production release.
Azure Static Web Apps¶
The application is deployed as a static site. No server-side rendering is involved. The build output is the dist/ directory produced by npm run build (Vite), built on the runner and uploaded as-is with skip_app_build: true. It was build/ under Create React App until v2026.09.1 โ the deploy workflows and the build changed in the same commit, because that rename cannot be half-applied.
There is no staticwebapp.config.json in the repository, so routing and CORS
fall back to the Static Web Apps defaults.
Environment variables¶
Vite exposes only VITE_-prefixed variables to the bundle. vite build runs in
production mode, so it loads .env.production โ for both environments.
| Variable | Set by | Value |
|---|---|---|
VITE_BACKEND_URL |
The Build step, per environment, overriding .env.production |
https://acc.backend.linkeddata.open-regels.nl (ACC), https://backend.linkeddata.open-regels.nl (PROD) |
VITE_OPERATON_URL |
.env.production |
https://operaton.open-regels.nl โ the same engine for both environments |
VITE_BUILD_SHA, VITE_BUILD_RUN |
The Build step, from github.sha and github.run_number |
Build provenance shown in the Changelog tab |
.env.acceptance exists but is loaded by nothing: only --mode acceptance
would read it, and no workflow passes that. Locally, npm start uses
.env.development (localhost:3001 for the Linked Data Explorer backend,
localhost:8081 for Operaton).
The build provenance is passed in rather than derived from git at build time, so the build id has exactly one source. The release version identifies a release, not a build of it: ACC and PROD can serve different builds of the same version, and the SHA and run number tell them apart.
The TriplyDB base URL, account, dataset and API token are entered by the user at runtime and stored in browser localStorage.