Skip to content

Leverage Deployment Stacks for idempotent destroy#44

Open
Copilot wants to merge 4 commits intomainfrom
copilot/leverage-deployment-stacks-idempotency
Open

Leverage Deployment Stacks for idempotent destroy#44
Copilot wants to merge 4 commits intomainfrom
copilot/leverage-deployment-stacks-idempotency

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

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)

  • Default deploy method changed from az deployment sub create to az stack sub create --action-on-unmanage deleteAll
  • New "Capture managed resources" step walks stack resources or deployment operations post-deploy
  • state.json now populated with stackId, deployMethod, managedResources[], resourceGroups[], subscriptions[], externalReferences[]
  • metadata.json gains deployMethod and resourceGroups[] on commit

Destroy workflow (git-ape-destroy.exampleyml)

  • Stack path: az stack sub delete --action-on-unmanage deleteAll when stackId present — single command covers all managed resources regardless of scope
  • Fallback path: preserved legacy sub-scoped resource cleanup → az group delete for pre-stack deployments
  • Soft-delete purge loop: iterates managedResources[].softDeletable, purges non-protected Key Vaults, marks purge-protected as retained-soft-deleted
  • Deployment history cleanup: az deployment sub delete to prevent 800/scope accumulation
  • New terminal statuses: partially-destroyed, retained-soft-deleted

State schema (website/docs/deployment/state.md)

  • Documented extended state.json schema with destroy strategy selection logic
  • Updated lifecycle diagram with new terminal states
  • Added deployMethod field to metadata.json spec

Example 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.

Copilot AI linked an issue May 5, 2026 that may be closed by this pull request
…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>
Copilot AI changed the title [WIP] Leverage deployment stacks for idempotency in destroy flow Leverage Deployment Stacks for idempotent destroy May 5, 2026
Copilot AI requested a review from arnaudlh May 5, 2026 02:27
- 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
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

⚠️ Documentation Staleness Warning

Source files (agents, skills, workflows, or config) changed in this PR, but the generated documentation is out of date.

Changed docs that need regeneration:

  • website/docs/reference/marketplace.md
  • website/docs/reference/plugin-json.md
  • website/docs/skills/azure-stack-destroy.md
  • website/docs/workflows/git-ape-deploy.md
  • website/docs/workflows/overview.md

To fix: Run the following command and commit the results:

node scripts/generate-docs.js

This is an advisory check — it does not block the PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 create to az 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-deploy and /azure-stack-destroy skills (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_GROUPS is derived via jq capture("/resourceGroups/(?<rg>[^/]+)") over every managedResources[].id, which will error on subscription-scoped resource IDs (no /resourceGroups/). This can make the deploy skill fail while writing state.json even 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. Use grep -oP or 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

Comment on lines +252 to +259
# 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')
Comment on lines +383 to +388
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/||')
Comment on lines +200 to +205
# 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")
Comment on lines +235 to +236
echo -e "${YELLOW}Stack already gone — skipping stack delete${NC}"
STACK_DELETED="true"
Comment on lines +237 to +238
Write-Color 'Stack already gone — skipping stack delete' Yellow
$StackDeleted = $true
Comment on lines +317 to +334
# 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. |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leverage Deployment Stacks for idempotency

3 participants