Skip to content

CI/CD pipeline setup ​

Status: Live — implemented 2026-06-21 (ADO AB#3082). Workflow files are in .github/workflows/.

ADR references: ADR 0004 (CI/CD tool choice: GitHub Actions), ADR 0024 (containerised compute: Azure Container Apps + Docker), ADR 0029 (Azure authentication: managed identity with OIDC), ADR 0001 (monorepo structure: pnpm + Turborepo), ADR 0008 (platform composition: API-first, single SDK entry point).


Overview ​

Heritage Community Hub uses GitHub Actions (.github/workflows/deploy.yml) for all continuous integration (CI) and continuous deployment (CD). Azure DevOps (ADO) Boards is the work-tracking tool only — all pipeline YAML lives in GitHub.

The CI/CD design is shaped by three decisions:

  1. GitHub Actions is the CI/CD tool (ADR 0004). The code lives on GitHub, so keeping CI/CD there eliminates cross-system token management.
  2. The API is a containerised Node.js application (ADR 0024). The CI pipeline builds a Docker image and pushes it to GitHub Container Registry (GHCR); CD deploys it to Azure Container Apps. The web app is a Vite/React SPA deployed to Azure Static Web Apps.
  3. Azure authentication uses a user-assigned managed identity with OIDC workload identity federation (ADR 0029). No client secrets are stored in GitHub.

Pipeline structure ​

One workflow file handles both CI and CD: .github/workflows/deploy.yml.

Jobs ​

JobTriggerWhat it does
build-and-pushEvery push to mainBuilds the API Docker image and pushes to GHCR (ghcr.io/heritage-virginia/heritage-community-hub/api)
deploy-apiAZURE_DEPLOY_API_ENABLED == 'true'Authenticates to Azure via OIDC; runs az containerapp update with the new image tag
deploy-webAZURE_DEPLOY_WEB_ENABLED == 'true'Builds apps/web with Vite (baking in VITE_CLERK_PUBLISHABLE_KEY and VITE_API_BASE_URL); uploads pre-built dist/ to Azure SWA via Azure/static-web-apps-deploy@v1
smoke-checkAfter deploy-api succeedscurl -f /health against the live Container App URL

Environment variables baked into the web build ​

VariableValueNotes
VITE_CLERK_PUBLISHABLE_KEYfrom GitHub secretMust be the Clerk publishable key (pk_live_...). Set once per Clerk app creation.
VITE_API_BASE_URLhttps://ca-heritageva-api-prod.orangeriver-77e27b61.eastus.azurecontainerapps.io/api/v1Hardcoded in the workflow; update if the Container App FQDN changes

Deployment gates (repository variables) ​

VariableDefaultEffect
AZURE_DEPLOY_API_ENABLEDtrueEnables deploy-api and smoke-check jobs
AZURE_DEPLOY_WEB_ENABLEDtrueEnables deploy-web job

Set to false to suspend deployment while keeping builds running (e.g., during infrastructure changes).


Single environment ​

There is one environment: production. There are no dev, staging, or test environments (AB#3080 deleted). The GitHub Actions environment: production block enforces the approval gate.


Secrets management ​

No secrets, tokens, or credentials are stored in pipeline YAML or repository files.

Secret categorySourceMechanism
Database connection stringAzure Key Vault (kv-heritageva-prod)Container App system-assigned managed identity reads Key Vault at startup
Clerk secret keyAzure Key Vault (kv-heritageva-prod, secret: clerk-secret-key)Same system-assigned managed identity
Twilio credentialsAzure Key Vault (kv-heritageva-prod)Same system-assigned managed identity
Azure → GitHub Actions authUser-assigned managed identity id-heritageva-github-actionsOIDC workload identity federation — see ADR 0029. No client secret stored.
Azure SWA deployment tokenGitHub Actions secret (AZURE_STATIC_WEB_APPS_API_TOKEN)Set once by the platform owner; rotated annually
Clerk publishable key (web build)GitHub Actions secret (VITE_CLERK_PUBLISHABLE_KEY)Public key baked into the Vite bundle at build time; safe to store as a secret
GHCR pushsecrets.GITHUB_TOKEN (automatic)GitHub provides this automatically; no configuration needed

GitHub Actions → Azure authentication ​

The workflow uses azure/login@v2 with the user-assigned managed identity (ADR 0029):

yaml
- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

The managed identity id-heritageva-github-actions has a federated credential scoped to repo:Heritage-Virginia/heritage-community-hub:ref:refs/heads/main. The azure/login action exchanges the GitHub OIDC token for a short-lived Azure access token — no stored secret.

The workflow has permissions: id-token: write to enable OIDC token issuance.

Container App → Key Vault authentication ​

The Container App uses its system-assigned managed identity (separate from the GitHub Actions managed identity) with Key Vault Secrets User role on kv-heritageva-prod. Application secrets are injected as Container App secrets (backed by Key Vault references) at container startup.


Container registry ​

API images are pushed to GHCR (GitHub Container Registry), not Azure Container Registry.

DetailValue
Registryghcr.io
Imageghcr.io/heritage-virginia/heritage-community-hub/api
Tagssha-<7-char-git-sha> (primary) + latest
Authdocker/login-action@v3 with secrets.GITHUB_TOKEN

The GHCR package is private. The Container App pulls via a registry credential (ghcr-token secret on the Container App).


Branch strategy &ZeroWidthSpace;

RuleDetail
main is productionNo direct commits to main. All changes arrive via pull request.
Feature branchesNamed feat/, fix/, chore/, or docs/ following the project commit convention
No long-lived branchesFeature branches are merged and deleted
PR required to mergeBranch protection on main requires at least one approving review and passing CI
Mobile release branchesrelease/mobile/<version> for EAS Build (Expo Application Services) — separate from the web/API pipeline (ADR 0002)

ADO integration &ZeroWidthSpace;

  • Commit messages include AB#<id> references. ADO automatically transitions linked work items when the commit reaches main.
  • Pull request descriptions include AB#<id> to link to the ADO work item.
  • ADO Pipelines is not used (ADR 0004). All pipeline YAML lives in .github/workflows/.

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.