Skip to content

Latest commit

 

History

History
178 lines (137 loc) · 8.7 KB

File metadata and controls

178 lines (137 loc) · 8.7 KB

TaskFlow — DevOps Setup Guide

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.


1. One-time Azure setup

# Variables
RG=taskflow-rg
LOC=centralindia
SUB=$(az account show --query id -o tsv)

az group create -n $RG -l $LOC

1a. Create the Entra app + OIDC federated credentials (no stored secrets)

GitHub 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

1b. Deploy the infrastructure

# 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"

1c. Create the JWT secret in Key Vault

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

1d. Grant the API's managed identity access to SQL

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 DDL

The CI deploy job authenticates to SQL as your OIDC service principal, so also add it as a user (same CREATE USER ... FROM EXTERNAL PROVIDER with the SP name) and grant db_owner so it can run migrations.


2. GitHub repository configuration

2a. Secrets (Settings → Secrets and variables → Actions → Secrets)

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

2b. Variables (same screen → Variables)

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

2c. Protected environment (the deploy approval gate)

Settings → Environments → New environmentproduction → enable Required reviewers (add yourself). The deploy job won't run until approved.

2d. Branch protection (Settings → Rules → Rulesets, or Branches → Add rule on main)

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.


3. SonarCloud (free for public repos)

  1. Sign in at sonarcloud.io with GitHub, import the repo.
  2. Set New Code definition and turn on the Sonar way quality gate.
  3. Disable "Automatic Analysis" (we run CI-based analysis).
  4. 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.


4. Expand-contract migrations (why the deploy order is safe)

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:

  1. 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.
  2. Migrate app code to read/write the new shape.
  3. 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 update over an Entra ("Active Directory Default") connection — no SQL passwords, reusing the OIDC az session.
  • 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>

5. Local dev quick reference

# 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/Api

6. App-side config the hardened infra expects

The infra uses managed identity everywhere — no keys or passwords. The API code must match:

  • SQL: connection string uses Authentication=Active Directory Managed Identity. Use Microsoft.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 with services.AddSignalR().AddAzureSignalR() (reads Azure:SignalR:ConnectionString). The service runs in Default mode, so you write normal server-side Hubs — identical to local self-hosted SignalR. No serverless /negotiate or upstream webhooks needed.
  • JWT: read Jwt:SigningKey from config — it resolves transparently from Key Vault via the app-setting reference.
  • CORS: configure in code (AddCors with 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).