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¶
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¶
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)¶
Duration: ~20–30 minutes (sequential chain)
Scenario D: Release Published (Stable → Prod)¶
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 listand 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.yml → deploy-application.yml → deploy-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-tokenand baked into the frontend image asVITE_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_URLandVITE_UIGEN_URLbuild 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=trueandimage_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:
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¶
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¶
- OIDC & Secrets — OIDC setup, federated credentials, GitHub Secrets
- Custom Domains — DNS zones, 3-stage cert process, shared.bicep
- Resource Overview — Azure resources deployed by these workflows
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 |