Skip to content

Commit bde9957

Browse files
committed
feat: add teardown workflow for Evidence Portal workshop environment
1 parent 5325768 commit bde9957

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

.github/workflows/teardown.yml

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# ---------------------------------------------------------------------------
2+
# Tear down the Evidence Portal workshop environment.
3+
#
4+
# This workflow is the destructive counterpart to deploy.yml. It is
5+
# manual-only (workflow_dispatch) and requires the operator to type
6+
# "DESTROY" into the confirmation input before anything is deleted.
7+
#
8+
# What it does
9+
# ------------
10+
# 1. Validates the typed confirmation.
11+
# 2. Logs into Azure via OIDC (same federated identity used by deploy.yml).
12+
# 3. Deletes the Azure resource group (App Service Plan, App Services,
13+
# Storage account, Private Endpoint, Private DNS Zone, VNet,
14+
# Application Insights, Log Analytics workspace, role assignments).
15+
# 4. (Optional) Deletes the API + SPA Entra ID app registrations using
16+
# the IDs stored in .entra-apps.json. Off by default because the
17+
# registrations live in a tenant that may also host other workloads.
18+
#
19+
# What it does NOT do
20+
# -------------------
21+
# * Touch the multi-tenant `Evidence Portal Multi-Tenant SPA` registration
22+
# or its service principals in foreign tenants.
23+
# * Delete .entra-apps.json from the repo (clean up locally if needed).
24+
# * Remove the federated identity credential or RBAC assignments granted
25+
# to the GitHub OIDC service principal itself.
26+
#
27+
# Required GitHub repository secrets (same as deploy.yml):
28+
#
29+
# AZURE_CLIENT_ID OIDC SP with Owner (or Contributor + User Access
30+
# Administrator) on the resource group, plus
31+
# Application.ReadWrite.OwnedBy on Microsoft Graph
32+
# if `delete_entra_apps=true` is used.
33+
# AZURE_TENANT_ID Entra tenant id hosting the app registrations.
34+
# AZURE_SUBSCRIPTION_ID Subscription hosting the resource group.
35+
# ---------------------------------------------------------------------------
36+
name: Teardown workshop
37+
38+
on:
39+
workflow_dispatch:
40+
inputs:
41+
confirm:
42+
description: 'Type DESTROY to confirm. Anything else aborts.'
43+
required: true
44+
type: string
45+
resource_group:
46+
description: 'Azure resource group to delete.'
47+
required: true
48+
type: string
49+
default: 'rg-evidence-workshop'
50+
delete_entra_apps:
51+
description: 'Also delete the API + SPA Entra app registrations from .entra-apps.json.'
52+
required: true
53+
type: boolean
54+
default: false
55+
56+
permissions:
57+
id-token: write
58+
contents: read
59+
60+
# Serialize teardown against itself; never run while a deploy is in flight.
61+
concurrency:
62+
group: teardown-workshop
63+
cancel-in-progress: false
64+
65+
env:
66+
CONFIRM_PHRASE: 'DESTROY'
67+
68+
jobs:
69+
# -------------------------------------------------------------------------
70+
# Fail fast if the operator did not type the confirmation phrase exactly.
71+
# -------------------------------------------------------------------------
72+
guard:
73+
name: Confirm intent
74+
runs-on: ubuntu-latest
75+
steps:
76+
- name: Validate confirmation
77+
run: |
78+
set -euo pipefail
79+
if [ "${{ inputs.confirm }}" != "${CONFIRM_PHRASE}" ]; then
80+
echo "::error::Confirmation phrase did not match. Expected '${CONFIRM_PHRASE}', got '${{ inputs.confirm }}'. Aborting."
81+
exit 1
82+
fi
83+
echo "Confirmation accepted. Will delete resource group '${{ inputs.resource_group }}'."
84+
if [ "${{ inputs.delete_entra_apps }}" = "true" ]; then
85+
echo "Will ALSO delete API + SPA Entra app registrations from .entra-apps.json."
86+
else
87+
echo "Entra app registrations will be PRESERVED."
88+
fi
89+
90+
# -------------------------------------------------------------------------
91+
# Delete the Azure resource group. This is the bulk of the teardown:
92+
# everything provisioned by infra/main.bicep is contained in one group.
93+
# -------------------------------------------------------------------------
94+
delete-resource-group:
95+
name: Delete resource group
96+
needs: guard
97+
runs-on: ubuntu-latest
98+
# To require human approval before destructive runs, create a GitHub
99+
# environment (e.g. `workshop-teardown`) with required reviewers and
100+
# add `environment: workshop-teardown` here.
101+
steps:
102+
- name: Azure login (OIDC)
103+
uses: azure/login@v2
104+
with:
105+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
106+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
107+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
108+
109+
- name: Show what will be deleted
110+
run: |
111+
set -euo pipefail
112+
rg='${{ inputs.resource_group }}'
113+
if ! az group show --name "$rg" -o none 2>/dev/null; then
114+
echo "Resource group '$rg' does not exist. Nothing to do."
115+
exit 0
116+
fi
117+
echo "Resources currently in '$rg':"
118+
az resource list --resource-group "$rg" \
119+
--query "[].{name:name, type:type, location:location}" -o table
120+
121+
- name: Delete resource group (synchronous)
122+
run: |
123+
set -euo pipefail
124+
rg='${{ inputs.resource_group }}'
125+
if ! az group show --name "$rg" -o none 2>/dev/null; then
126+
echo "Resource group '$rg' already gone."
127+
exit 0
128+
fi
129+
echo "Deleting resource group '$rg' (this can take several minutes)..."
130+
az group delete --name "$rg" --yes
131+
echo "Resource group '$rg' deleted."
132+
133+
- name: Verify deletion
134+
run: |
135+
set -euo pipefail
136+
rg='${{ inputs.resource_group }}'
137+
if az group show --name "$rg" -o none 2>/dev/null; then
138+
echo "::error::Resource group '$rg' still exists after delete."
139+
exit 1
140+
fi
141+
echo "Verified: resource group '$rg' no longer exists."
142+
143+
# -------------------------------------------------------------------------
144+
# (Optional) Delete the API + SPA Entra app registrations created by
145+
# scripts/setup-entra-apps.ps1. Driven by .entra-apps.json so the IDs are
146+
# always current. Skipped unless the operator opts in.
147+
# -------------------------------------------------------------------------
148+
delete-entra-apps:
149+
name: Delete Entra app registrations
150+
needs: delete-resource-group
151+
if: ${{ inputs.delete_entra_apps == true }}
152+
runs-on: ubuntu-latest
153+
steps:
154+
- name: Checkout
155+
uses: actions/checkout@v4
156+
157+
- name: Read app registration IDs from .entra-apps.json
158+
id: ids
159+
run: |
160+
set -euo pipefail
161+
if [ ! -f .entra-apps.json ]; then
162+
echo "::warning::.entra-apps.json not found. Nothing to delete."
163+
echo "skip=true" >> "$GITHUB_OUTPUT"
164+
exit 0
165+
fi
166+
api_app_id=$(jq -r '.apiAppId // empty' .entra-apps.json)
167+
spa_app_id=$(jq -r '.spaAppId // empty' .entra-apps.json)
168+
echo "apiAppId=$api_app_id"
169+
echo "spaAppId=$spa_app_id"
170+
echo "api_app_id=$api_app_id" >> "$GITHUB_OUTPUT"
171+
echo "spa_app_id=$spa_app_id" >> "$GITHUB_OUTPUT"
172+
echo "skip=false" >> "$GITHUB_OUTPUT"
173+
174+
- name: Azure login (OIDC)
175+
if: ${{ steps.ids.outputs.skip == 'false' }}
176+
uses: azure/login@v2
177+
with:
178+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
179+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
180+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
181+
allow-no-subscriptions: true
182+
183+
- name: Delete API app registration
184+
if: ${{ steps.ids.outputs.skip == 'false' && steps.ids.outputs.api_app_id != '' }}
185+
run: |
186+
set -euo pipefail
187+
app_id='${{ steps.ids.outputs.api_app_id }}'
188+
if ! az ad app show --id "$app_id" -o none 2>/dev/null; then
189+
echo "API app registration $app_id not found (already deleted?). Skipping."
190+
exit 0
191+
fi
192+
echo "Deleting API app registration $app_id..."
193+
az ad app delete --id "$app_id"
194+
echo "Deleted."
195+
196+
- name: Delete SPA app registration
197+
if: ${{ steps.ids.outputs.skip == 'false' && steps.ids.outputs.spa_app_id != '' }}
198+
run: |
199+
set -euo pipefail
200+
app_id='${{ steps.ids.outputs.spa_app_id }}'
201+
if ! az ad app show --id "$app_id" -o none 2>/dev/null; then
202+
echo "SPA app registration $app_id not found (already deleted?). Skipping."
203+
exit 0
204+
fi
205+
echo "Deleting SPA app registration $app_id..."
206+
az ad app delete --id "$app_id"
207+
echo "Deleted."
208+
209+
- name: Reminder to clean up local state
210+
if: ${{ steps.ids.outputs.skip == 'false' }}
211+
run: |
212+
echo "Entra app registrations deleted. Remember to remove or update .entra-apps.json"
213+
echo "in your local checkout if you plan to re-bootstrap with different IDs."
214+
215+
# -------------------------------------------------------------------------
216+
# Summary banner so the run page shows a clear outcome at a glance.
217+
# -------------------------------------------------------------------------
218+
summary:
219+
name: Summary
220+
needs: [delete-resource-group, delete-entra-apps]
221+
if: ${{ always() }}
222+
runs-on: ubuntu-latest
223+
steps:
224+
- name: Print summary
225+
run: |
226+
{
227+
echo "## Teardown summary"
228+
echo ""
229+
echo "| Step | Result |"
230+
echo "| --- | --- |"
231+
echo "| Resource group \`${{ inputs.resource_group }}\` | ${{ needs.delete-resource-group.result }} |"
232+
echo "| Entra app registrations | ${{ needs.delete-entra-apps.result }} |"
233+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)