Skip to content

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):

gh api repos/SvenRelijveld1995/portfolio/issues/<PR_NUMBER>/comments -f body="/deploy"

Option 2 — preview label

Add the preview label to the PR. The pipeline fires automatically on:

  • labeled — label added to the PR
  • synchronize — new commit pushed while the label is already present
  • unlabeled / 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=draft to every CMS API request via the shared cmsUrl() helper in client/src/lib/cms-fetch.ts.
  • An Authorization: Bearer <VITE_CMS_PREVIEW_TOKEN> header is added to every request via cmsHeaders().
  • A fixed amber banner — "Preview mode — showing draft content" — is displayed at the top of the page via PreviewBanner (rendered at the root in App.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:

  1. Build and push CMS image to dev ACR
  2. Deploy CMS Container App (pr-<N> tag)
  3. Poll {cms_url}/_health until the CMS responds (handles cold-start delay)
  4. GET {cms_url}/api/preview-token with X-Bootstrap-Secret header
  5. Build frontend image with VITE_CMS_PREVIEW_TOKEN=<token> build arg
  6. 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 /deploy slash command. Teardown on close only fires when the preview label 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