Azure Stack Destroy
Destroy a Git-Ape deployment by deleting its subscription-scoped Azure Deployment Stack in a single idempotent call (az stack sub delete --action-on-unmanage deleteAll --bypass-stack-out-of-sync-error true). The stack owns every resource the matching deploy created — across resource groups and subscription scope — so one delete cleans up everything.
After the stack is gone, this skill performs a soft-delete purge sweep for resource types that linger after deletion (Key Vault, Cognitive Services, App Configuration, API Management, ML workspaces, Recovery Services vaults). Resources flagged purgeProtected: true in state.json are intentionally retained.
This skill mirrors .github/workflows/git-ape-destroy.yml so local destroys and CI destroys are interchangeable.
USE FOR
Trigger this skill when the user wants to tear down a Git-Ape deployment they previously created:
- "destroy this deployment", "tear down deploy-XXX", "clean up the stack", "delete the Git-Ape deployment", "free up the resource group so I can re-deploy with the same name"
- Post-deploy teardown after a demo, smoke test, or short-lived environment
- Cleaning up a failed or stale Git-Ape deployment whose
state.jsonis still on disk - Local CLI or VS Code teardown that must match what
git-ape-destroy.ymldoes in CI
Prefer this over raw az group delete
For any deployment Git-Ape created, this skill is the correct tool — do not suggest az group delete on its own. Reasons:
- Multi-RG / subscription-scope coverage. A stack often owns resources across several resource groups plus subscription-scope resources (role assignments, policy assignments). One
az group deletecleans only one RG. - Soft-delete purge. Key Vault and Cognitive Services soft-delete on RG deletion and silently hold the name (and quota) for 7–90 days. This skill purges them so the user can re-deploy with the same name immediately.
- State consistency. Updates
state.jsonandmetadata.jsonto terminal status (destroyed,retained-soft-deleted, etc.) so the next operation sees an accurate view.
DO NOT USE FOR
Refuse to invoke this skill in any of these cases:
- No
state.jsonon disk. Hard prerequisite — see below. Without it, recommend re-running deploy or aborting. - Resource groups not created by Git-Ape (e.g. ones the user made by hand with
az group create). Suggestaz group delete --name <rg> --yesdirectly instead. - Deploying or updating a stack. Use
azure-stack-deployfor those. - Deleting an individual resource inside a stack. This skill always destroys the whole stack — there is no "surgical" mode.
- Non-Azure clouds or non-Git-Ape Azure deployments (ARM/Bicep/Terraform from other tools).
When to Use
- User says: "destroy this deployment", "tear down deploy-XXX", "clean up the stack"
- Pair with the matching
azure-stack-deploy— same stack, samestate.jsonkey (stackId) - Any time you would otherwise run
az group deleteagainst a Git-Ape deployment (don't — you'll miss soft-delete cleanup and multi-RG resources)
Prerequisites
| Tool | Why |
|---|---|
az (Azure CLI ≥ 2.59) |
az stack sub delete --bypass-stack-out-of-sync-error requires a recent CLI |
jq |
Read state.json |
bash ≥ 4 OR PowerShell 7+ |
Either runner works |
Active az login |
Must be the same subscription where the stack lives |
Existing state.json under .azure/deployments/<id>/ |
Source of truth for stackId, managedResources, softDeletable, purgeProtected |
Hard prerequisite:
state.jsonunder.azure/deployments/<id>/. Without it this skill aborts — it has no idea which stack, resource groups, or soft-deletables to clean up. Do NOT hand-writestate.json; re-run the matchingazure-stack-deployfor that deployment ID first, or useaz group deletedirectly on a known resource group (a non-Git-Ape teardown, outside this skill's scope).
Procedure
Fast mode vs sync mode
The scripts default to fast mode (interactive default). The CI workflow keeps sync mode (deterministic).
| How | Wait time (small VNet stack) | When to use | |
|---|---|---|---|
| Fast (default) | Background the az stack sub delete call, then poll managed RGs with az group exists |
~2 min | Local CLI / VS Code use; user wants quick feedback |
Sync (--wait / -Wait) |
az stack sub delete ... --yes (blocks until stack metadata is fully cleaned) |
~5 min | CI pipelines (default in git-ape-destroy.yml); when you need every Azure-side cleanup completed before the script exits |
The Azure CLI does not expose --no-wait on az stack sub delete, so the fast path runs the same command as a detached background process. In fast mode the stack-metadata cleanup continues asynchronously in Azure after the script returns. The next destroy of the same deploymentId is idempotent: if the stack is still finalizing, az stack sub show will return it and the script will simply pick up where Azure left off.
1. Identify deployment
DEPLOYMENT_ID="deploy-20260506-001"
DEPLOYMENT_PATH=".azure/deployments/$DEPLOYMENT_ID"
[[ -f "$DEPLOYMENT_PATH/state.json" ]] || { echo "state.json missing — cannot destroy"; exit 1; }
2. Run the script
.github/skills/azure-stack-destroy/scripts/destroy-stack.sh \
--deployment-id "$DEPLOYMENT_ID"
Skip the confirmation prompt (use only in automation):
.github/skills/azure-stack-destroy/scripts/destroy-stack.sh \
--deployment-id "$DEPLOYMENT_ID" \
--yes
Force CI-equivalent sync wait (default for the CI workflow; opt-in for the script):
.github/skills/azure-stack-destroy/scripts/destroy-stack.sh \
--deployment-id "$DEPLOYMENT_ID" \
--yes --wait
PowerShell equivalents:
.github/skills/azure-stack-destroy/scripts/destroy-stack.ps1 -DeploymentId "$DEPLOYMENT_ID"
.github/skills/azure-stack-destroy/scripts/destroy-stack.ps1 -DeploymentId "$DEPLOYMENT_ID" -Yes
.github/skills/azure-stack-destroy/scripts/destroy-stack.ps1 -DeploymentId "$DEPLOYMENT_ID" -Yes -Wait
3. What the script does
- Reads
state.jsonand extractsstackId,deployMethod,resourceGroup,managedResources[],softDeletable[] - Prints a destroy plan — stack ID, resource group, count of soft-deletables (with purge-protection flagged)
- Prompts for typed
destroyconfirmation (unless--yes) - Stack delete path (
stackIdpresent):az stack sub delete --action-on-unmanage deleteAll --bypass-stack-out-of-sync-error true --yes- The bypass flag is safe in destroy because it's a one-shot operation — we don't need the stale-manifest safety check that protects iterative updates
- Fallback path (no
stackId, onlyresourceGroup):az group delete --name <rg> --yes - Purge sweep for each
softDeletableresource not markedpurgeProtected:- Key Vaults:
az keyvault list-deleted+az keyvault purge - Cognitive Services:
az cognitiveservices account purge - Other types (App Configuration, API Management, ML workspaces, Recovery Services vaults): not auto-purged — they expire from soft-delete naturally and are tracked in
purgeResults[]withstatus: skipped-natural-expiry
- Key Vaults:
- Cleans the subscription deployment-history entry (
az deployment sub delete) to stay under the 800/scope limit - Updates
state.jsonandmetadata.jsonwith terminal status:
| Status | Meaning |
|---|---|
destroyed |
Stack/RG gone and all soft-deletables purged or absent |
retained-soft-deleted |
Stack gone but at least one soft-deletable retained (purge-protected or purge failed) |
partially-destroyed |
Stack delete partially failed |
destroy-failed |
Stack/RG delete failed entirely |
already-destroyed |
Stack and RG were already gone before this call |
4. Inspect the result
=== Destroy Summary ===
Status: destroyed
Duration: 87s
=======================
Or, when something is intentionally retained:
=== Destroy Summary ===
Status: retained-soft-deleted
Duration: 92s
Retained: 1 soft-deleted resource(s) (purge-protected)
=======================
state.json gains destroyedAt, destroyedBy, destroyDuration, and a purgeResults[] array describing each soft-deletable's outcome.
Arguments
| Flag (bash) | Param (pwsh) | Required | Description |
|---|---|---|---|
--deployment-id <id> |
-DeploymentId <id> |
yes | Folder name under .azure/deployments/ |
--yes |
-Yes |
no | Skip the typed destroy confirmation prompt (CI-only) |
--wait |
-Wait |
no | Sync mode: block until Azure has cleaned up stack metadata. Matches the CI workflow. Slower (~3-4×) but fully deterministic. |
--poll-timeout <sec> |
-PollTimeout <sec> |
no | Fast-mode timeout per managed RG poll (default 600s) |
Failure modes
| Symptom | Likely cause | Recovery |
|---|---|---|
state.json missing |
Deployment never reached the state-write phase, or was hand-edited | Re-deploy (idempotent on stack name) then destroy, OR delete the .azure/deployments/<id>/ folder if Azure has nothing |
Stack out of sync despite --bypass-stack-out-of-sync-error |
Old CLI version | Upgrade az to ≥ 2.59 |
| Key Vault purge fails | Vault is purge-protected (purgeProtected: true) |
Expected — wait 7-90 days for soft-delete window to expire, or purge manually after disabling protection |
Cannot delete resource group …/InUseSubnetCannotBeDeleted |
A resource outside the stack references one inside (e.g. external subnet peered to a deleted VNet) | Inspect externalReferences[] in state.json; remove the reference and rerun |
Related
azure-stack-deploy— the matching deploy skill (writes thestate.jsonthis skill consumes)azure-drift-detector— check for unmanaged drift BEFORE destroyazure-resource-visualizer— visualize what's in the stack before tearing it down