-
Notifications
You must be signed in to change notification settings - Fork 0
250 lines (234 loc) · 10.3 KB
/
teardown.yml
File metadata and controls
250 lines (234 loc) · 10.3 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
# ---------------------------------------------------------------------------
# Tear down the Evidence Portal workshop environment.
#
# This workflow is the destructive counterpart to deploy.yml. It is
# manual-only (workflow_dispatch) and requires the operator to type
# "DESTROY" into the confirmation input before anything is deleted.
#
# What it does
# ------------
# 1. Validates the typed confirmation.
# 2. Logs into Azure via OIDC (same federated identity used by deploy.yml).
# 3. Deletes the Azure resource group (App Service Plan, App Services,
# Storage account, Private Endpoint, Private DNS Zone, VNet,
# Application Insights, Log Analytics workspace, role assignments).
# 4. (Optional) Deletes the API + SPA Entra ID app registrations using
# the IDs stored in .entra-apps.json. Off by default because the
# registrations live in a tenant that may also host other workloads.
#
# What it does NOT do
# -------------------
# * Touch the multi-tenant `Evidence Portal Multi-Tenant SPA` registration
# or its service principals in foreign tenants.
# * Delete .entra-apps.json from the repo (clean up locally if needed).
# * Remove the federated identity credential or RBAC assignments granted
# to the GitHub OIDC service principal itself.
#
# Required GitHub repository secrets (same as deploy.yml):
#
# AZURE_CLIENT_ID OIDC SP with Owner (or Contributor + User Access
# Administrator) on the resource group, plus
# Application.ReadWrite.OwnedBy on Microsoft Graph
# if `delete_entra_apps=true` is used.
# AZURE_TENANT_ID Entra tenant id hosting the app registrations.
# AZURE_SUBSCRIPTION_ID Subscription hosting the resource group.
# ---------------------------------------------------------------------------
name: Teardown workshop
on:
workflow_dispatch:
inputs:
confirm:
description: 'Type DESTROY to confirm. Anything else aborts.'
required: true
type: string
resource_group:
description: 'Azure resource group to delete.'
required: true
type: string
default: 'rg-evidence-workshop'
delete_entra_apps:
description: 'Also delete the API + SPA Entra app registrations from .entra-apps.json.'
required: true
type: boolean
default: false
permissions:
id-token: write
contents: read
# Serialize teardown against itself; never run while a deploy is in flight.
concurrency:
group: teardown-workshop
cancel-in-progress: false
env:
CONFIRM_PHRASE: 'DESTROY'
jobs:
# -------------------------------------------------------------------------
# Fail fast if the operator did not type the confirmation phrase exactly.
# -------------------------------------------------------------------------
guard:
name: Confirm intent
runs-on: ubuntu-latest
steps:
- name: Validate confirmation
run: |
set -euo pipefail
if [ "${{ inputs.confirm }}" != "${CONFIRM_PHRASE}" ]; then
echo "::error::Confirmation phrase did not match. Expected '${CONFIRM_PHRASE}', got '${{ inputs.confirm }}'. Aborting."
exit 1
fi
echo "Confirmation accepted. Will delete resource group '${{ inputs.resource_group }}'."
if [ "${{ inputs.delete_entra_apps }}" = "true" ]; then
echo "Will ALSO delete API + SPA Entra app registrations from .entra-apps.json."
else
echo "Entra app registrations will be PRESERVED."
fi
# -------------------------------------------------------------------------
# Delete the Azure resource group. This is the bulk of the teardown:
# everything provisioned by infra/main.bicep is contained in one group.
# -------------------------------------------------------------------------
delete-resource-group:
name: Delete resource group
needs: guard
runs-on: ubuntu-latest
# To require human approval before destructive runs, create a GitHub
# environment (e.g. `workshop-teardown`) with required reviewers and
# add `environment: workshop-teardown` here.
steps:
- 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 }}
- name: Show what will be deleted
run: |
set -euo pipefail
rg='${{ inputs.resource_group }}'
if ! az group show --name "$rg" -o none 2>/dev/null; then
echo "Resource group '$rg' does not exist. Nothing to do."
exit 0
fi
echo "Resources currently in '$rg':"
az resource list --resource-group "$rg" \
--query "[].{name:name, type:type, location:location}" -o table
- name: Delete resource group (async + poll)
run: |
set -euo pipefail
rg='${{ inputs.resource_group }}'
if ! az group show --name "$rg" -o none 2>/dev/null; then
echo "Resource group '$rg' already gone."
exit 0
fi
# --no-wait avoids polling /subscriptions/<sub>/operationresults/...,
# which would require subscription-scoped read permissions. With
# only RG-scoped Contributor we instead poll `az group exists`,
# which is an RG-scoped read.
echo "Issuing async delete of resource group '$rg'..."
az group delete --name "$rg" --yes --no-wait
echo "Polling for completion (timeout 30 min)..."
for i in $(seq 1 60); do
if [ "$(az group exists --name "$rg")" = "false" ]; then
echo "Resource group '$rg' deleted after ~$((i * 30))s."
exit 0
fi
echo " [$i/60] still present, sleeping 30s..."
sleep 30
done
echo "::error::Timed out waiting for resource group '$rg' to delete."
exit 1
- name: Verify deletion
run: |
set -euo pipefail
rg='${{ inputs.resource_group }}'
if az group show --name "$rg" -o none 2>/dev/null; then
echo "::error::Resource group '$rg' still exists after delete."
exit 1
fi
echo "Verified: resource group '$rg' no longer exists."
# -------------------------------------------------------------------------
# (Optional) Delete the API + SPA Entra app registrations created by
# scripts/setup-entra-apps.ps1. Driven by .entra-apps.json so the IDs are
# always current. Skipped unless the operator opts in.
# -------------------------------------------------------------------------
delete-entra-apps:
name: Delete Entra app registrations
needs: delete-resource-group
if: ${{ inputs.delete_entra_apps == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Read app registration IDs from .entra-apps.json
id: ids
run: |
set -euo pipefail
if [ ! -f .entra-apps.json ]; then
echo "::warning::.entra-apps.json not found. Nothing to delete."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
api_app_id=$(jq -r '.apiAppId // empty' .entra-apps.json)
spa_app_id=$(jq -r '.spaAppId // empty' .entra-apps.json)
echo "apiAppId=$api_app_id"
echo "spaAppId=$spa_app_id"
echo "api_app_id=$api_app_id" >> "$GITHUB_OUTPUT"
echo "spa_app_id=$spa_app_id" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Azure login (OIDC, tenant-only for Graph)
if: ${{ steps.ids.outputs.skip == 'false' }}
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
# Graph (`az ad app delete`) does not need a subscription. Passing
# subscription-id together with allow-no-subscriptions still makes
# the action attempt `az account set`, which fails with "No
# subscriptions found" if the OIDC SP has no role on that sub.
allow-no-subscriptions: true
- name: Delete API app registration
if: ${{ steps.ids.outputs.skip == 'false' && steps.ids.outputs.api_app_id != '' }}
run: |
set -euo pipefail
app_id='${{ steps.ids.outputs.api_app_id }}'
if ! az ad app show --id "$app_id" -o none 2>/dev/null; then
echo "API app registration $app_id not found (already deleted?). Skipping."
exit 0
fi
echo "Deleting API app registration $app_id..."
az ad app delete --id "$app_id"
echo "Deleted."
- name: Delete SPA app registration
if: ${{ steps.ids.outputs.skip == 'false' && steps.ids.outputs.spa_app_id != '' }}
run: |
set -euo pipefail
app_id='${{ steps.ids.outputs.spa_app_id }}'
if ! az ad app show --id "$app_id" -o none 2>/dev/null; then
echo "SPA app registration $app_id not found (already deleted?). Skipping."
exit 0
fi
echo "Deleting SPA app registration $app_id..."
az ad app delete --id "$app_id"
echo "Deleted."
- name: Reminder to clean up local state
if: ${{ steps.ids.outputs.skip == 'false' }}
run: |
echo "Entra app registrations deleted. Remember to remove or update .entra-apps.json"
echo "in your local checkout if you plan to re-bootstrap with different IDs."
# -------------------------------------------------------------------------
# Summary banner so the run page shows a clear outcome at a glance.
# -------------------------------------------------------------------------
summary:
name: Summary
needs: [delete-resource-group, delete-entra-apps]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Print summary
run: |
{
echo "## Teardown summary"
echo ""
echo "| Step | Result |"
echo "| --- | --- |"
echo "| Resource group \`${{ inputs.resource_group }}\` | ${{ needs.delete-resource-group.result }} |"
echo "| Entra app registrations | ${{ needs.delete-entra-apps.result }} |"
} >> "$GITHUB_STEP_SUMMARY"