PR Preview¶
Every pull request can be deployed to a full isolated environment — frontend, backend, CMS
(Strapi), and UIGen — running in the shared dev Azure Container Apps environment tagged with the PR
number (e.g. pr-42).
How to trigger a PR preview¶
Option 1 — /deploy slash command¶
Comment /deploy on any open PR. The bot reacts with a rocket emoji and starts the preview
pipeline. This is the default method for one-off deploys.
To trigger from the command line (use PowerShell to avoid git-bash path conversion):
Option 2 — preview label¶
Add the preview label to the PR. The pipeline fires automatically on:
labeled— label added to the PRsynchronize— new commit pushed while the label is already presentunlabeled/closed— teardown job removes the PR replicas
This is the preferred method for PRs under active review, because re-deploys happen automatically on every push without needing a new comment.
Option 3 — workflow_dispatch¶
Trigger deploy-pr-preview.yml manually from the GitHub Actions tab with a pr_number input.
Useful for debugging the pipeline itself.
What gets deployed¶
| Container | Tag pattern | Notes |
|---|---|---|
| Frontend | pr-<N> |
Built with CMS preview mode enabled (see below) |
| Backend | pr-<N> |
Standard API build |
| CMS (Strapi) | pr-<N> |
Deployed first; token fetched before frontend build |
| UIGen | pr-<N> |
Admin UI for content and CV endpoints |
All containers share the dev Container Apps environment. Only one PR is active at a time.
CMS preview mode¶
When the PR preview pipeline runs, the frontend is built with draft CMS content enabled. This allows content editors and reviewers to see unpublished (draft) Strapi entries rendered in the PR frontend.
How it works¶
deploy-pr-preview.yml
|
+-- deploy CMS container (pr-N tag)
|
+-- poll CMS health endpoint until ready
|
+-- fetch preview token:
| GET {cms_url}/api/preview-token
| Header: X-Bootstrap-Secret: <STRAPI_BOOTSTRAP_SECRET>
|
+-- build frontend Docker image:
| --build-arg VITE_CMS_PREVIEW_MODE=true
| --build-arg VITE_CMS_PREVIEW_TOKEN=<token>
| --build-arg VITE_CMS_URL=<cms_url>
|
+-- deploy frontend container (pr-N tag)
|
+-- post PR comment with URLs
The frontend reads VITE_CMS_PREVIEW_MODE (baked in at build time). When true:
- All 7 CMS fetch hooks (
useHeroSection,useAboutSection, etc.) append?status=draftto every CMS API request via the sharedcmsUrl()helper inclient/src/lib/cms-fetch.ts. - An
Authorization: Bearer <VITE_CMS_PREVIEW_TOKEN>header is added to every request viacmsHeaders(). - A fixed amber banner — "Preview mode — showing draft content" — is displayed at the top of the
page via
PreviewBanner(rendered at the root inApp.tsx).
In production, VITE_CMS_PREVIEW_MODE is false (the default). The production frontend fetches
only published content with no auth header.
Strapi preview-button plugin¶
The Strapi admin sidebar shows a "Preview" button for project-page, content-post, and
hero-section entries. Clicking it opens the PR frontend at the matching URL, showing the draft
state of that entry.
The plugin reads PREVIEW_FRONTEND_URL from the CMS container environment. In the PR preview
pipeline, this is set to https://dev.sven-relijveld.com. In production it falls back to
https://www.sven-relijveld.com.
Secrets and variables¶
STRAPI_BOOTSTRAP_SECRET (GitHub secret, dev environment)¶
A shared secret used to authenticate the token fetch call from CI to the CMS
/api/preview-token endpoint. The endpoint returns 403 if the header is missing or wrong.
The same secret is injected into the CMS container as an environment variable via Bicep
(strapi_bootstrap_secret param in container_apps.bicep). It is not exposed to the frontend.
For local development, set STRAPI_BOOTSTRAP_SECRET in cms/.env. The docker-compose :-
fallback does not apply if the variable is set but empty in the host shell — the .env file is
required.
PREVIEW_FRONTEND_URL (Bicep param, injected at CMS deploy time)¶
The URL the Strapi preview-button plugin opens when an editor clicks "Preview". Set to the PR
frontend URL at deploy time. Defaults to https://www.sven-relijveld.com in production.
CI serialization¶
The CMS must be deployed and healthy before the frontend image is built, because the preview token is fetched at build time and baked into the frontend image. The pipeline order is:
- Build and push CMS image to dev ACR
- Deploy CMS Container App (
pr-<N>tag) - Poll
{cms_url}/_healthuntil the CMS responds (handles cold-start delay) GET {cms_url}/api/preview-tokenwithX-Bootstrap-Secretheader- Build frontend image with
VITE_CMS_PREVIEW_TOKEN=<token>build arg - Deploy frontend, backend, UIGen
This adds approximately 2-3 minutes to the total preview pipeline duration compared to a run without CMS changes.
Token endpoint implementation notes¶
The /api/preview-token endpoint is registered directly on the Koa router inside Strapi's
register() lifecycle hook (cms/src/index.js), not as a standard src/api/ directory. Strapi
v5 does not discover custom APIs that lack a content-type schema, so the standard API registration
path cannot be used.
Token creation is deferred to the first HTTP request rather than running in bootstrap(), because
tokenService.list() is not safe to call during bootstrap in Strapi v5.
The endpoint returns:
| Status | Condition |
|---|---|
| 200 | Token value returned in JSON |
| 403 | Missing or wrong X-Bootstrap-Secret header |
| 409 | Token exists in DB but cached plaintext is gone (CMS redeployed without clearing token row) |
If a 409 occurs, the fix is to delete the token row from the Strapi strapi_api_tokens table and
redeploy the CMS container so bootstrap creates a fresh token.
Job structure (deploy-pr-preview.yml)¶
| Job | Trigger | What it does |
|---|---|---|
check-slash-command |
/deploy comment |
Validates trigger; reacts with rocket emoji |
detect-changes |
After trigger confirmed | Detects which layers changed vs. main |
deploy-infra |
infra/** changed |
Bicep validate + deploy infrastructure to dev |
deploy-app |
Frontend, backend, or CMS changed | Serialized CMS → token fetch → frontend build → deploy all containers; smoke tests |
playwright-e2e |
After deploy-app |
Playwright E2E with EasyAuth bypass; warmup polls all containers |
test-backend |
After deploy-app |
pytest against deployed backend |
deploy-docs |
Docs files changed | MkDocs → dev SWA |
post-comment |
Always | Posts or updates PR comment with status and URLs |
The pull_request trigger (label-based) follows the same job structure. PR_NUMBER is resolved
from github.event.pull_request.number when the event is pull_request, or from the slash
command context when it is issue_comment.
Limitations¶
- One active preview at a time — the dev Container Apps environment is shared. Deploying a second PR overwrites the first.
- No automatic teardown on PR close when using the
/deployslash command. Teardown on close only fires when thepreviewlabel is present (label-triggered flow). - The CMS preview token is baked into the frontend image at build time. If the CMS container is restarted and a new token is created (e.g. the old token row was deleted), a new preview deploy is needed to pick up the new token.
- UIGen CRUD E2E tests (
uigen-crud.spec.ts) are skipped in CI — they require a locally running UIGen instance.
See also¶
- Pipeline Overview — full CI/CD workflow reference
- Plan: PR Preview + Strapi — implementation notes and phase-by-phase design decisions