-
Notifications
You must be signed in to change notification settings - Fork 0
214 lines (184 loc) · 7.9 KB
/
Copy pathci-cd.yml
File metadata and controls
214 lines (184 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# ============================================================================
# TaskFlow CI/CD — build, quality gate, gated DB migration, deploy
# Auth: GitHub OIDC -> Azure (no stored cloud secrets)
# ============================================================================
name: ci-cd
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Least-privilege; id-token needed for OIDC federation to Azure.
permissions:
contents: read
id-token: write
pull-requests: write
env:
DOTNET_VERSION: '10.0.x'
NODE_VERSION: '20'
API_PROJECT: backend/src/Api/Api.csproj
MIGRATIONS_PROJECT: backend/src/Infrastructure/Infrastructure.csproj
STARTUP_PROJECT: backend/src/Api/Api.csproj
FRONTEND_DIR: frontend
# Don't let two runs on the same ref overlap (protects DB migrations).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# --------------------------------------------------------------------------
# 1) Build + test + SonarCloud quality gate. This job MUST pass before deploy.
# --------------------------------------------------------------------------
build-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for accurate Sonar analysis
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: ${{ env.FRONTEND_DIR }}/package-lock.json
- name: Setup Java (required by Sonar scanner)
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install SonarCloud scanner
run: dotnet tool install --global dotnet-sonarscanner
# ---- Backend: Sonar begin -> build -> test (coverage) -> Sonar end ----
- name: Sonar begin
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
dotnet-sonarscanner begin \
/k:"${{ vars.SONAR_PROJECT_KEY }}" \
/o:"${{ vars.SONAR_ORG }}" \
/d:sonar.host.url="https://sonarcloud.io" \
/d:sonar.token="${SONAR_TOKEN}" \
/d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" \
/d:sonar.qualitygate.wait=true
- name: Build (backend)
run: dotnet build backend --configuration Release --no-incremental
- name: Test (backend) with coverage
run: |
dotnet test backend \
--configuration Release \
--collect:"XPlat Code Coverage;Format=opencover" \
--results-directory ./TestResults
- name: Sonar end (blocks on quality gate)
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: dotnet-sonarscanner end /d:sonar.token="${SONAR_TOKEN}"
# ---- Frontend: lint + build ----
- name: Frontend install
working-directory: ${{ env.FRONTEND_DIR }}
run: npm ci
- name: Frontend lint
working-directory: ${{ env.FRONTEND_DIR }}
run: npm run lint
- name: Frontend build
working-directory: ${{ env.FRONTEND_DIR }}
run: npm run build
# ---- Publish artifacts for the deploy job ----
- name: Publish API
run: dotnet publish ${{ env.API_PROJECT }} -c Release -o ./publish/api
- uses: actions/upload-artifact@v4
with:
name: api
path: ./publish/api
- uses: actions/upload-artifact@v4
with:
name: frontend
path: ${{ env.FRONTEND_DIR }}/dist
# --------------------------------------------------------------------------
# 2) Deploy — only on main, only after build-test passes.
# Uses a protected GitHub Environment so it can require manual approval.
# Order: BACKUP -> MIGRATE (expand) -> DEPLOY APP.
# --------------------------------------------------------------------------
deploy:
needs: build-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 25
environment: production # add reviewers in repo settings for an approval gate
steps:
- uses: actions/checkout@v4 # source needed to run EF migrations
- uses: actions/download-artifact@v4
with: { name: api, path: ./publish/api }
- uses: actions/download-artifact@v4
with: { name: frontend, path: ./dist }
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# ---- Open SQL firewall for THIS runner (GitHub runners aren't "Azure services") ----
- name: Open SQL firewall for runner
run: |
RUNNER_IP=$(curl -s https://api.ipify.org)
az sql server firewall-rule create \
-g "${{ vars.AZURE_RG }}" -s "${{ vars.SQL_SERVER_NAME }}" \
-n "ci-${{ github.run_id }}" \
--start-ip-address "$RUNNER_IP" --end-ip-address "$RUNNER_IP"
# ---- BACKUP SAFETY NET: automatic PITR (free, 7-day). No DB copy (would break free tier) ----
- name: Verify PITR restore point
run: |
EARLIEST=$(az sql db show -g "${{ vars.AZURE_RG }}" -s "${{ vars.SQL_SERVER_NAME }}" \
-n "${{ vars.SQL_DB_NAME }}" --query earliestRestoreDate -o tsv 2>/dev/null || echo "")
echo "PITR earliest restore point: ${EARLIEST:-new DB, none yet}"
# ---- MIGRATE (expand-contract) via EF + Entra; uses the OIDC az session ----
- name: Apply EF migration
run: |
dotnet tool install --global dotnet-ef
export PATH="$PATH:$HOME/.dotnet/tools"
CONN="Server=tcp:${{ vars.SQL_SERVER_NAME }}.database.windows.net,1433;Database=${{ vars.SQL_DB_NAME }};Authentication=Active Directory Default;Encrypt=True;"
dotnet ef database update \
--project ${{ env.MIGRATIONS_PROJECT }} \
--startup-project ${{ env.STARTUP_PROJECT }} \
--connection "$CONN"
# ---- Always close the runner firewall hole, even if migration failed ----
- name: Close SQL firewall for runner
if: always()
run: |
az sql server firewall-rule delete \
-g "${{ vars.AZURE_RG }}" -s "${{ vars.SQL_SERVER_NAME }}" \
-n "ci-${{ github.run_id }}" || true
# ---- DEPLOY API (only runs if migration succeeded) ----
- name: Deploy API to App Service
uses: azure/webapps-deploy@v3
with:
app-name: ${{ vars.API_APP_NAME }}
package: ./publish/api
# ---- DEPLOY FRONTEND: fetch SWA token JIT via OIDC (not a stored secret) ----
- name: Get SWA deployment token
id: swa
run: |
TOKEN=$(az staticwebapp secrets list -n "${{ vars.SWA_NAME }}" \
-g "${{ vars.AZURE_RG }}" --query "properties.apiKey" -o tsv)
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
- name: Deploy frontend to Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ steps.swa.outputs.token }}
action: upload
app_location: 'dist'
skip_app_build: true
# ---- SMOKE TEST (longer budget for cold F1 + paused SQL) ----
- name: Smoke test
run: |
for i in {1..20}; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ vars.API_APP_NAME }}.azurewebsites.net/health" || true)
if [ "$code" = "200" ]; then echo "Healthy"; exit 0; fi
echo "Attempt $i: $code — retrying"; sleep 15
done
echo "Health check failed"; exit 1