CICD and Infra#1
Conversation
Backend: - Add ASP.NET Identity with ApplicationUser and EF Core migrations - Add AuthController with register, login, and me endpoints - Add JwtTokenService for issuing signed access tokens - Scope NotesController to authenticated user's notes via OwnerId - Add AuthEndpointsTests, NotesAuthorizationTests, and update NotesEndpointsTests to use authenticated clients Frontend: - Add AuthProvider, useAuth hook, ProtectedRoute, and token storage - Add LoginPage and RegisterPage with client-side validation - Extract shared HTTP client with automatic Bearer header injection - Update Layout with sign-out button and user email display - Update MSW handlers and test helpers with auth support
Bicep build output (ARM JSON) should not be committed; it's regenerated on demand by az bicep build and the CI workflow.
Provisions the full stack on cheapest-tier Azure resources: - Resource group per environment (dev, prod) - Linux App Service on F1 (Free) with .NET 10, system-assigned MI - Azure SQL serverless with auto-pause and useFreeLimit, Entra-only auth - Static Web Apps on Free tier - Key Vault (RBAC) holding the JWT signing key, referenced via @Microsoft.KeyVault - Log Analytics + Application Insights App Service MI is granted Key Vault Secrets User; the SQL contained user is created by the CD workflow. No secrets are present in source or in .bicepparam files (values come from env vars at deploy time). Includes bootstrap/setup-oidc.sh for one-time GitHub OIDC app registration and subscription role assignments.
- ci.yml: backend (dotnet test), frontend (lint/typecheck/test/build), and bicep build on PR and push to main - infra-validate.yml: az deployment group what-if on PRs touching infra/**, posted to the PR summary - cd.yml: orchestrates build-backend + build-frontend, then dev deploy, then prod deploy behind the prod environment's reviewer gate - _deploy.yml: reusable deploy job that runs bicep, seeds jwt-signing-key in Key Vault if missing, grants the App Service MI access to SQL via sqlcmd + Entra access token, deploys the API zip, rebuilds the SPA with VITE_API_BASE_URL, and uploads to SWA All Azure auth is OIDC federated; no long-lived client secrets.
Covers architecture, cost breakdown, security model (OIDC + managed identity + Key Vault), repo layout, first-time OIDC bootstrap, GitHub Environment setup, day-to-day operations (what-if, JWT rotation, teardown), the app-code env-var contract, outstanding app-code work, and troubleshooting.
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end CI/CD and Azure infrastructure provisioning for the Notes app, while introducing JWT-based authentication + user-scoped notes in the backend and corresponding auth UX/testing updates in the frontend.
Changes:
- Added subscription/resource-group scoped Bicep templates + bootstrap scripts to provision App Service, SWA, SQL, Key Vault, and observability resources.
- Introduced GitHub Actions workflows for CI, infra what-if validation on PRs, and reusable CD deployments (dev → prod).
- Implemented JWT authentication + ASP.NET Identity in the backend (including migrations and integration tests) and added frontend auth context, routes, and MSW support.
Reviewed changes
Copilot reviewed 63 out of 66 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/modules/workload.bicep | Orchestrates workload resource modules and wires outputs between them. |
| infra/modules/static-web-app.bicep | Defines the Static Web App resource (unlinked, deployed via token). |
| infra/modules/sql.bicep | Provisions Azure SQL server/database with Entra admin + firewall settings. |
| infra/modules/log-analytics.bicep | Creates Log Analytics workspace used by App Insights. |
| infra/modules/key-vault.bicep | Creates Key Vault with RBAC enabled and role assignments for deployers. |
| infra/modules/app-service.bicep | Creates Linux App Service + settings for DB/JWT/CORS and KV access. |
| infra/modules/app-insights.bicep | Creates App Insights linked to Log Analytics workspace. |
| infra/main.prod.bicepparam | Prod parameterization via environment variables (no literals). |
| infra/main.dev.bicepparam | Dev parameterization via environment variables (no literals). |
| infra/main.bicep | Subscription-scope entrypoint creating RG + workload module. |
| infra/bootstrap/setup-oidc.sh | Bash bootstrap for GitHub OIDC app/SP + federated creds + role assignments. |
| infra/bootstrap/setup-oidc.ps1 | PowerShell bootstrap equivalent for GitHub OIDC setup. |
| infra/abbreviations.json | Centralized naming prefixes used by Bicep modules. |
| frontend/src/test/setup.ts | Resets auth + notes stores and clears token between tests. |
| frontend/src/test/server.ts | Adds auth store reset/seeding helpers for MSW test server. |
| frontend/src/test/renderWithProviders.tsx | Wraps tests with QueryClient/Router/AuthProvider and seeds auth state. |
| frontend/src/test/handlers.ts | Extends MSW handlers with auth endpoints and per-user note scoping. |
| frontend/src/pages/RegisterPage.tsx | Adds registration page with client-side validation and navigation. |
| frontend/src/pages/RegisterPage.test.tsx | Tests register flow, validation, and server duplicate-email errors. |
| frontend/src/pages/LoginPage.tsx | Adds login page with validation and UnauthorizedError handling. |
| frontend/src/pages/LoginPage.test.tsx | Tests login flows and invalid-credentials behavior. |
| frontend/src/main.tsx | Wraps app in AuthProvider at the application root. |
| frontend/src/components/Layout.tsx | Displays signed-in email and adds sign-out action in header. |
| frontend/src/components/Layout.test.tsx | Tests header email rendering and sign-out behavior. |
| frontend/src/auth/useAuth.ts | Adds useAuth() hook for consuming the auth context. |
| frontend/src/auth/tokenStorage.ts | Adds safe localStorage helpers for token persistence. |
| frontend/src/auth/authContextInternal.ts | Defines auth context/state types and creates context. |
| frontend/src/auth/ProtectedRoute.tsx | Adds route guard that redirects unauthenticated users to /login. |
| frontend/src/auth/ProtectedRoute.test.tsx | Tests redirect vs authenticated rendering behavior. |
| frontend/src/auth/AuthContext.tsx | Implements AuthProvider bootstrap/login/register/logout + HTTP hooks wiring. |
| frontend/src/api/notes.ts | Switches notes API functions to use shared HTTP request helper. |
| frontend/src/api/http.ts | Introduces shared request wrapper with auth header injection + 401 handling. |
| frontend/src/api/auth.ts | Adds typed auth API client (register/login/me). |
| frontend/src/App.tsx | Adds login/register routes and protects notes routes behind ProtectedRoute. |
| docs/deployment.md | Documents Azure architecture, CI/CD, bootstrap steps, and operational runbooks. |
| backend/Notes.Api/appsettings.json | Adds Jwt config section for issuer/audience/signing key/lifetime. |
| backend/Notes.Api/appsettings.Development.json | Adds dev-only JWT signing key override. |
| backend/Notes.Api/Program.cs | Wires authentication middleware and auth service registration. |
| backend/Notes.Api/Notes.Api.csproj | Adds JWT bearer auth + ASP.NET Identity EF Core packages. |
| backend/Notes.Api/Migrations/Sqlite/NotesDbContextModelSnapshot.cs | Updates SQLite snapshot for Identity + Note owner relationship. |
| backend/Notes.Api/Migrations/Sqlite/20260418001323_AddIdentityAndNoteOwner.cs | Adds Identity tables + Note.OwnerId for SQLite provider. |
| backend/Notes.Api/Migrations/Sqlite/20260418001323_AddIdentityAndNoteOwner.Designer.cs | EF designer for the SQLite migration. |
| backend/Notes.Api/Migrations/SqlServer/20260418000922_AddIdentityAndNoteOwner.cs | Adds Identity tables + Note.OwnerId for SQL Server provider. |
| backend/Notes.Api/Migrations/SqlServer/20260418000922_AddIdentityAndNoteOwner.Designer.cs | EF designer for the SQL Server migration. |
| backend/Notes.Api/Extensions/ServiceCollectionExtensions.cs | Adds AddAppAuthentication() and JWT/Identity service configuration. |
| backend/Notes.Api/Domain/Note.cs | Adds OwnerId to Note domain model. |
| backend/Notes.Api/Domain/ApplicationUser.cs | Adds ApplicationUser Identity model. |
| backend/Notes.Api/Data/NotesDbContext.cs | Switches DbContext to IdentityDbContext and calls base.OnModelCreating(). |
| backend/Notes.Api/Data/Configurations/NoteConfiguration.cs | Enforces OwnerId required, indexed, and FK to ApplicationUser. |
| backend/Notes.Api/Controllers/NotesController.cs | Requires auth and scopes all note operations to the current user. |
| backend/Notes.Api/Auth/JwtTokenService.cs | Issues JWT access tokens with standard claims. |
| backend/Notes.Api/Auth/JwtOptions.cs | Adds strongly-typed JWT options binding target. |
| backend/Notes.Api/Auth/IJwtTokenService.cs | Defines token service interface. |
| backend/Notes.Api/Auth/AuthController.cs | Adds register/login/me endpoints backed by Identity + JWT issuance. |
| backend/Notes.Api/Auth/AuthContracts.cs | Adds request/response DTOs for auth endpoints. |
| backend/Notes.Api.Tests/NotesEndpointsTests.cs | Updates notes tests to use authenticated clients. |
| backend/Notes.Api.Tests/NotesAuthorizationTests.cs | Adds tests for 401 without token and per-owner data isolation. |
| backend/Notes.Api.Tests/NotesApiFactory.cs | Adds JWT config + helpers to create authenticated test clients. |
| backend/Notes.Api.Tests/AuthEndpointsTests.cs | Adds integration tests for register/login/me endpoints. |
| .gitignore | Ignores compiled Bicep JSON artifacts under infra/. |
| .github/workflows/infra-validate.yml | Adds PR what-if workflow for infra changes with step summary output. |
| .github/workflows/ci.yml | Adds CI jobs for backend, frontend, and Bicep build validation. |
| .github/workflows/cd.yml | Adds CD pipeline that builds artifacts and deploys dev/prod via reusable workflow. |
| .github/workflows/_deploy.yml | Adds reusable deploy job: infra, KV seeding, SQL grants, API deploy, SWA deploy. |
| .github/agents/devops.agent.md | Updates DevOps agent tool access and makes it user-invocable. |
| .github/agents/coder.agent.md | Updates Clean Coder agent tool access list. |
Files not reviewed (2)
- backend/Notes.Api/Migrations/SqlServer/20260418000922_AddIdentityAndNoteOwner.Designer.cs: Language not supported
- backend/Notes.Api/Migrations/Sqlite/20260418001323_AddIdentityAndNoteOwner.Designer.cs: Language not supported
Comments suppressed due to low confidence (1)
backend/Notes.Api/Program.cs:17
- CORS is still hardcoded to
http://localhost:5173, but the infra setsCors__AllowedOrigins__0to the SWA hostname. In Azure this will block browser calls. Read allowed origins from configuration (e.g.,Cors:AllowedOrigins) and use those in the CORS policy.
builder.Services.AddCors(options =>
{
options.AddPolicy("Frontend", policy => policy
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod());
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ftpsState: 'Disabled' | ||
| healthCheckPath: '/health' | ||
| cors: { |
There was a problem hiding this comment.
healthCheckPath is set to /health, but the API project doesn’t currently map a health endpoint (no MapHealthChecks / controller route). App Service health probes will get 404 and can mark instances unhealthy. Either add a real /health endpoint in the backend or remove/parameterize healthCheckPath.
| resource kvRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { | ||
| name: guid(keyVault.id, site.id, secretsUserRoleId) | ||
| scope: keyVault | ||
| properties: { | ||
| roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', secretsUserRoleId) | ||
| principalId: site.identity.principalId | ||
| principalType: 'ServicePrincipal' | ||
| } |
There was a problem hiding this comment.
The role assignment name is derived from site.id, but the assigned principal is site.identity.principalId. If the system-assigned identity is ever re-created (principalId changes) while the site resource ID stays the same, the role assignment name will stay constant and the deployment can fail because role assignments are immutable. Use site.identity.principalId in the guid(...) input to keep the name tied to the principal.
| # Allow this runner's public IP temporarily so sqlcmd can connect. | ||
| runner_ip=$(curl -s https://api.ipify.org) | ||
| az sql server firewall-rule create \ | ||
| --resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \ | ||
| --server "$SQL_SERVER" \ | ||
| --name "gh-runner-${{ github.run_id }}" \ | ||
| --start-ip-address "$runner_ip" \ | ||
| --end-ip-address "$runner_ip" >/dev/null | ||
|
|
||
| # Install sqlcmd (go edition). | ||
| curl -fsSL https://github.com/microsoft/go-sqlcmd/releases/download/v1.8.0/sqlcmd-linux-amd64.tar.bz2 \ | ||
| | tar -xj -C /tmp | ||
| sudo mv /tmp/sqlcmd /usr/local/bin/sqlcmd | ||
|
|
||
| # Acquire access token as the deployer SP (already logged in via OIDC). | ||
| token=$(az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv) | ||
|
|
||
| cat <<SQL > /tmp/grant.sql | ||
| IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'${APP_NAME}') | ||
| BEGIN | ||
| CREATE USER [${APP_NAME}] FROM EXTERNAL PROVIDER; | ||
| END | ||
| ALTER ROLE db_datareader ADD MEMBER [${APP_NAME}]; | ||
| ALTER ROLE db_datawriter ADD MEMBER [${APP_NAME}]; | ||
| ALTER ROLE db_ddladmin ADD MEMBER [${APP_NAME}]; | ||
| SQL | ||
|
|
||
| sqlcmd -S "$fqdn" -d "$SQL_DB" --access-token "$token" -i /tmp/grant.sql | ||
|
|
||
| # Clean up firewall rule. | ||
| az sql server firewall-rule delete \ | ||
| --resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \ | ||
| --server "$SQL_SERVER" \ | ||
| --name "gh-runner-${{ github.run_id }}" >/dev/null |
There was a problem hiding this comment.
The SQL firewall rule is created and deleted inline, but if sqlcmd (or any earlier command) fails, the cleanup step won’t run and the runner IP rule will be left behind. Add a trap (or finally-style cleanup) to ensure the firewall rule is removed on all exit paths.
| az deployment group what-if \ | ||
| --resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \ | ||
| --template-file infra/main.bicep \ | ||
| --parameters infra/main.dev.bicepparam \ | ||
| --no-pretty-print \ | ||
| > whatif.txt || true | ||
| { | ||
| echo '### Bicep what-if (dev)'; | ||
| echo '```'; | ||
| cat whatif.txt; | ||
| echo '```'; | ||
| } >> "$GITHUB_STEP_SUMMARY" |
There was a problem hiding this comment.
The what-if command is suffixed with || true, so an auth/configuration failure in az deployment group what-if won’t fail the workflow and the step summary will likely be empty (errors go to stderr). For infra validation, it’s better to fail the job on what-if errors (or at least capture stderr and surface it explicitly).
| az deployment group what-if \ | |
| --resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \ | |
| --template-file infra/main.bicep \ | |
| --parameters infra/main.dev.bicepparam \ | |
| --no-pretty-print \ | |
| > whatif.txt || true | |
| { | |
| echo '### Bicep what-if (dev)'; | |
| echo '```'; | |
| cat whatif.txt; | |
| echo '```'; | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| set +e | |
| az deployment group what-if \ | |
| --resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \ | |
| --template-file infra/main.bicep \ | |
| --parameters infra/main.dev.bicepparam \ | |
| --no-pretty-print \ | |
| > whatif.txt 2>&1 | |
| whatif_exit_code=$? | |
| set -e | |
| { | |
| echo '### Bicep what-if (dev)'; | |
| echo '```'; | |
| cat whatif.txt; | |
| echo '```'; | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$whatif_exit_code" -ne 0 ]; then | |
| exit "$whatif_exit_code" | |
| fi |
| nullable: false, | ||
| defaultValue: ""); |
There was a problem hiding this comment.
OwnerId is added as non-nullable with a default empty string, and later a foreign key is added to AspNetUsers(Id). If the Notes table already contains rows, those existing rows will have OwnerId = '' and the FK addition will fail. Consider a two-step migration (nullable column + backfill + make non-null), or explicitly clean/assign existing rows before adding the FK constraint.
| nullable: false, | |
| defaultValue: ""); | |
| nullable: true); |
| export async function request<T>(url: string, init?: RequestInit): Promise<T> { | ||
| const headers = new Headers(init?.headers) | ||
| if (!headers.has('Content-Type')) { | ||
| headers.set('Content-Type', 'application/json') | ||
| } | ||
| const token = getToken() | ||
| if (token && !headers.has('Authorization')) { | ||
| headers.set('Authorization', `Bearer ${token}`) | ||
| } | ||
|
|
||
| const response = await fetch(url, { ...init, headers }) | ||
|
|
There was a problem hiding this comment.
request() uses fetch(url, ...) with whatever relative path the caller passes. In deployed SWA (unlinked) the frontend needs to call the App Service hostname, so relative /api/... requests will hit the SWA origin and fail. Prefix requests with import.meta.env.VITE_API_BASE_URL (while still allowing absolute URLs) before calling fetch.
| resource server 'Microsoft.Sql/servers@2023-08-01-preview' = { | ||
| name: serverName | ||
| location: location | ||
| tags: tags | ||
| identity: { | ||
| type: 'SystemAssigned' | ||
| } | ||
| properties: { | ||
| // Entra-only authentication. No SQL auth, no passwords. | ||
| administrators: { | ||
| administratorType: 'ActiveDirectory' | ||
| azureADOnlyAuthentication: true | ||
| login: adminPrincipalName | ||
| sid: adminPrincipalId | ||
| principalType: adminPrincipalType | ||
| tenantId: tenant().tenantId | ||
| } | ||
| minimalTlsVersion: '1.2' | ||
| publicNetworkAccess: 'Enabled' | ||
| restrictOutboundNetworkAccess: 'Disabled' | ||
| version: '12.0' | ||
| } | ||
| } | ||
|
|
||
| // Allow Azure services (App Service outbound IPs) to reach this server. | ||
| resource allowAzure 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = { | ||
| name: 'AllowAllWindowsAzureIps' | ||
| parent: server | ||
| properties: { | ||
| startIpAddress: '0.0.0.0' | ||
| endIpAddress: '0.0.0.0' | ||
| } | ||
| } | ||
|
|
||
| resource database 'Microsoft.Sql/servers/databases@2023-08-01-preview' = { | ||
| name: databaseName |
There was a problem hiding this comment.
This module uses the Microsoft.Sql/*@2023-08-01-preview API versions. Preview API versions can introduce breaking schema changes and are risky for long-lived infra (especially prod). Prefer a stable (non--preview) API version for servers, firewallRules, and databases if one is available.
| "github/*", | ||
| "github/*", | ||
| todo, |
There was a problem hiding this comment.
The frontmatter tools: value is written as a flow sequence with a trailing comma (e.g., todo, before ]). Many YAML parsers reject trailing commas, which would make this agent definition invalid. Remove trailing commas (and the duplicated "github/*" entries) to ensure the frontmatter parses reliably.
| "github/*", | |
| "github/*", | |
| todo, | |
| todo |
| com.microsoft/azure/search, | ||
| "playwright/*", | ||
| "azure-mcp/*", | ||
| todo, |
There was a problem hiding this comment.
The frontmatter tools: list uses a bracketed flow sequence with a trailing comma (todo, before the closing ]). If the agent config loader uses a strict YAML parser, this can fail to parse. Remove trailing commas in the flow sequence to avoid breaking agent loading.
| todo, | |
| todo |
|
|
||
| app.UseHttpsRedirection(); | ||
| app.UseCors("Frontend"); | ||
| app.UseAuthentication(); | ||
| app.UseAuthorization(); |
There was a problem hiding this comment.
The pipeline/infra expects migrations to run in App Service (it sets RUN_MIGRATIONS_ON_START=true while ASPNETCORE_ENVIRONMENT=Production), but this file currently runs db.Database.Migrate() only under IsDevelopment(). That means deployed environments won’t migrate and can fail at runtime. Consider gating migrations on RUN_MIGRATIONS_ON_START (or moving migration execution into the deploy workflow).
This pull request introduces a comprehensive CI/CD pipeline for the project, including new GitHub Actions workflows for continuous integration, deployment, and infrastructure validation. It also adds thorough authentication endpoint tests for the backend API and improves test infrastructure setup. Additionally, the agent definitions for coding and DevOps tasks are enhanced for better tool access and usability.
CI/CD Pipeline and Workflow Automation
ci.yml) that builds and tests both backend and frontend code, and validates Bicep infrastructure templates._deploy.yml) to automate environment-specific deployments, including infrastructure provisioning, secrets management, SQL access configuration, and separate backend/frontend deployments.cd.yml) that builds artifacts and triggers deployments to development and production environments using the reusable deployment workflow.infra-validate.yml) that runs Bicep "what-if" analysis on pull requests affecting infrastructure, summarizing changes directly in the workflow output.Backend Testing Improvements
AuthEndpointsTests.cs, covering registration, login, duplicate handling, input validation, and authenticated user retrieval.NotesApiFactory.cs) to support authenticated test clients and properly configure JWT settings for testing scenarios. [1] [2]Agent Configuration Enhancements