-
Notifications
You must be signed in to change notification settings - Fork 8
297 lines (259 loc) · 10.9 KB
/
Copy pathBuild-Test-And-Deploy.yml
File metadata and controls
297 lines (259 loc) · 10.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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
name: Build, Test, and Deploy EssentialCSharp.Web
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
environment: "BuildAndUploadImage"
steps:
- uses: actions/checkout@v6
- name: Set up .NET Core
uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
source-url: https://pkgs.dev.azure.com/intelliTect/_packaging/EssentialCSharp/nuget/v3/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_PAT }}
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: EssentialCSharp.Web/package-lock.json
- name: Set up dependency caching for faster builds
uses: actions/cache@v5
id: nuget-cache
with:
path: |
~/.nuget/packages
${{ github.workspace }}/**/obj/project.assets.json
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
${{ runner.os }}-nuget-
- name: Restore with dotnet
run: dotnet restore
- name: Build with dotnet
run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True --configuration Release --no-restore
- name: Expose GitHub Actions Runtime
uses: actions/github-script@v9
with:
script: |
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']);
core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']);
- name: Run .NET Tests
run: dotnet test --no-build --configuration Release
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Write IndexNow key verification file
if: env.INDEXNOW_API_KEY != ''
env:
INDEXNOW_API_KEY: ${{ secrets.INDEXNOW_API_KEY }}
run: printf '%s' "$INDEXNOW_API_KEY" > "EssentialCSharp.Web/wwwroot/$INDEXNOW_API_KEY.txt"
# Only build for dev registry — prod gets the image via az acr import in deploy-production
- name: Build Container Image
if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request'
uses: docker/build-push-action@v7
with:
tags: ${{ vars.DEVCONTAINER_REGISTRY }}/essentialcsharpweb:${{ github.sha }},${{ vars.DEVCONTAINER_REGISTRY }}/essentialcsharpweb:latest
file: ./EssentialCSharp.Web/Dockerfile
context: .
secrets: |
"nuget_pat=${{ secrets.AZURE_DEVOPS_PAT }}"
outputs: type=docker,dest=${{ github.workspace }}/essentialcsharpwebimage.tar
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: essentialcsharpwebimage
path: ${{ github.workspace }}/essentialcsharpwebimage.tar
deploy-development:
if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: build-and-test
concurrency:
group: deploy-development
cancel-in-progress: false
environment:
name: "Development"
permissions:
id-token: write
contents: read
steps:
- name: Azure Login
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Download artifact
uses: actions/download-artifact@v8
with:
name: essentialcsharpwebimage
path: ${{ github.workspace }}
- name: Load image
run: |
docker load --input ${{ github.workspace }}/essentialcsharpwebimage.tar
docker image ls -a
- name: Log in to container registry
run: |
REGISTRY="${{ vars.DEVCONTAINER_REGISTRY }}"
az acr login --name "${REGISTRY%.azurecr.io}"
- name: Push Image to Dev Container Registry
run: docker push --all-tags ${{ vars.DEVCONTAINER_REGISTRY }}/essentialcsharpweb
- name: Deploy to Container App
env:
CONTAINER_APP_NAME: ${{ vars.CONTAINER_APP_NAME }}
RESOURCEGROUP: ${{ vars.RESOURCEGROUP }}
run: |
az extension add --name containerapp --upgrade --only-show-errors
az containerapp update \
--name "$CONTAINER_APP_NAME" \
--resource-group "$RESOURCEGROUP" \
--image "${{ vars.DEVCONTAINER_REGISTRY }}/essentialcsharpweb:${{ github.sha }}"
- name: Logout of Azure CLI
if: always()
uses: azure/CLI@v3
with:
inlineScript: |
az logout
az cache purge
az account clear
deploy-production:
if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [deploy-development]
concurrency:
group: deploy-production
cancel-in-progress: false
environment:
name: "Production"
permissions:
id-token: write
contents: write # needed for git deploy tag
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Azure Login
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Server-side copy from dev ACR to prod ACR — no artifact download needed.
# PREREQUISITE: prod OIDC identity must have AcrPull on the dev ACR (Terraform RBAC).
- name: Import image from dev ACR to prod ACR
id: import
run: |
DEV_ACR="${{ vars.DEVCONTAINER_REGISTRY }}"
PROD_ACR="${{ vars.PRODCONTAINER_REGISTRY }}"
az acr import \
--name "${PROD_ACR%.azurecr.io}" \
--source "${DEV_ACR}/essentialcsharpweb:${{ github.sha }}" \
--image "essentialcsharpweb:${{ github.sha }}" \
--image "essentialcsharpweb:latest" \
--force
DIGEST=$(az acr repository show \
--name "${PROD_ACR%.azurecr.io}" \
--image "essentialcsharpweb:${{ github.sha }}" \
--query "digest" -o tsv)
if [ -z "$DIGEST" ]; then
echo "::error::Failed to capture image digest from prod ACR after import"
exit 1
fi
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
- name: Deploy to Container App
env:
CONTAINER_APP_NAME: ${{ vars.CONTAINER_APP_NAME }}
RESOURCEGROUP: ${{ vars.RESOURCEGROUP }}
run: |
az extension add --name containerapp --upgrade --only-show-errors
az containerapp update \
--name "$CONTAINER_APP_NAME" \
--resource-group "$RESOURCEGROUP" \
--image "${{ vars.PRODCONTAINER_REGISTRY }}/essentialcsharpweb@${{ steps.import.outputs.digest }}"
- name: Verify deployed image
env:
CONTAINER_APP_NAME: ${{ vars.CONTAINER_APP_NAME }}
RESOURCEGROUP: ${{ vars.RESOURCEGROUP }}
run: |
DEPLOYED=$(az containerapp show \
--name "$CONTAINER_APP_NAME" \
--resource-group "$RESOURCEGROUP" \
--query "properties.template.containers[0].image" -o tsv)
EXPECTED="${{ vars.PRODCONTAINER_REGISTRY }}/essentialcsharpweb@${{ steps.import.outputs.digest }}"
if [ "$DEPLOYED" != "$EXPECTED" ]; then
echo "::error::Image mismatch! Expected $EXPECTED but found $DEPLOYED"
exit 1
fi
echo "Deployed image verified: $DEPLOYED"
- name: Smoke test
env:
CONTAINER_APP_NAME: ${{ vars.CONTAINER_APP_NAME }}
RESOURCEGROUP: ${{ vars.RESOURCEGROUP }}
run: |
FQDN=$(az containerapp show \
--name "$CONTAINER_APP_NAME" \
--resource-group "$RESOURCEGROUP" \
--query "properties.configuration.ingress.fqdn" -o tsv)
# --retry-all-errors ensures HTTP 5xx (cold-start 503s) also trigger retries
curl --fail --retry 10 --retry-delay 15 --retry-all-errors --max-time 30 "https://$FQDN/health"
- name: Tag commit as deployed
run: |
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
# -f allows re-tagging the same SHA on workflow re-runs
git tag -f "deployed/prod/${{ github.sha }}"
git push origin "deployed/prod/${{ github.sha }}" --force
- name: Notify IndexNow of Sitemap Update
continue-on-error: true
env:
INDEXNOW_API_KEY: ${{ secrets.INDEXNOW_API_KEY }}
run: |
# IndexNow Protocol: Notifies search engines (Bing, Yandex, Naver) of content updates.
# See: https://www.indexnow.org/documentation
#
# Domain ownership is verified via a key file hosted at:
# https://essentialcsharp.com/{key}.txt (served from wwwroot/{key}.txt)
# The file must contain exactly the key value. Without it, submissions return HTTP 403.
#
# We fetch the live sitemap (app is confirmed up after smoke test) and extract all <loc>
# URLs to submit as actual content URLs per the IndexNow spec.
# Fetch the live sitemap and extract all <loc> URLs into a newline-delimited list
SITEMAP=$(curl -sf "https://essentialcsharp.com/sitemap.xml")
if [ -z "$SITEMAP" ]; then
echo "::warning::Could not fetch sitemap; skipping IndexNow submission."
exit 0
fi
# Build a JSON array from <loc>...</loc> entries
URL_ARRAY=$(echo "$SITEMAP" \
| grep -oP '(?<=<loc>)[^<]+' \
| jq -Rn '[inputs]')
URL_COUNT=$(echo "$URL_ARRAY" | jq 'length')
echo "Submitting $URL_COUNT URLs to IndexNow..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST https://api.indexnow.org/indexnow \
-H "Content-Type: application/json; charset=utf-8" \
-d "{
\"host\": \"essentialcsharp.com\",
\"key\": \"$INDEXNOW_API_KEY\",
\"urlList\": $URL_ARRAY
}")
echo "IndexNow response: HTTP $RESPONSE"
if [ "$RESPONSE" != "200" ] && [ "$RESPONSE" != "202" ]; then
echo "::warning::IndexNow submission returned HTTP $RESPONSE (200/202 = success, 202 = key validation pending)."
fi
- name: Logout of Azure CLI
if: always()
uses: azure/CLI@v3
with:
inlineScript: |
az logout
az cache purge
az account clear