| Layer | Technology |
|---|---|
| API + Hosting | .NET 10 (ASP.NET Core), Linux App Service (B1/S1) |
| Frontend | Blazor WASM hosted inside same App Service |
| IaC | Azure Developer CLI (azd) + Bicep |
| CI/CD | GitHub Actions → azd up |
| Auth | OIDC federated credentials (no stored secret/PAT) |
| Local dev | Docker Compose (Azurite) + dotnet run |
All infrastructure is defined in infra/resources.bicep and infra/main.bicep.
| Resource | SKU | Notes |
|---|---|---|
| App Service (Linux) | B1 / configurable | Same RG as storage |
| App Service Plan | asp-poshared-linux | Shared — lives in PoShared RG |
| Azure Storage Account | Standard LRS | Existing resource; allowBlobPublicAccess: false |
| Azure Application Insights | Workspace-based | Shared — lives in PoShared RG |
| Log Analytics Workspace | Per GB pay-as-you-go | Shared — lives in PoShared RG |
| Azure Key Vault | Standard | Shared — lives in PoShared RG |
- Runtime:
DOTNETCORE|10.0 - HTTPS only:
true, minimum TLS 1.2 - Managed Identity:
SystemAssigned - Health probe:
GET /health
| Secret Name | Description |
|---|---|
GitHub--ClientId |
GitHub OAuth App client ID |
GitHub--ClientSecret |
GitHub OAuth App client secret |
GitHub--PAT |
Personal Access Token for cloning private repos |
Secrets are loaded at startup via PrefixKeyVaultSecretManager using DefaultAzureCredential (Managed Identity in production, developer account locally).
# Install Azure Developer CLI
winget install Microsoft.Azd
# Install Azure CLI
winget install Microsoft.AzureCLI
# Login
az login
azd auth logincd c:\path\to\PoRepoLineTracker
# Initialise environment (once)
azd env new <env-name> # e.g. "prod" or "dev"
azd env set AZURE_LOCATION eastus
azd env set AZURE_SUBSCRIPTION_ID <your-sub-id>
# Provision infrastructure + deploy application
azd upazd up runs azd provision (Bicep) then azd deploy (dotnet publish → zip deploy).
# Replace <kv-name> with the Key Vault name from azd output
az keyvault secret set --vault-name <kv-name> --name "GitHub--ClientId" --value "<github-oauth-client-id>"
az keyvault secret set --vault-name <kv-name> --name "GitHub--ClientSecret" --value "<github-oauth-client-secret>"
az keyvault secret set --vault-name <kv-name> --name "GitHub--PAT" --value "<github-pat>"- Go to GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Set Authorization callback URL to
https://<your-app-service-url>/signin-github. - Copy the Client ID and generate a Client Secret → store in Key Vault (above).
.github/workflows/azure-dev.yml
- Push to
masterbranch → automatic deploy. - Manual:
workflow_dispatch.
checkout → azd up --no-prompt → done
azd up handles both infrastructure provisioning (idempotent Bicep) and application deployment in a single command.
Variables (not secrets) — set in Repository Settings → Secrets and variables → Actions → Variables tab.
| Variable | Value |
|---|---|
AZURE_CLIENT_ID |
Entra App Registration client ID for OIDC |
AZURE_TENANT_ID |
Azure tenant ID |
AZURE_SUBSCRIPTION_ID |
Azure subscription ID |
AZURE_ENV_NAME |
azd environment name (e.g. prod) |
AZURE_LOCATION |
Azure region (e.g. eastus) |
# Create an App Registration, add federated credentials for GitHub Actions
az ad app create --display-name "PoRepoLineTracker-CICD"
# Then add federated credential for the repo + branch in Azure Portal:
# Azure AD → App Registrations → <app> → Certificates & secrets → Federated credentials
# Issuer: https://token.actions.githubusercontent.com
# Subject: repo:<github-org/repo>:ref:refs/heads/masterNo client secret is stored anywhere — OIDC token exchange is used exclusively.
- .NET 10 SDK
- Docker Desktop (for Azurite)
- Azure CLI (for Key Vault access, optional)
cd c:\path\to\PoRepoLineTracker
docker compose up -d
# Azurite exposes: Blob 10000, Queue 10001, Table 10002dotnet run --project src/PoRepoLineTracker.Api
# App available at: http://localhost:5000
# Health check: http://localhost:5000/healthappsettings.Development.json points AzureWebJobsStorage / Table Storage connection to UseDevelopmentStorage=true (Azurite).
For GitHub OAuth to work in dev, register a separate GitHub OAuth App with callback:
http://localhost:5000/signin-github
When ASPNETCORE_ENVIRONMENT=Development, a test endpoint is available:
GET http://localhost:5000/test-login-redirect?email=<your-email>
This sets an auth cookie and redirects to / without a real GitHub OAuth round-trip.
Requires: Azurite running (user record is written to Table Storage).
dotnet build PoRepoLineTracker.slndotnet test tests/PoRepoLineTracker.UnitTests# Requires Azurite running
docker compose up -d
dotnet test tests/PoRepoLineTracker.IntegrationTestscd tests/PoRepoLineTracker.E2ETests.TS
npm install
npx playwright test
# Reports in: playwright-report/- Serilog sinks: Console + rolling file (
log<date>.txt) + Application Insights Serilog sink. - Log level controlled via
appsettings.json→Serilog:MinimumLevel.
- Exported to Application Insights via
AddAzureMonitorTraceExporter. - Traces include HTTP request spans, MediatR handler spans, and Table Storage calls.
-- All errors in the last 24 hours
exceptions
| where timestamp > ago(24h)
| project timestamp, outerMessage, severityLevel, operation_Name
| order by timestamp desc
-- Request latency p95
requests
| where timestamp > ago(7d)
| summarize percentile(duration, 95) by bin(timestamp, 1h)- Auth cookie: HttpOnly, Secure, SameSite=Lax, 7-day sliding expiration.
- Managed Identity: App Service uses SystemAssigned identity to access Key Vault — no credentials stored in app config.
- HTTPS only: Enforced at App Service level + HSTS in middleware.
- Secrets: Never in source control — all in Key Vault.
- GitHub PAT scope: Minimum required —
repo(read) for private repos, orpublic_repofor public only. - Table Storage:
allowBlobPublicAccess: false; access via connection string from Key Vault.