Everything needed to make the Bicep + workflows in this folder actually run. Parts that don't live in files (OIDC federation, branch protection, repo secrets) are documented here step by step.
File placement: copy
infra/,.github/to the root of your repo. The repo must be public for SonarCloud + CodeQL to be free.
# Variables
RG=taskflow-rg
LOC=centralindia
SUB=$(az account show --query id -o tsv)
az group create -n $RG -l $LOCGitHub Actions authenticates to Azure with a short-lived token — no client secret is ever stored.
# App registration + service principal
APP_ID=$(az ad app create --display-name "taskflow-github-oidc" --query appId -o tsv)
az ad sp create --id $APP_ID
# Federated credential trusting your repo's main branch
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "taskflow-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<OWNER>/<REPO>:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
# A second credential for the "production" environment approval gate
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "taskflow-env-prod",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<OWNER>/<REPO>:environment:production",
"audiences": ["api://AzureADTokenExchange"]
}'
# Give the SP rights on the resource group (Contributor is enough for deploy)
az role assignment create --assignee $APP_ID --role Contributor \
--scope /subscriptions/$SUB/resourceGroups/$RG# Your own object id becomes the SQL Entra admin
MY_OID=$(az ad signed-in-user show --query id -o tsv)
az deployment group create -g $RG -f infra/main.bicep \
-p appName=taskflow location=$LOC \
sqlAadAdminObjectId=$MY_OID \
sqlAadAdminLogin="$(az ad signed-in-user show --query userPrincipalName -o tsv)" \
alertEmail="you@example.com"Key Vault runs in RBAC mode, so grant yourself secret-write, then set the value. (Done post-deploy on purpose — avoids a same-deployment RBAC propagation race.)
KV=<keyVaultName-from-portal> # taskflowkv<suffix>
ME=$(az ad signed-in-user show --query id -o tsv)
az role assignment create --assignee $ME --role "Key Vault Secrets Officer" \
--scope $(az keyvault show -n $KV --query id -o tsv)
az keyvault secret set --vault-name $KV --name JwtSigningKey --value "$(openssl rand -base64 48)"The connection string uses managed identity (no password). Create a contained DB user for it. Connect to the DB as the Entra admin and run:
-- Replace <API_APP_NAME> with the deployed Web App name
CREATE USER [<API_APP_NAME>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [<API_APP_NAME>];
ALTER ROLE db_datawriter ADD MEMBER [<API_APP_NAME>];
ALTER ROLE db_ddladmin ADD MEMBER [<API_APP_NAME>]; -- needed if app runs any DDLThe CI deploy job authenticates to SQL as your OIDC service principal, so also add it as a user (same
CREATE USER ... FROM EXTERNAL PROVIDERwith the SP name) and grantdb_ownerso it can run migrations.
| Secret | Where it comes from |
|---|---|
AZURE_CLIENT_ID |
$APP_ID from step 1a |
AZURE_TENANT_ID |
az account show --query tenantId -o tsv |
AZURE_SUBSCRIPTION_ID |
$SUB |
SONAR_TOKEN |
SonarCloud → My Account → Security → Generate token |
The SWA deployment token is not stored — the workflow fetches it just-in-time via the OIDC session (
az staticwebapp secrets list).
| Variable | Value |
|---|---|
AZURE_RG |
taskflow-rg |
API_APP_NAME |
deployed Web App name (Bicep output apiName) |
SWA_NAME |
deployed Static Web App name (Bicep output swaName) |
SQL_SERVER_NAME |
Bicep sqlServerFqdn without .database.windows.net |
SQL_DB_NAME |
taskflowdb |
SONAR_PROJECT_KEY |
from SonarCloud project |
SONAR_ORG |
your SonarCloud org key |
Settings → Environments → New environment → production → enable Required reviewers (add yourself). The deploy job won't run until approved.
Enable all of:
- Require a pull request before merging (≥1 approval; dismiss stale approvals).
- Require status checks to pass — select:
build-test,analyze (csharp),analyze (javascript-typescript). - Require branches to be up to date before merging.
- Require conversation resolution.
- Block force pushes and do not allow bypassing the above.
Result: nothing reaches main without a green build, passing tests, a passing Sonar quality gate, and clean CodeQL.
- Sign in at sonarcloud.io with GitHub, import the repo.
- Set New Code definition and turn on the Sonar way quality gate.
- Disable "Automatic Analysis" (we run CI-based analysis).
- Copy the project key + org into the GitHub variables above.
The workflow runs dotnet-sonarscanner with sonar.qualitygate.wait=true, so a failing gate fails the build — which branch protection then blocks from merging.
The deploy job runs backup → migrate → deploy app, and migrations follow the expand-contract pattern so the currently running app keeps working while the schema changes:
- Expand (this release): only additive, backward-compatible schema changes — add nullable columns, new tables, new indexes. Never rename/drop in the same release. The old app version still runs against the new schema, so applying the migration before swapping the app is safe.
- Migrate app code to read/write the new shape.
- Contract (a later release, after the new code is live everywhere): drop the old column/table.
Rules baked into the pipeline:
- The migration applies via
dotnet ef database updateover an Entra ("Active Directory Default") connection — no SQL passwords, reusing the OIDCazsession. - The runner's IP is added to the SQL firewall just-in-time before migrating and removed afterward (
if: always()), because GitHub-hosted runners aren't covered by "Allow Azure services." - Backup safety net is automatic Point-in-Time Restore (free, 7-day) — the pipeline does not copy the DB (a copy would fall outside the free offer). The deploy step logs the PITR window before migrating.
- On a multi-step destructive change, split it across two PRs/releases (expand, then contract) — never both at once.
Rollback: if a deploy fails the smoke test, redeploy the previous API artifact; the expand-only schema is still compatible with it. For a bad data migration, restore to a pre-deploy timestamp:
az sql db restore -g <rg> -s <srv> -n taskflowdb \
--dest-name taskflowdb-restored --time <UTC-timestamp-before-deploy># API
dotnet run --project backend/src/Api
# Frontend
cd frontend && npm install && npm run dev
# Add a migration
dotnet ef migrations add <Name> \
--project backend/src/Infrastructure --startup-project backend/src/ApiThe infra uses managed identity everywhere — no keys or passwords. The API code must match:
- SQL: connection string uses
Authentication=Active Directory Managed Identity. UseMicrosoft.Data.SqlClient(EF Core default on .NET 8+); no password. - SignalR: the app setting is a managed-identity connection string (
AuthType=azure.msi). Wire it withservices.AddSignalR().AddAzureSignalR()(readsAzure:SignalR:ConnectionString). The service runs in Default mode, so you write normal server-side Hubs — identical to local self-hosted SignalR. No serverless/negotiateor upstream webhooks needed. - JWT: read
Jwt:SigningKeyfrom config — it resolves transparently from Key Vault via the app-setting reference. - CORS: configure in code (
AddCorswith the SWA origin +AllowCredentials()), not at the App Service level — the Bicep deliberately omits platform CORS to avoid duplicate headers on credentialed WebSocket requests. - /health: expose a lightweight endpoint that does not touch SQL (so the keep-warm pinger keeps the app warm without resuming the database).