Skip to content

Pipeline Overview

The CI/CD system has clear separation between testing (automatic on PRs) and deploying (always intentional — release publish or manual dispatch).

How it works

flowchart LR PR["Pull Request"] -->|auto| Tests["test.yml\nRequired checks"] PR -->|auto, if infra changed| Validate["validate-infrastructure.yml\nBicep lint + what-if + PR comment"] PR -->|/deploy comment| Preview["deploy-pr-preview.yml\nDeploy to dev + E2E"] Release["Release published\n(release-please)"] -->|prerelease| DevChain["deploy-release.yml\ninfra → app → docs\non dev"] Release -->|stable| ProdChain["deploy-release.yml\ninfra → app → docs\non prod"] Dispatch["workflow_dispatch"] --> Any["Any individual workflow\nor deploy-release.yml"]

Deployment trigger matrix

Event Infra App Docs
PR opened / pushed never never never
Push to main never never never
/deploy comment on PR dev (via pr-preview) dev (via pr-preview) dev (via pr-preview)
Release published (prerelease) dev via orchestrator dev via orchestrator dev via orchestrator
Release published (stable) prod via orchestrator prod via orchestrator prod via orchestrator
workflow_dispatch choice choice choice

Tests trigger matrix

Event test.yml validate-infrastructure.yml
PR to main always (cancels on new push) if infra/** changed
Push to main never never
workflow_dispatch yes yes

Execution Scenarios

Scenario A: PR from Feature Branch

flowchart TD PR([PR opened / pushed]) --> VI{infra/** changed?} PR --> TC{any code changed?} VI -->|yes| INFRA[validate-infrastructure.yml\nlint → validate → what-if\nsecurity scan → PR comment] TC -->|yes| TEST[test.yml\ncancels previous run on new push] TEST --> U[unit\nVitest] TEST --> A[api\npytest] TEST --> TY[typecheck\nOpenAPI drift + tsc] TEST --> LK[links\nlinkinator] U & A & TY & LK --> GATE[gate\nTests Passed ✓] PR -->|/deploy comment| PRP[deploy-pr-preview.yml\ncheck-slash-command] PRP --> DC[detect-changes] DC --> FE{frontend or\nbackend changed?} DC --> DS{docs changed?} FE -->|yes| DA[deploy-app\nbuild images → deploy to dev\nsmoke tests] DA --> E2E[playwright-e2e] DA --> TB[test-backend] E2E & TB --> CMT[post-comment\nPR status + URLs] DS -->|yes| DD[deploy-docs\nMkDocs → dev SWA] DD --> CMT

Duration: ~12–20 min (full app+tests after /deploy), ~3–5 min (required checks only)


Scenario B: Merge to Main

No automatic deploy fires. Required checks (test.yml) ran on the PR. Deploy happens when a release is published.


Scenario C: Release Published (Prerelease → Dev)

flowchart TD REL([release-please publishes\nprerelease e.g. v1.2.0-alpha]) --> ORC[deploy-release.yml\ndetermine-environment → dev] ORC --> INFRA[deploy-infrastructure.yml\nvalidate → deploy infra to dev] INFRA --> APP[deploy-application.yml\nbuild images → push to dev ACR\ndeploy containers to dev] APP --> DOCS[deploy-docs.yml\nMkDocs → dev SWA]

Duration: ~20–30 minutes (sequential chain)


Scenario D: Release Published (Stable → Prod)

flowchart TD REL([release-please publishes\nstable release e.g. v1.2.0]) --> ORC[deploy-release.yml\ndetermine-environment → prod] ORC --> INFRA[deploy-infrastructure.yml\nvalidate → deploy infra to prod] INFRA --> APP[deploy-application.yml\nbuild images → push to prod ACR\ndeploy containers to prod] APP --> DOCS[deploy-docs.yml\nMkDocs → prod SWA]

Note: smoke-test-prod.yml is NOT part of the release chain. It runs on a schedule (3x/day on weekdays) and via workflow_dispatch only.

Duration: ~20–30 minutes (sequential chain)


Workflows

1. validate-infrastructure.yml — Bicep Validation (PR only)

Triggers: PR to main touching infra/** or the workflow file itself; workflow_dispatch

Job structure:

Job Runs when What it does
detect-changes Always Uses dorny/paths-filter to classify changed files into main_modules, dev_params, prod_params, shared
lint Always az bicep build on all templates and params files
validate-dev main_modules or dev_params changed ARM template validation (without + with containers)
validate-prod main_modules or prod_params changed ARM template validation for prod
validate-shared shared changed ARM template validation for shared infra
security-scan Always Checkov IaC scan; findings annotated inline on the PR
pr-comment PR only Posts/updates a summary comment with all job outcomes and what-if preview

Duration: ~2–4 minutes

2. deploy-infrastructure.yml — Infrastructure Deployment

Triggers: workflow_dispatch only (called by deploy-release.yml on releases)

Inputs: environment (dev/prod), ref (git ref to deploy), deploy_containers, deploy_custom_domain

Job structure:

Job What it does
determine-environment Passes through workflow_dispatch inputs
validate Bicep lint + ARM validate + what-if — always runs before deploy; has environment: context for OIDC token scoping
deploy Creates/updates all Azure resources; writes SWA token to Key Vault; registers Postgres AAD admin

What it does:

  • Creates/updates all Azure resources (resource group, ACR, Key Vault, Container Apps Environment, Log Analytics, App Insights)
  • Always creates managed identities and ACR/Key Vault role assignments — even without containers — so roles propagate before the next run
  • Fetches the SWA deployment token via az staticwebapp secrets list and writes it directly to Key Vault
  • Registers the backend managed identity as the PostgreSQL AAD administrator

Dev Container Apps scale-to-zero: dev CA frontend and backend have minReplicas=0. They scale up on the first HTTP request (~30–60 s cold start) and back to zero when idle.

Duration: ~5–10 minutes

3. deploy-release.yml — Release Orchestrator

Triggers: release: [published]; workflow_dispatch (with environment and ref inputs)

Environment routing: prerelease (e.g. v1.0.0-alpha) → dev; stable (e.g. v1.0.0) → prod

What it does: Triggers and sequentially waits for deploy-infrastructure.ymldeploy-application.ymldeploy-docs.yml, each using gh workflow run + gh run watch. A failure in any step stops the chain and opens a failure issue.

Duration: ~20–30 minutes (combines all three workflows)

4. deploy-pr-preview.yml — PR Preview

Triggers: /deploy comment on a PR; preview label added/push to labeled PR; workflow_dispatch (with pr_number input)

How to trigger: Comment /deploy on any open PR (one-off), or add the preview label for automatic re-deploys on every push. See PR Preview for full details.

Job structure:

Job Runs when What it does
check-slash-command /deploy comment Validates the trigger; reacts with rocket emoji
detect-changes After trigger confirmed Detects frontend/backend/cms/docs/infra changes vs main
deploy-infra infra/** changed Bicep lint + validate + deploy infrastructure to dev; registers Postgres AAD admin
deploy-app Frontend, backend, or CMS changed (after deploy-infra) Deploys CMS first, fetches preview token, builds frontend with VITE_CMS_PREVIEW_MODE=true, deploys all containers, smoke tests
playwright-e2e After deploy-app Playwright E2E with EasyAuth bypass; warms up frontend, backend, UIGen, and CMS
test-backend After deploy-app pytest against deployed backend
deploy-docs Docs files changed MkDocs → dev SWA
post-comment Always Posts/updates PR comment with status and URLs

Notes:

  • Uses shared dev environment — only one PR deployed at a time
  • Checks out refs/pull/<N>/merge — the merged state of the PR
  • Label-triggered flow auto-tears down on PR close or label removal; slash command does not clean up
  • The CMS is always deployed before the frontend — the preview token is fetched from GET {cms_url}/api/preview-token and baked into the frontend image as VITE_CMS_PREVIEW_TOKEN
  • UIGen CRUD E2E tests (uigen-crud.spec.ts) are skipped in CI — they require a locally running UIGen instance

Duration: ~14–20 min (app+tests including CMS serialization), ~2–3 min (docs only)

5. test.yml — Parallel Test Suite

Triggers: PR to main; workflow_dispatch

Concurrency: cancel-in-progress: true scoped to PR number — a new push to the same PR cancels the in-progress run immediately.

Job structure (all run in parallel):

Job What it does
unit npm run test:unit:coverage — Vitest; uploads unit-coverage artifact
api pytest across all 4 test files with coverage; uploads api-coverage artifact
typecheck Starts FastAPI, regenerates OpenAPI types, checks drift, runs npm run check
links Builds frontend, serves it locally, runs linkinator
coverage-doc Posts coverage summary as PR comment (needs unit + api)
changes Detects UI file changes for Chromatic skip logic
storybook Publishes to Chromatic (skipped if no UI files changed)
gate Required status check — fails if any job failed

Duration: ~3–5 minutes

6. deploy-application.yml — Application Deployment

Triggers: workflow_run after deploy-infrastructure.yml; workflow_dispatch (with environment, ref, deploy_containers inputs)

Environment detection for workflow_run: queries the triggering run's artifacts. If an artifact named deployment-prod-* exists → prod; otherwise → dev.

What it does:

  • Builds frontend (with VITE_DOCS_URL and VITE_UIGEN_URL build args), backend, UIGen, and CMS (portfolio-cms, Strapi v5) Docker images, tagged with commit SHA and :latest
  • Pushes all tags to the target environment's ACR
  • Deploys Container Apps with deploy_containers=true and image_tag=<sha>
  • Runs smoke tests against container URLs and custom domain URLs

Duration: ~5–8 minutes

7. deploy-docs.yml — MkDocs to Azure Static Web App

Triggers: workflow_run after deploy-application.yml (mirrors env); workflow_dispatch

Concurrency: docs-deploy-<env> — prevents parallel doc deployments to the same environment.

What it does: Builds MkDocs site (including draw.io PNG export, coverage report, Storybook) and deploys to the Azure Static Web App for the target environment.

URLs: https://docs.sven-relijveld.com (prod) / https://dev-docs.sven-relijveld.com (dev)

8. deploy-docs-github-pages.yml — MkDocs to GitHub Pages

Triggers: Push to main touching docs/** or mkdocs.yml

What it does: Builds and deploys MkDocs site to GitHub Pages.

URL: https://svenrelijveld1995.github.io/portfolio/

9. deploy-infrastructure-shared.yml — Shared Infrastructure (DNS + Domain)

Triggers: Push to main touching infra/shared.bicep, infra/modules/dns_zone.bicep, infra/modules/app_service_domain.bicep, infra/modules/app_configuration.bicep, infra/modules/app_configuration_rbac.bicep, infra/parameters.shared.bicepparam, or the workflow file; also fires via workflow_run after "Deploy Infrastructure" completes on main (to keep DNS TXT records in sync)

See Custom Domains for the full breakdown.

10. test-frontend.yml — Playwright E2E Tests (on-demand)

Triggers: workflow_dispatch only (accepts optional base_url input)

Use case: on-demand E2E validation against any deployed URL.

11. check-revision-health.yml — Revision Health Check

Triggers: Schedule — twice daily at 06:00 and 18:00 UTC

What it does: Checks Container App revision health in dev and prod. Opens a GitHub Issue labeled revision-unhealthy if any revision is unhealthy.


Environment Mapping

GitHub Environment Azure Resources Used By
dev dna-dev-portfolio-westeurope-rg PR preview, release prerelease
prod dna-prod-portfolio-westeurope-rg Release stable
shared dna-shared-portfolio-westeurope-rg DNS zone / custom domain

Deployed URLs

Production

Service URL
Frontend https://www.sven-relijveld.com
Frontend (raw) https://dna-prd-portfolio-we-ca-fe.politepebble-2dcbf46f.westeurope.azurecontainerapps.io
Backend https://dna-prd-portfolio-we-ca-be.politepebble-2dcbf46f.westeurope.azurecontainerapps.io
Docs (SWA) https://docs.sven-relijveld.com
Docs (Pages) https://svenrelijveld1995.github.io/portfolio/

Dev

Service URL
Frontend https://dev.sven-relijveld.com
Docs (SWA) https://dev-docs.sven-relijveld.com

Key Design Decisions

No Auto-Deploy on Push to Main

Pushing to main never triggers a deploy. The required checks (test.yml) still run automatically on every PR. Deployment is always intentional: triggered by a release publish or workflow_dispatch.

/deploy Slash Command for PR Previews

Commenting /deploy on any PR triggers deploy-pr-preview.yml. This lets you choose when to spend CI minutes on a preview, rather than deploying every push. The workflow can also be triggered from the Actions tab via workflow_dispatch.

Release-Driven Promotion (release-please)

Release-please manages two release tracks: the main branch publishes alpha prereleases (e.g. v0.21.0-alpha) that deploy to dev — this is fully automated. The release branch is used for stable releases (e.g. v0.21.0) but requires a manual tag push: release-please cannot compute a version bump on release because merging main produces a single chore: commit that release-please ignores. See Release Workflow for the complete 6-step stable release process.

Validation Always Runs Before Infra Deploy

deploy-infrastructure.yml has a validate job that runs Bicep lint + ARM validate + what-if before every deploy — whether triggered by deploy-release.yml or workflow_dispatch. This catches template errors before touching Azure.

Managed Identities Are Always Deployed

Managed identities and RBAC role assignments in container_apps.bicep are unconditional — they deploy even when deploy_containers=false. This ensures roles propagate in Azure AD before the subsequent run that actually creates the Container Apps.


Troubleshooting

PR preview fails: "Container Registry not found"

Dev infrastructure hasn't been deployed yet. Run the infrastructure workflow manually:

bash
gh workflow run deploy-infrastructure.yml --field environment=dev --field deploy_containers=false

Application deploys to wrong environment

The workflow_run trigger in deploy-application.yml and deploy-docs.yml detects the environment by querying the triggering run's artifact name (deployment-prod-* → prod, otherwise dev). If this misbehaves, check that the infra run uploaded its artifact via the "Upload Deployment Artifacts" step.

OIDC authentication fails

bash
# Verify federated credentials exist for the right subject
az ad app federated-credential list --id <AZURE_CLIENT_ID>

Expected subjects: repo:SvenRelijveld1995/portfolio:ref:refs/heads/main, repo:SvenRelijveld1995/portfolio:pull_request, repo:SvenRelijveld1995/portfolio:environment:prod, repo:SvenRelijveld1995/portfolio:environment:dev.


Next Steps

Page history

Field Value
Last updated 2026-06-29

Changelog

Date PRs Summary
2026-03-17 #77 Added SWA token KV write step and scale-to-zero dev CA note to deploy-infrastructure.yml description.
2026-03-20 #83 Updated playwright-e2e job description and notes to reflect Entra ID SSO auth bypass via CI_AUTH + EasyAuth client credentials flow.
2026-03-20 #87 Updated deploy-app to wait for infra; updated test-api.yml trigger (tests/** only); added dev-docs smoke tests; added deploy-dev-infra concurrency group; fixed EasyAuth session cookie flow via /.auth/me.
2026-03-22 #88 deploy-application.yml now passes image_tag=sha to Bicep to force a new Container App revision on every deploy; EasyAuth session detection updated to try Set-Cookie from /.auth/login/aad then /.auth/me then authenticationToken fallback, plus browser-level verification.
2026-04-13 Consolidated test-api, test-unit, typecheck, check-links into test.yml with parallel jobs and a gate check. test-frontend.yml now triggers post-deploy via workflow_run on PR Preview. Added Mermaid flow diagrams for all scenarios.
2026-04-16 test.yml: unit job now runs with --coverage and uploads lcov artifact; api job includes all 4 test files and uploads XML coverage artifact; push trigger broadened to server/, api/, tests/**. Removed incorrect postgres service container note.
2026-04-19 Push to main now deploys to dev (not prod). Only v* release tags deploy to prod. deploy-docs.yml gains env-detection job so it follows the same routing. Scenarios B and D added; environment routing table updated.
2026-04-21 #255 Fix workflow_run prod environment detection: both deploy-application.yml and deploy-docs.yml now detect prod by querying triggering run's artifact name (deployment-prod-*) instead of github.ref. Update deploy-application.yml and deploy-docs.yml workflow descriptions; add UIGen smoke tests note to E2E section. Fix troubleshooting note for wrong environment routing. Inaccurate note that workflow_run uses github.ref to detect prod (it was unreliable for manual dispatches)
2026-04-26 #266, #273, #274, #276 Update smoke test expected HTTP codes (frontend 401, backend /api/health 200); update deploy-infrastructure.yml description to note MI principal name fix and ENV_SHORT correction; update revision health check schedule to 12h. Fixed workflow count (16 not 11); added check-revision-health.yml entry. Incorrect smoke test HTTP code expectation for frontend root path
2026-05-08 #412 Streamlined CI/CD: no more auto-deploys on push-to-main or tag push. Tests only run on PRs (cancel-in-progress on new pushes). Deployments triggered by release publish or manual dispatch only. /deploy slash command from PR page triggers preview deploy. New deploy-release.yml orchestrator chains infra → app → docs on release. Push-to-main and tag-push auto-deploy triggers from all deploy workflows.
2026-05-30 #471, #472 validate job now has environment: context for OIDC token scoping (#471); release-please now manages two tracks: main→alpha prereleases (dev), release→stable releases (prod) (#472).
2026-06-02 #489, #490, #497, #508, #511, #514, #515 deploy-application.yml now builds and pushes the portfolio-cms image alongside frontend, backend, and UIGen. PR preview now builds and warms up CMS container; UIGen CRUD E2E tests skipped in CI.
2026-06-29 #585 Correct release-please Key Design Decision: stable releases require manual tag push; release-please cannot auto-tag the release branch Incorrect claim that no manual tag pushes are required for stable releases