Leverage Deployment Stacks for idempotent destroy#44
Conversation
…tate schema - Deploy workflow: use `az stack sub create` with `--action-on-unmanage deleteAll` as the default deployment method, with `az deployment sub create` as fallback - Deploy workflow: add managed resources capture step after deploy that walks deployment operations or stack resources to populate state.managedResources[] - Destroy workflow: use `az stack sub delete` when stackId is present in state, covering multi-RG, sub-scope, and MG-scope resources uniformly - Destroy workflow: add soft-delete purge loop for Key Vault and Cognitive Services - Destroy workflow: add deployment history cleanup step - Destroy workflow: support new terminal statuses: `partially-destroyed` and `retained-soft-deleted` - State schema: extend state.json with stackId, deployMethod, managedResources[], resourceGroups[], subscriptions[], externalReferences[] - Metadata schema: add deployMethod and resourceGroups[] fields - Documentation: update deployment state docs with new schema, statuses, and destroy strategy selection logic - Regenerate workflow documentation pages Agent-Logs-Url: https://github.com/Azure/git-ape/sessions/d2d1da54-9a38-41ef-9254-b5f585eab10e Co-authored-by: arnaudlh <20535201+arnaudlh@users.noreply.github.com>
- introduce azure-stack-deploy and azure-stack-destroy skills (bash + pwsh) - destroy: fast async mode (default) polls resource groups, --wait for sync - align workflows + agents + docs with new skills - bump plugin to 0.1.0 🚀 - Generated by Copilot
|
🔧 - Generated by Copilot
There was a problem hiding this comment.
Pull request overview
Updates Git-Ape’s deploy/destroy flows to use Azure Deployment Stacks as the primary lifecycle primitive, aiming to make destroy + redeploy idempotent across resource groups, subscription-scope resources, and soft-deletable services. It also introduces local “stack deploy/destroy” skills and extends the persisted deployment state schema to record stack identity and managed resources for deterministic teardown.
Changes:
- Switch deploy from
az deployment sub createtoaz stack sub create(with state capture of managed resources / RGs). - Switch destroy to prefer
az stack sub delete --action-on-unmanage deleteAll, add soft-delete purge sweep + subscription deployment-history cleanup. - Add
/azure-stack-deployand/azure-stack-destroyskills (bash + PowerShell) and update docs/agent guidance + version bumps.
Show a summary per file
| File | Description |
|---|---|
website/docs/workflows/git-ape-destroy.md |
Documents new stack-first destroy workflow, purge sweep, and new terminal statuses. |
website/docs/workflows/git-ape-deploy.md |
Documents stack-first deploy workflow and managed-resource/state capture. |
website/docs/skills/overview.md |
Adds “General Skills” entries for stack deploy/destroy. |
website/docs/skills/azure-stack-destroy.md |
New docs page for stack-based destroy skill. |
website/docs/skills/azure-stack-deploy.md |
New docs page for stack-based deploy skill. |
website/docs/deployment/state.md |
Extends state/metadata schema docs and lifecycle diagram for stack + new destroy outcomes. |
website/docs/agents/git-ape.md |
Updates agent guidance to prefer stacks and soft-delete purge on destroy. |
website/docs/agents/azure-template-generator.md |
Updates generated-agent guidance to prefer stacks (fallback to legacy deployment). |
website/docs/agents/azure-resource-deployer.md |
Updates deployer guidance to use stack validate/create and verify extended state.json. |
plugin.json |
Bumps plugin version to 0.1.0. |
.github/workflows/git-ape-destroy.exampleyml |
Implements stack delete path, purge sweep, deployment history cleanup, and new statuses. |
.github/workflows/git-ape-deploy.exampleyml |
Implements stack validate/create and writes extended state.json + metadata updates. |
.github/skills/azure-stack-destroy/SKILL.md |
Adds new user-invocable destroy skill spec and usage. |
.github/skills/azure-stack-destroy/scripts/destroy-stack.sh |
Adds local bash destroy implementation (stack delete + purge + state updates). |
.github/skills/azure-stack-destroy/scripts/destroy-stack.ps1 |
Adds local PowerShell destroy implementation (stack delete + purge + state updates). |
.github/skills/azure-stack-deploy/SKILL.md |
Adds new user-invocable deploy skill spec and usage. |
.github/skills/azure-stack-deploy/scripts/deploy-stack.sh |
Adds local bash deploy implementation (stack create + managed resource capture + state writes). |
.github/skills/azure-stack-deploy/scripts/deploy-stack.ps1 |
Adds local PowerShell deploy implementation (stack create + managed resource capture + state writes). |
.github/scripts/deployment-manager.sh |
Re-scopes manager script to inventory-only and points deploy/destroy to new skills. |
.github/plugin/marketplace.json |
Bumps marketplace metadata version to 0.1.0. |
.github/copilot-instructions.md |
Updates guidance to use stack deploy/destroy skills in local + CI flows. |
.github/agents/git-ape.agent.md |
Mirrors website agent docs: stacks preferred + purge sweep guidance. |
.github/agents/azure-template-generator.agent.md |
Mirrors website generator docs: stacks preferred + fallback guidance. |
.github/agents/azure-resource-deployer.agent.md |
Mirrors website deployer docs: stack validate/create + extended state verification. |
Copilot's findings
Comments suppressed due to low confidence (2)
.github/skills/azure-stack-deploy/scripts/deploy-stack.sh:233
RESOURCE_GROUPSis derived viajq capture("/resourceGroups/(?<rg>[^/]+)")over everymanagedResources[].id, which will error on subscription-scoped resource IDs (no/resourceGroups/). This can make the deploy skill fail while writingstate.jsoneven though the deployment itself succeeded; filter to RG-scoped IDs or use a non-throwing match (capture(...)?/try).
RESOURCE_GROUPS=$(echo "$MANAGED_RESOURCES" | jq -c '[.[].id | capture("/resourceGroups/(?<rg>[^/]+)") | .rg] | unique')
[[ "$(echo "$RESOURCE_GROUPS" | jq 'length')" == "0" && -n "$RG_NAME" ]] && RESOURCE_GROUPS="[\"$RG_NAME\"]"
.github/skills/azure-stack-destroy/scripts/destroy-stack.sh:298
grep -oE '(?<=locations/)[^/]+'uses a PCRE lookbehind, but-E(ERE) doesn’t support lookbehind. This will fail to extract the Cognitive Services location and silently skip purge. Usegrep -oPor another non-lookbehind parsing approach.
"Microsoft.CognitiveServices/accounts")
if [[ "$PURGE_PROTECTED" != "true" ]]; then
LOC=$(echo "$RES_ID" | grep -oE '(?<=locations/)[^/]+' || echo "")
if [[ -n "$LOC" ]]; then
az cognitiveservices account purge --name "$RES_NAME" --location "$LOC" \
- Files reviewed: 24/24 changed files
- Comments generated: 10
| # Determine deploy method: prefer deployment stacks (idempotent destroy) | ||
| # Fall back to az deployment sub create if stacks are unavailable | ||
| DEPLOY_METHOD="stack" | ||
| # Verbose output goes to a temp file so it does not contaminate the | ||
| # JSON that downstream jq calls need to parse. | ||
| VERBOSE_LOG=$(mktemp) | ||
| trap 'rm -f "$VERBOSE_LOG"' EXIT | ||
|
|
| done | ||
|
|
||
| # Extract resource groups from managed resources | ||
| RESOURCE_GROUPS=$(echo "$MANAGED_RESOURCES" | jq -c '[.[].id | capture("/resourceGroups/(?<rg>[^/]+)") | .rg] | unique') |
| done | ||
|
|
||
| MANAGED_RESOURCES=$(echo "$MANAGED_RESOURCES" | jq --arg id "$RES_ID" --arg type "$RES_TYPE" \ | ||
| --arg scope "$RES_SCOPE" --argjson sd "$IS_SOFT_DELETABLE" \ | ||
| '. + [{"id": $id, "type": $type, "scope": $scope, "softDeletable": $sd, "purgeProtected": false}]') | ||
| done |
| _classify_resource() { | ||
| local RES_ID="$1" | ||
| local RES_TYPE | ||
| RES_TYPE=$(echo "$RES_ID" | grep -oE 'providers/[^/]+/[^/]+' | head -1 | sed 's|providers/||') |
| # If the bg process already failed, surface it early | ||
| if ! kill -0 "$STACK_BG_PID" 2>/dev/null; then | ||
| wait "$STACK_BG_PID" 2>/dev/null || true | ||
| BG_EXIT=$? | ||
| if [[ $BG_EXIT -ne 0 ]]; then | ||
| EXISTS=$(az group exists --name "$RG" 2>/dev/null || echo "true") |
| echo -e "${YELLOW}Stack already gone — skipping stack delete${NC}" | ||
| STACK_DELETED="true" |
| Write-Color 'Stack already gone — skipping stack delete' Yellow | ||
| $StackDeleted = $true |
| # Determine deploy method: prefer deployment stacks (idempotent destroy) | ||
| # Fall back to az deployment sub create if stacks are unavailable | ||
| DEPLOY_METHOD="stack" | ||
|
|
||
| if [[ "$DEPLOY_METHOD" == "stack" ]]; then | ||
| DEPLOY_OUTPUT=$(az stack sub create \ | ||
| --name "$DEPLOYMENT_ID" \ | ||
| --location "$LOCATION" \ | ||
| --template-file "$DEPLOY_DIR/template.json" \ | ||
| --parameters @"$DEPLOY_DIR/parameters.json" \ | ||
| --action-on-unmanage deleteAll \ | ||
| --deny-settings-mode none \ | ||
| --description "Git-Ape deployment $DEPLOYMENT_ID" \ | ||
| --tags "managedBy=git-ape" "deploymentId=$DEPLOYMENT_ID" \ | ||
| --yes \ | ||
| --verbose \ | ||
| --output json 2>&1) | ||
| else |
|
|
||
| **Destroy strategy selection:** | ||
|
|
||
| 1. If `stackId` is present → `az stack sub delete --name <stackId> --action-on-unmanage deleteAll --bypass-stack-out-of-sync-error true` |
| | `managedResources[].id` | `string` | Full ARM resource ID. | | ||
| | `managedResources[].type` | `string` | ARM resource type (e.g., `Microsoft.KeyVault/vaults`). | | ||
| | `managedResources[].scope` | `string` | Scope level: `resourceGroup`, `subscription`, or `managementGroup`. | | ||
| | `managedResources[].apiVersion` | `string` | API version used for the resource. | |
The destroy workflow deletes a single resource group but leaves behind soft-deleted resources (Key Vault with purge protection), subscription-scoped resources, and multi-RG deployments — making destroy + redeploy non-idempotent.
Changes
Deploy workflow (
git-ape-deploy.exampleyml)az deployment sub createtoaz stack sub create --action-on-unmanage deleteAllstate.jsonnow populated withstackId,deployMethod,managedResources[],resourceGroups[],subscriptions[],externalReferences[]metadata.jsongainsdeployMethodandresourceGroups[]on commitDestroy workflow (
git-ape-destroy.exampleyml)az stack sub delete --action-on-unmanage deleteAllwhenstackIdpresent — single command covers all managed resources regardless of scopeaz group deletefor pre-stack deploymentsmanagedResources[].softDeletable, purges non-protected Key Vaults, marks purge-protected asretained-soft-deletedaz deployment sub deleteto prevent 800/scope accumulationpartially-destroyed,retained-soft-deletedState schema (
website/docs/deployment/state.md)state.jsonschema with destroy strategy selection logicdeployMethodfield tometadata.jsonspecExample state.json (post-deploy)
{ "stackId": "/subscriptions/.../providers/Microsoft.Resources/deploymentStacks/deploy-20260218-143022", "deployMethod": "stack", "managedResources": [ { "id": "/subscriptions/.../Microsoft.KeyVault/vaults/kv-api-dev-eus", "type": "Microsoft.KeyVault/vaults", "scope": "resourceGroup", "softDeletable": true, "purgeProtected": true } ], "resourceGroups": ["rg-api-dev-eastus"], "subscriptions": ["00000000-..."], "externalReferences": [] }This implements Phase 1 (schema + state capture) and Phase 2 (Deployment Stacks integration) from the issue. Phase 3 (extract destroy to standalone script) and Phase 4 (fixture validation) are deferred.