Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/agents/coder.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
description: "Use when: writing, refactoring, or reviewing application code and tests — especially .NET Web APIs, React frontends, Entity Framework Core data access, unit/integration tests, or Playwright end-to-end tests. Produces simple, maintainable, idiomatic code that follows SOLID and Clean Code principles. Trigger phrases: clean code, idiomatic, SOLID, refactor, .NET API, ASP.NET Core, React component, EF Core, DbContext, write tests, unit test, integration test, e2e test, Playwright, maintainable code."
name: "Clean Coder"
tools:
[vscode, execute, read, edit, search, "playwright/*", azure-mcp/search, todo]
[
vscode,
execute,
read,
edit,
search,
com.microsoft/azure/search,
"playwright/*",
"azure-mcp/*",
todo,

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
todo,
todo

Copilot uses AI. Check for mistakes.
]
user-invocable: false
model: Claude Opus 4.7 (copilot)
---
Expand Down
18 changes: 16 additions & 2 deletions .github/agents/devops.agent.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
---
description: "Use when: authoring or reviewing Azure Infrastructure as Code (Bicep), GitHub Actions CI/CD pipelines, or Git commit messages and PR titles. Handles IaC creation, pipelines as code, and commit/PR conventions. Trigger phrases: bicep, iac, infra, azure resources, main.bicep, bicepparam, AVM, azd, github actions, workflow, CI, CD, pipeline, deploy, OIDC, reusable workflow, commit message, conventional commit, PR title."
name: "DevOps"
tools: [read, edit, search, execute, todo, azure/*, bicep/*, github/*]
user-invocable: false
tools:
[
vscode/askQuestions,
execute,
read,
edit,
search,
"github/*",
"azure-mcp/*",
"bicep/*",
"com.microsoft/azure/*",
"github/*",
"github/*",
todo,
Comment on lines +15 to +17

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"github/*",
"github/*",
todo,
todo

Copilot uses AI. Check for mistakes.
]
user-invocable: true
---

You are a DevOps specialist. You write and review three things:
Expand Down
170 changes: 170 additions & 0 deletions .github/workflows/_deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Deploy (reusable)

on:
workflow_call:
inputs:
environment:
required: true
type: string
bicepParamFile:
required: true
type: string

permissions:
id-token: write
contents: read

jobs:
deploy:
name: ${{ inputs.environment }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
env:
SQL_ADMIN_OBJECT_ID: ${{ vars.SQL_ADMIN_OBJECT_ID }}
SQL_ADMIN_PRINCIPAL_NAME: ${{ vars.SQL_ADMIN_PRINCIPAL_NAME }}
AZURE_DEPLOYER_OBJECT_ID: ${{ vars.AZURE_DEPLOYER_OBJECT_ID }}
steps:
- uses: actions/checkout@v4

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

# ---------- Infra ----------
- name: Ensure resource group
run: |
az group create \
--name "${{ vars.AZURE_RESOURCE_GROUP }}" \
--location "${{ vars.AZURE_LOCATION }}" \
--tags workload=notes environment=${{ inputs.environment }} managed-by=bicep >/dev/null

- name: Deploy Bicep
id: bicep
run: |
outputs=$(az deployment group create \
--resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \
--template-file infra/main.bicep \
--parameters "${{ inputs.bicepParamFile }}" \
--query properties.outputs \
-o json)
echo "$outputs" | jq -r 'to_entries[] | "\(.key)=\(.value.value)"' >> "$GITHUB_OUTPUT"

# ---------- Secrets ----------
- name: Seed JWT signing key (if missing)
env:
KV_NAME: ${{ steps.bicep.outputs.keyVaultName }}
run: |
if ! az keyvault secret show --vault-name "$KV_NAME" --name jwt-signing-key >/dev/null 2>&1; then
echo "Creating jwt-signing-key"
value=$(openssl rand -base64 64 | tr -d '\n')
az keyvault secret set --vault-name "$KV_NAME" --name jwt-signing-key --value "$value" --only-show-errors >/dev/null
else
echo "jwt-signing-key already exists"
fi

# ---------- SQL: grant App Service MI access ----------
- name: Grant App Service MI access to SQL DB
env:
SQL_SERVER: ${{ steps.bicep.outputs.sqlServerName }}
SQL_DB: ${{ steps.bicep.outputs.sqlDatabaseName }}
APP_NAME: ${{ steps.bicep.outputs.appServiceName }}
APP_MI_ID: ${{ steps.bicep.outputs.appServicePrincipalId }}
run: |
set -euo pipefail
fqdn="${SQL_SERVER}.database.windows.net"

# 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
Comment on lines +78 to +111

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

# ---------- Backend ----------
- uses: actions/download-artifact@v4
with:
name: api
path: .

- name: Deploy API
uses: azure/webapps-deploy@v3
with:
app-name: ${{ steps.bicep.outputs.appServiceName }}
package: api.zip

# ---------- Frontend ----------
- uses: actions/download-artifact@v4
with:
name: frontend-src
path: frontend-src

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Build frontend with environment API URL
working-directory: frontend-src
env:
VITE_API_BASE_URL: https://${{ steps.bicep.outputs.appServiceDefaultHostName }}
run: |
npm ci
npm run build

- name: Get SWA deployment token
id: swa
run: |
token=$(az staticwebapp secrets list \
--name "${{ steps.bicep.outputs.staticWebAppName }}" \
--resource-group "${{ vars.AZURE_RESOURCE_GROUP }}" \
--query properties.apiKey -o tsv)
echo "::add-mask::$token"
echo "token=$token" >> "$GITHUB_OUTPUT"

- name: Deploy SWA
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ steps.swa.outputs.token }}
action: upload
app_location: frontend-src/dist
skip_app_build: true
skip_api_build: true

- name: Summary
run: |
{
echo "## Deployment (${{ inputs.environment }})";
echo "- API: https://${{ steps.bicep.outputs.appServiceDefaultHostName }}";
echo "- SWA: https://${{ steps.bicep.outputs.staticWebAppDefaultHostName }}";
echo "- SQL: ${{ steps.bicep.outputs.sqlServerName }} / ${{ steps.bicep.outputs.sqlDatabaseName }}";
echo "- KeyVault: ${{ steps.bicep.outputs.keyVaultName }}";
} >> "$GITHUB_STEP_SUMMARY"
95 changes: 95 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: CD

on:
push:
branches: [ main ]
workflow_dispatch:
inputs:
environment:
description: Target environment
required: true
default: dev
type: choice
options: [ dev, prod ]

permissions:
id-token: write
contents: read

concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: false

jobs:
build-backend:
name: Build backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.x"
- name: Publish
run: |
dotnet publish backend/Notes.Api/Notes.Api.csproj \
--configuration Release \
--runtime linux-x64 \
--self-contained false \
--output ./publish
- name: Zip
run: |
cd publish
zip -r ../api.zip .
- uses: actions/upload-artifact@v4
with:
name: api
path: api.zip
retention-days: 7

build-frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
working-directory: frontend
- name: Build (base URL set at deploy time)
run: npm run build
working-directory: frontend
env:
# The API base URL is stamped in per-environment during deploy by
# rebuilding there. This build is for CI sanity only.
VITE_API_BASE_URL: https://placeholder.invalid
- uses: actions/upload-artifact@v4
with:
name: frontend-src
path: |
frontend
!frontend/node_modules
!frontend/dist
retention-days: 1

deploy-dev:
name: Deploy dev
needs: [ build-backend, build-frontend ]
if: github.event_name == 'push' || github.event.inputs.environment == 'dev'
uses: ./.github/workflows/_deploy.yml
with:
environment: dev
bicepParamFile: infra/main.dev.bicepparam
secrets: inherit

deploy-prod:
name: Deploy prod
needs: [ deploy-dev ]
if: github.event_name == 'push' || github.event.inputs.environment == 'prod'
uses: ./.github/workflows/_deploy.yml
with:
environment: prod
bicepParamFile: infra/main.prod.bicepparam
secrets: inherit
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: CI

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
name: Backend build & test
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.x"

- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('backend/**/*.csproj') }}
restore-keys: ${{ runner.os }}-nuget-

- run: dotnet restore
- run: dotnet build --no-restore --configuration Release
- run: dotnet test --no-build --configuration Release --logger
"trx;LogFileName=test-results.trx"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-test-results
path: backend/**/TestResults/*.trx
if-no-files-found: ignore

frontend:
name: Frontend build & test
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json

- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --run
- run: npm run build

bicep:
name: Bicep lint & build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build main.bicep
run: az bicep build --file infra/main.bicep
Loading
Loading