From bc89221c53f5f02a07a05d9d9379f93565f2b945 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Mon, 8 Jun 2026 17:42:48 +0700 Subject: [PATCH] fix(docs): rewrite cross-skill SKILL.md links to resolvable slugs The Docs Deploy build (run 27131977843) failed after #142 merged: Docusaurus threw on broken markdown links because generated agent and skill pages still pointed at relative `SKILL.md` paths (e.g. `../skills/prereq-check/SKILL.md`, `../azure-stack-destroy/SKILL.md`) that don't exist in the rendered site. generate-docs.js rewrote `.agent.md` links and skill script/reference links but never handled cross-references to other skills' SKILL.md files. Add rewriteSkillRefLinks(): from agent pages it targets `../skills/`, from skill pages `./`. Regenerated docs; `docusaurus build` now passes. Also regenerates git-ape-release.md, which had drifted from its source workflow after #144. --- scripts/generate-docs.js | 18 +- .../docs/agents/azure-resource-deployer.md | 26 +-- .../docs/agents/azure-template-generator.md | 32 ++-- website/docs/skills/azure-stack-deploy.md | 14 +- website/docs/skills/azure-stack-destroy.md | 8 +- website/docs/workflows/git-ape-release.md | 174 +++++------------- 6 files changed, 106 insertions(+), 166 deletions(-) diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js index 0e0bcb2..025e726 100644 --- a/scripts/generate-docs.js +++ b/scripts/generate-docs.js @@ -94,6 +94,20 @@ function rewriteSkillLinks(body, skillDir) { }); } +// Rewrite cross-references to a skill's `SKILL.md` into docusaurus-friendly slug links. +// Source files link to skills via relative paths like `../skills//SKILL.md` (from +// agent sources) or `..//SKILL.md` (from sibling skill sources). The generated +// docusaurus pages live at `docs/skills/.md`, so rewrite the target to the slug +// using the supplied base (`../skills/` from agent pages, `./` from skill pages). +function rewriteSkillRefLinks(body, targetBase) { + return body.replace(/(\[[^\]\n]+\])\(([^)\s]+\/SKILL\.md)\)/g, (_match, label, url) => { + if (/^https?:\/\//i.test(url)) return `${label}(${url})`; + const segments = url.replace(/\/SKILL\.md$/, '').split('/'); + const skillName = segments[segments.length - 1]; + return `${label}(${targetBase}${slugify(skillName)})`; + }); +} + // --------------------------------------------------------------------------- // Agent doc generation // --------------------------------------------------------------------------- @@ -123,7 +137,7 @@ function generateAgentDocs() { agents.push({ name, slug, description, userInvocable, file }); // Extract meaningful body (skip the frontmatter warning section, keep substance) - const bodyTrimmed = rewriteAgentLinks(body.trim()); + const bodyTrimmed = rewriteSkillRefLinks(rewriteAgentLinks(body.trim()), '../skills/'); let content = `--- title: "${name}" @@ -262,7 +276,7 @@ function generateSkillDocs() { skills.push({ name, slug, description, userInvocable, phase, dir }); - const bodyTrimmed = rewriteSkillLinks(body.trim(), dir); + const bodyTrimmed = rewriteSkillRefLinks(rewriteSkillLinks(body.trim(), dir), './'); let content = `--- title: "${toTitleCase(name)}" diff --git a/website/docs/agents/azure-resource-deployer.md b/website/docs/agents/azure-resource-deployer.md index 8dc9b2f..4070344 100644 --- a/website/docs/agents/azure-resource-deployer.md +++ b/website/docs/agents/azure-resource-deployer.md @@ -47,11 +47,11 @@ This agent is a thin orchestrator over the following skills. Do not duplicate th | Stage | Skill | Why | |-------|-------|-----| -| Pre-flight | [`/prereq-check`](../skills/prereq-check/SKILL.md) | Verify `az`, `jq`, `gh`, `git` are installed and `az login` is active | -| Pre-flight | [`/azure-deployment-preflight`](../skills/azure-deployment-preflight/SKILL.md) | What-if analysis, permission checks, change preview (CREATE/MODIFY/DELETE) | -| Deploy | [`/azure-stack-deploy`](../skills/azure-stack-deploy/SKILL.md) | The canonical `az stack sub create` runner — writes `state.json` (schemaVersion 1.0), classifies soft-deletable + purge-protected resources | -| Verify | [`/azure-integration-tester`](../skills/azure-integration-tester/SKILL.md) | Post-deployment health checks and endpoint tests | -| Rollback | [`/azure-stack-destroy`](../skills/azure-stack-destroy/SKILL.md) | `az stack sub delete --action-on-unmanage deleteAll` + soft-delete purge sweep | +| Pre-flight | [`/prereq-check`](../skills/prereq-check) | Verify `az`, `jq`, `gh`, `git` are installed and `az login` is active | +| Pre-flight | [`/azure-deployment-preflight`](../skills/azure-deployment-preflight) | What-if analysis, permission checks, change preview (CREATE/MODIFY/DELETE) | +| Deploy | [`/azure-stack-deploy`](../skills/azure-stack-deploy) | The canonical `az stack sub create` runner — writes `state.json` (schemaVersion 1.0), classifies soft-deletable + purge-protected resources | +| Verify | [`/azure-integration-tester`](../skills/azure-integration-tester) | Post-deployment health checks and endpoint tests | +| Rollback | [`/azure-stack-destroy`](../skills/azure-stack-destroy) | `az stack sub delete --action-on-unmanage deleteAll` + soft-delete purge sweep | ## Output Styling @@ -64,7 +64,7 @@ Use the shared progress bar and status line patterns for polling updates and sum Detect the auth context and configure accordingly. Never hardcode credentials. -> **Tool + session check:** Invoke [`/prereq-check`](../skills/prereq-check/SKILL.md) once at the very start of Stage 3 to confirm `az`, `jq`, and `gh` are installed at minimum versions AND that `az account show` returns an active subscription. The skill prints platform-specific install commands for anything missing. +> **Tool + session check:** Invoke [`/prereq-check`](../skills/prereq-check) once at the very start of Stage 3 to confirm `az`, `jq`, and `gh` are installed at minimum versions AND that `az account show` returns an active subscription. The skill prints platform-specific install commands for anything missing. ### Interactive (VS Code / local) The user is already authenticated via `az login`. The `prereq-check` skill above verifies this. If you need the subscription details directly: @@ -124,7 +124,7 @@ If invoked without user confirmation, **STOP** and report: "Deployment requires ### 1. Pre-Deployment Validation -**Delegate to:** [`/azure-deployment-preflight`](../skills/azure-deployment-preflight/SKILL.md) +**Delegate to:** [`/azure-deployment-preflight`](../skills/azure-deployment-preflight) Do not run ad-hoc `az deployment sub validate` or `az stack sub validate` yourself — the preflight skill already owns this and produces a structured report (`preflight-report.md`) with what-if categorization, permission checks, and a CREATE/MODIFY/DELETE summary. @@ -144,7 +144,7 @@ If the preflight report flags any blocking issue, **STOP** and surface the issue **Always deploy as a subscription-scoped Deployment Stack.** Stacks track every managed resource (across resource groups and subscription scope) and make destroy idempotent — a single `az stack sub delete --action-on-unmanage deleteAll` removes everything the stack owns, regardless of resource scope. -> **Single source of truth:** the deploy command, fallback handling, state.json writer, soft-delete classification, and Key Vault purge-protection detection all live in the [`azure-stack-deploy`](../skills/azure-stack-deploy/SKILL.md) skill. Both bash and PowerShell implementations are provided. +> **Single source of truth:** the deploy command, fallback handling, state.json writer, soft-delete classification, and Key Vault purge-protection detection all live in the [`azure-stack-deploy`](../skills/azure-stack-deploy) skill. Both bash and PowerShell implementations are provided. **Pre-flight: validate the stack before deploying** @@ -237,7 +237,7 @@ az deployment operation sub list \ ### 4. Verify Resource Creation -**Delegate to:** [`/azure-integration-tester`](../skills/azure-integration-tester/SKILL.md) +**Delegate to:** [`/azure-integration-tester`](../skills/azure-integration-tester) The integration tester is the single source of truth for post-deployment verification. It reads `state.json` (written by `azure-stack-deploy` in Step 2) to know what to check, then runs health probes per resource type — Function App HTTP probe, Storage Account `az storage account show`, App Service health endpoint, Database connection check, etc. @@ -281,7 +281,7 @@ Common outputs to capture: ### 6. Verify `state.json` was written -The [`azure-stack-deploy`](../skills/azure-stack-deploy/SKILL.md) skill writes `state.json` (schemaVersion 1.0) and updates `metadata.json` with `deployMethod` and `resourceGroups[]` as part of step 2. The agent's job here is to confirm the write succeeded and surface its contents for the user. +The [`azure-stack-deploy`](../skills/azure-stack-deploy) skill writes `state.json` (schemaVersion 1.0) and updates `metadata.json` with `deployMethod` and `resourceGroups[]` as part of step 2. The agent's job here is to confirm the write succeeded and surface its contents for the user. ```bash DEPLOYMENT_ID="{deployment-id}" @@ -295,7 +295,7 @@ jq '{schemaVersion, deploymentId, deployMethod, stackId, resourceGroups, managed If `deployMethod == "stack"` and `stackId` is empty, the deploy fell back silently — re-run the skill with `--no-fallback` to surface why stacks were rejected. -The destroy skill ([`azure-stack-destroy`](../skills/azure-stack-destroy/SKILL.md)) consumes this file as its sole source of truth. +The destroy skill ([`azure-stack-destroy`](../skills/azure-stack-destroy)) consumes this file as its sole source of truth. ### 7. Report Deployment Results @@ -330,7 +330,7 @@ Provide a comprehensive summary: To destroy this deployment and delete all its resources: > `@git-ape destroy deployment {deployment-id}` > -> Locally this invokes the [`azure-stack-destroy`](../skills/azure-stack-destroy/SKILL.md) skill, which uses `az stack sub delete --action-on-unmanage deleteAll --bypass-stack-out-of-sync-error true` (single command, idempotent across resource groups and subscription scope) and purges any soft-deletable resources that are not purge-protected. +> Locally this invokes the [`azure-stack-destroy`](../skills/azure-stack-destroy) skill, which uses `az stack sub delete --action-on-unmanage deleteAll --bypass-stack-out-of-sync-error true` (single command, idempotent across resource groups and subscription scope) and purges any soft-deletable resources that are not purge-protected. > > Or via GitHub: create a PR that sets `metadata.json` status to `destroy-requested`, then merge after approval. @@ -441,7 +441,7 @@ if [[ "$USER_CHOICE" == "A" ]]; then fi ``` -> **Important:** Never mix individual `az resource delete` calls when a `stackId` is present in `state.json`. The stack path is canonical — always invoke the [`azure-stack-destroy`](../skills/azure-stack-destroy/SKILL.md) skill, which encapsulates the stack delete, fallback RG delete, and soft-delete purge sweep (Key Vault, Cognitive Services, etc.) for any resources that are not purge-protected. +> **Important:** Never mix individual `az resource delete` calls when a `stackId` is present in `state.json`. The stack path is canonical — always invoke the [`azure-stack-destroy`](../skills/azure-stack-destroy) skill, which encapsulates the stack delete, fallback RG delete, and soft-delete purge sweep (Key Vault, Cognitive Services, etc.) for any resources that are not purge-protected. **Step 4: Update deployment state:** ```json diff --git a/website/docs/agents/azure-template-generator.md b/website/docs/agents/azure-template-generator.md index 8891dac..7889ca8 100644 --- a/website/docs/agents/azure-template-generator.md +++ b/website/docs/agents/azure-template-generator.md @@ -48,14 +48,14 @@ This agent is a thin orchestrator over the following skills. Do not duplicate th | Stage | Skill | Why | |-------|-------|-----| -| Step 0 (lookup) | [`/azure-rest-api-reference`](../skills/azure-rest-api-reference/SKILL.md) | Get exact property schemas, required fields, valid enum values, latest stable API version per resource type. **Mandatory before writing any resource.** | -| Step 0 (lookup) | [`/azure-naming-research`](../skills/azure-naming-research/SKILL.md) | CAF abbreviation, length / charset constraints, uniqueness scope. **Mandatory before naming any resource.** | -| Step 1 (write) | [`/azure-role-selector`](../skills/azure-role-selector/SKILL.md) | Least-privilege RBAC role lookup — returns the GUIDs for `Storage Blob Data Owner`, `Storage Account Contributor`, etc. Do NOT hardcode GUIDs in the agent. | -| Step 2 (assess) | [`/azure-security-analyzer`](../skills/azure-security-analyzer/SKILL.md) | Per-resource security best practices assessment + the BLOCKING security gate | -| Step 2 (assess) | [`/azure-policy-advisor`](../skills/azure-policy-advisor/SKILL.md) | Azure Policy compliance check against CIS / NIST / org framework (advisory) | -| Step 2 (assess) | [`/azure-resource-availability`](../skills/azure-resource-availability/SKILL.md) | Validate SKU + API version availability in target region + subscription quota (BLOCKING) | -| Step 2 (assess) | [`/azure-deployment-preflight`](../skills/azure-deployment-preflight/SKILL.md) | What-if analysis showing what will Create / Modify / Delete | -| Step 2 (assess) | [`/azure-cost-estimator`](../skills/azure-cost-estimator/SKILL.md) | Real pricing from Azure Retail Prices API | +| Step 0 (lookup) | [`/azure-rest-api-reference`](../skills/azure-rest-api-reference) | Get exact property schemas, required fields, valid enum values, latest stable API version per resource type. **Mandatory before writing any resource.** | +| Step 0 (lookup) | [`/azure-naming-research`](../skills/azure-naming-research) | CAF abbreviation, length / charset constraints, uniqueness scope. **Mandatory before naming any resource.** | +| Step 1 (write) | [`/azure-role-selector`](../skills/azure-role-selector) | Least-privilege RBAC role lookup — returns the GUIDs for `Storage Blob Data Owner`, `Storage Account Contributor`, etc. Do NOT hardcode GUIDs in the agent. | +| Step 2 (assess) | [`/azure-security-analyzer`](../skills/azure-security-analyzer) | Per-resource security best practices assessment + the BLOCKING security gate | +| Step 2 (assess) | [`/azure-policy-advisor`](../skills/azure-policy-advisor) | Azure Policy compliance check against CIS / NIST / org framework (advisory) | +| Step 2 (assess) | [`/azure-resource-availability`](../skills/azure-resource-availability) | Validate SKU + API version availability in target region + subscription quota (BLOCKING) | +| Step 2 (assess) | [`/azure-deployment-preflight`](../skills/azure-deployment-preflight) | What-if analysis showing what will Create / Modify / Delete | +| Step 2 (assess) | [`/azure-cost-estimator`](../skills/azure-cost-estimator) | Real pricing from Azure Retail Prices API | ## Output Styling @@ -68,7 +68,7 @@ see [git-ape.agent.md](git-ape). **Two skill invocations are mandatory before you write a single resource block.** Skipping either step is the #1 cause of preventable deployment failures (wrong property names, expired API versions, invalid characters, length overruns). -**0a. Property and API version lookup** — Invoke [`/azure-rest-api-reference`](../skills/azure-rest-api-reference/SKILL.md) for every resource type in the deployment. The skill returns: +**0a. Property and API version lookup** — Invoke [`/azure-rest-api-reference`](../skills/azure-rest-api-reference) for every resource type in the deployment. The skill returns: - Latest stable (non-preview) API version - Required vs optional properties - Valid enum values per property @@ -76,7 +76,7 @@ see [git-ape.agent.md](git-ape). Never rely on memorized schemas. Re-invoke whenever you change the API version of an existing resource. -**0b. Naming research** — Invoke [`/azure-naming-research`](../skills/azure-naming-research/SKILL.md) for every resource type. The skill returns: +**0b. Naming research** — Invoke [`/azure-naming-research`](../skills/azure-naming-research) for every resource type. The skill returns: - CAF abbreviation (e.g. `func`, `st`, `kv`, `cae`) - Length min / max - Valid character set (alphanumeric, hyphens, lowercase-only, etc.) @@ -245,7 +245,7 @@ Many Azure subscriptions enforce `allowSharedKeyAccess: false` via Azure Policy. **Required RBAC Roles for Function App → Storage:** -Do NOT hardcode role definition GUIDs in this agent. Invoke [`/azure-role-selector`](../skills/azure-role-selector/SKILL.md) with the resource pair (e.g. "Function App needs blob + file share access on Storage Account") and use the GUIDs the skill returns. The skill encodes least-privilege — it will recommend `Storage Blob Data Owner` (`b7e6dc6d-f1e8-4753-8033-0f276bb0955b`) + `Storage Account Contributor` (`17d1049b-9a84-46fb-8f53-869881c3d3ab`) for this specific pair, or narrower roles (`Storage Blob Data Contributor`, `Storage File Data SMB Share Contributor`) when full ownership is not needed. +Do NOT hardcode role definition GUIDs in this agent. Invoke [`/azure-role-selector`](../skills/azure-role-selector) with the resource pair (e.g. "Function App needs blob + file share access on Storage Account") and use the GUIDs the skill returns. The skill encodes least-privilege — it will recommend `Storage Blob Data Owner` (`b7e6dc6d-f1e8-4753-8033-0f276bb0955b`) + `Storage Account Contributor` (`17d1049b-9a84-46fb-8f53-869881c3d3ab`) for this specific pair, or narrower roles (`Storage Blob Data Contributor`, `Storage File Data SMB Share Contributor`) when full ownership is not needed. The GUIDs above appear in the example block only so you can verify the skill output matches — do not copy them into new templates without running the skill first. @@ -270,17 +270,17 @@ The GUIDs above appear in the example block only so you can verify the skill out #### General Best Practices -These are **write-time guardrails** — apply them while assembling resource blocks so the template starts in a known-good state. The full assessment runs in Step 3 via [`/azure-security-analyzer`](../skills/azure-security-analyzer/SKILL.md), which has the complete severity-tagged checklist per resource type. Do not duplicate that checklist here. +These are **write-time guardrails** — apply them while assembling resource blocks so the template starts in a known-good state. The full assessment runs in Step 3 via [`/azure-security-analyzer`](../skills/azure-security-analyzer), which has the complete severity-tagged checklist per resource type. Do not duplicate that checklist here. For **ALL resources**: -- ✓ Use latest **stable** API versions — returned by [`/azure-rest-api-reference`](../skills/azure-rest-api-reference/SKILL.md) in Step 0a; never hardcode -- ✓ Use names returned by [`/azure-naming-research`](../skills/azure-naming-research/SKILL.md) in Step 0b +- ✓ Use latest **stable** API versions — returned by [`/azure-rest-api-reference`](../skills/azure-rest-api-reference) in Step 0a; never hardcode +- ✓ Use names returned by [`/azure-naming-research`](../skills/azure-naming-research) in Step 0b - ✓ Enable diagnostic settings and logging - ✓ Apply resource tags from workspace standards - ✓ Use `dependsOn` for proper ordering - ✓ Output resource IDs and endpoints - ✓ **Use managed identity for all inter-resource access** (no keys/secrets) -- ✓ **Include RBAC role assignments** with GUIDs from [`/azure-role-selector`](../skills/azure-role-selector/SKILL.md), not from memory +- ✓ **Include RBAC role assignments** with GUIDs from [`/azure-role-selector`](../skills/azure-role-selector), not from memory **Non-negotiable identity patterns** — these are write-time, not assessment-time, because once a template ships with shared keys / connection strings it is hard to retrofit: @@ -728,7 +728,7 @@ After showing the preview, provide the complete ARM template: ## Deployment Commands -The canonical deploy and destroy paths live in the [`azure-stack-deploy`](../skills/azure-stack-deploy/SKILL.md) and [`azure-stack-destroy`](../skills/azure-stack-destroy/SKILL.md) skills. The commands below are reference recipes — prefer invoking the skills so local CLI / VS Code and CI pipelines stay in sync. +The canonical deploy and destroy paths live in the [`azure-stack-deploy`](../skills/azure-stack-deploy) and [`azure-stack-destroy`](../skills/azure-stack-destroy) skills. The commands below are reference recipes — prefer invoking the skills so local CLI / VS Code and CI pipelines stay in sync. **Azure CLI (Subscription-scoped Deployment Stack — preferred):** ```bash diff --git a/website/docs/skills/azure-stack-deploy.md b/website/docs/skills/azure-stack-deploy.md index 5edef24..f82dabb 100644 --- a/website/docs/skills/azure-stack-deploy.md +++ b/website/docs/skills/azure-stack-deploy.md @@ -25,7 +25,7 @@ description: "Run an Azure Deployment Stack create (subscription scope) for a pr # Azure Stack Deploy -Deploy a Git-Ape deployment artifact as a subscription-scoped **Azure Deployment Stack** (`az stack sub create --action-on-unmanage deleteAll`). The stack is the lifecycle owner of every resource the template creates — across resource groups and subscription scope — which makes destroy idempotent in a single call (see [`azure-stack-destroy`](../azure-stack-destroy/SKILL.md)). +Deploy a Git-Ape deployment artifact as a subscription-scoped **Azure Deployment Stack** (`az stack sub create --action-on-unmanage deleteAll`). The stack is the lifecycle owner of every resource the template creates — across resource groups and subscription scope — which makes destroy idempotent in a single call (see [`azure-stack-destroy`](./azure-stack-destroy)). This skill produces the **same `state.json`** schema (`schemaVersion: "1.0"`) as the CI workflow at `.github/workflows/git-ape-deploy.yml`, so local deployments and pipeline deployments are interchangeable. @@ -37,8 +37,8 @@ This skill produces the **same `state.json`** schema (`schemaVersion: "1.0"`) as ## Do NOT use for -- **Tearing down / destroying** an existing deployment — use [`azure-stack-destroy`](../azure-stack-destroy/SKILL.md) instead -- **What-if preview / preflight validation** without deploying — use [`azure-deployment-preflight`](../azure-deployment-preflight/SKILL.md) instead +- **Tearing down / destroying** an existing deployment — use [`azure-stack-destroy`](./azure-stack-destroy) instead +- **What-if preview / preflight validation** without deploying — use [`azure-deployment-preflight`](./azure-deployment-preflight) instead - **Off-topic** (non-Azure, non-deployment) requests - Generating or editing ARM templates — use `azure-prepare` or another IaC authoring skill @@ -159,7 +159,7 @@ See [website/docs/deployment/state.md](../../../website/docs/deployment/state.md `Microsoft.KeyVault/vaults`, `Microsoft.CognitiveServices/accounts`, `Microsoft.AppConfiguration/configurationStores`, `Microsoft.ApiManagement/service`, `Microsoft.MachineLearningServices/workspaces`, `Microsoft.RecoveryServices/vaults`. -The destroy skill ([`azure-stack-destroy`](../azure-stack-destroy/SKILL.md)) consumes the `softDeletable` and `purgeProtected` fields to drive its purge sweep. +The destroy skill ([`azure-stack-destroy`](./azure-stack-destroy)) consumes the `softDeletable` and `purgeProtected` fields to drive its purge sweep. ## Failure modes @@ -172,6 +172,6 @@ The destroy skill ([`azure-stack-destroy`](../azure-stack-destroy/SKILL.md)) con ## Related -- [`azure-stack-destroy`](../azure-stack-destroy/SKILL.md) — the matching destroy skill (single source of truth: `stackId`) -- [`azure-deployment-preflight`](../azure-deployment-preflight/SKILL.md) — what-if and permission checks BEFORE deploy -- [`azure-security-analyzer`](../azure-security-analyzer/SKILL.md) — security gate (BLOCKING) before deploy confirmation +- [`azure-stack-destroy`](./azure-stack-destroy) — the matching destroy skill (single source of truth: `stackId`) +- [`azure-deployment-preflight`](./azure-deployment-preflight) — what-if and permission checks BEFORE deploy +- [`azure-security-analyzer`](./azure-security-analyzer) — security gate (BLOCKING) before deploy confirmation diff --git a/website/docs/skills/azure-stack-destroy.md b/website/docs/skills/azure-stack-destroy.md index c415e1c..72f21f1 100644 --- a/website/docs/skills/azure-stack-destroy.md +++ b/website/docs/skills/azure-stack-destroy.md @@ -61,7 +61,7 @@ Refuse to invoke this skill in any of these cases: ## When to Use - User says: "destroy this deployment", "tear down deploy-XXX", "clean up the stack" -- Pair with the matching [`azure-stack-deploy`](../azure-stack-deploy/SKILL.md) — same stack, same `state.json` key (`stackId`) +- Pair with the matching [`azure-stack-deploy`](./azure-stack-deploy) — same stack, same `state.json` key (`stackId`) - Any time you would otherwise run `az group delete` against a Git-Ape deployment (don't — you'll miss soft-delete cleanup and multi-RG resources) ## Prerequisites @@ -193,6 +193,6 @@ Retained: 1 soft-deleted resource(s) (purge-protected) ## Related -- [`azure-stack-deploy`](../azure-stack-deploy/SKILL.md) — the matching deploy skill (writes the `state.json` this skill consumes) -- [`azure-drift-detector`](../azure-drift-detector/SKILL.md) — check for unmanaged drift BEFORE destroy -- [`azure-resource-visualizer`](../azure-resource-visualizer/SKILL.md) — visualize what's in the stack before tearing it down +- [`azure-stack-deploy`](./azure-stack-deploy) — the matching deploy skill (writes the `state.json` this skill consumes) +- [`azure-drift-detector`](./azure-drift-detector) — check for unmanaged drift BEFORE destroy +- [`azure-resource-visualizer`](./azure-resource-visualizer) — visualize what's in the stack before tearing it down diff --git a/website/docs/workflows/git-ape-release.md b/website/docs/workflows/git-ape-release.md index 4556c21..e2e544e 100644 --- a/website/docs/workflows/git-ape-release.md +++ b/website/docs/workflows/git-ape-release.md @@ -99,8 +99,22 @@ jobs: fi echo "Resolved version: $VERSION (tag: v$VERSION)" - - name: Bump plugin.json and marketplace.json - id: bump + - name: Validate release commit is on main history + run: | + set -euo pipefail + # Runs for both push (tag already exists) and workflow_dispatch (tag + # created later in this job). Guarding here — before any tag is + # created or pushed — ensures a manual release cannot publish from a + # commit that is not reachable from main. + git fetch origin main + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "❌ Release commit $GITHUB_SHA is not reachable from origin/main." + echo "Create releases from commits already merged to main." + exit 1 + fi + echo "✅ Release commit is reachable from origin/main." + + - name: Validate release version invariant env: VERSION: ${{ steps.ver.outputs.version }} run: | @@ -110,45 +124,53 @@ jobs: MARKETPLACE_JSON=".github/plugin/marketplace.json" PLUGIN_NAME=$(jq -r '.name' "$PLUGIN_JSON") - OLD_PLUGIN_VERSION=$(jq -r '.version' "$PLUGIN_JSON") - OLD_MKT_VERSION=$(jq -r '.metadata.version' "$MARKETPLACE_JSON") - OLD_MKT_ENTRY_VERSION=$(jq -r --arg name "$PLUGIN_NAME" \ + PLUGIN_VERSION=$(jq -r '.version' "$PLUGIN_JSON") + MKT_METADATA_VERSION=$(jq -r '.metadata.version' "$MARKETPLACE_JSON") + MKT_ENTRY_VERSION=$(jq -r --arg name "$PLUGIN_NAME" \ '.plugins[] | select(.name == $name) | .version' "$MARKETPLACE_JSON") - echo "Current versions:" - echo " plugin.json: $OLD_PLUGIN_VERSION" - echo " marketplace.meta: $OLD_MKT_VERSION" - echo " marketplace.entry: $OLD_MKT_ENTRY_VERSION" + echo "Current versions at release commit:" + echo " plugin.json: $PLUGIN_VERSION" + echo " marketplace.meta: $MKT_METADATA_VERSION" + echo " marketplace.entry: $MKT_ENTRY_VERSION" echo "Target version: $VERSION" - jq --arg v "$VERSION" '.version = $v' "$PLUGIN_JSON" > "$PLUGIN_JSON.tmp" - mv "$PLUGIN_JSON.tmp" "$PLUGIN_JSON" + ERRORS=0 - jq --arg v "$VERSION" --arg name "$PLUGIN_NAME" ' - .metadata.version = $v - | .plugins |= map(if .name == $name then .version = $v else . end) - ' "$MARKETPLACE_JSON" > "$MARKETPLACE_JSON.tmp" - mv "$MARKETPLACE_JSON.tmp" "$MARKETPLACE_JSON" + if [[ "$PLUGIN_VERSION" != "$VERSION" ]]; then + echo "❌ plugin.json version ($PLUGIN_VERSION) does not match release version ($VERSION)" + ERRORS=$((ERRORS + 1)) + fi - if git diff --quiet plugin.json .github/plugin/marketplace.json; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" + if [[ "$MKT_ENTRY_VERSION" != "$VERSION" ]]; then + echo "❌ marketplace plugin entry version ($MKT_ENTRY_VERSION) does not match release version ($VERSION)" + ERRORS=$((ERRORS + 1)) + fi + + if [[ "$MKT_METADATA_VERSION" != "$VERSION" ]]; then + echo "❌ marketplace metadata.version ($MKT_METADATA_VERSION) does not match release version ($VERSION)" + ERRORS=$((ERRORS + 1)) fi - - name: Commit version bump (workflow_dispatch only) - if: github.event_name == 'workflow_dispatch' && steps.bump.outputs.changed == 'true' + if [[ "$ERRORS" -gt 0 ]]; then + echo + echo "Release invariant failed: tag v$VERSION must point to a commit where version files are already synchronized." + echo "Fix by merging a release PR that bumps plugin.json + .github/plugin/marketplace.json before tagging." + exit 1 + fi + + - name: Ensure release tag exists (workflow_dispatch only) + if: github.event_name == 'workflow_dispatch' env: - VERSION: ${{ steps.ver.outputs.version }} TAG: ${{ steps.ver.outputs.tag }} run: | set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add plugin.json .github/plugin/marketplace.json - git commit -m "chore(release): bump plugin to v$VERSION" + if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists on origin; reusing it." + exit 0 + fi + git tag -a "$TAG" -m "Release $TAG" - git push origin HEAD:${{ github.ref_name }} git push origin "$TAG" - name: Generate release notes @@ -364,102 +386,6 @@ jobs: echo "Publishing $VSIX_FILE to VS Code Marketplace (Release channel)" vsce publish --packagePath "$VSIX_FILE" --no-dependencies - - name: Bump version files and update CHANGELOG.md on main - if: steps.ver.outputs.prerelease == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.ver.outputs.tag }} - VERSION: ${{ steps.ver.outputs.version }} - run: | - set -euo pipefail - - # Always work against the tip of main so the bump + changelog stay - # current, even when this run was triggered by a tag push from an - # older commit. (On tag-push the earlier "Commit version bump" step - # is skipped, so plugin.json / marketplace.json on main would - # otherwise stay at the previous version.) - git fetch origin main - git checkout -B changelog-update origin/main - - # Re-apply the version bump against the fresh main tree so the same - # commit lands the version-bearing files AND the changelog entry. - PLUGIN_JSON="plugin.json" - MARKETPLACE_JSON=".github/plugin/marketplace.json" - PLUGIN_NAME=$(jq -r '.name' "$PLUGIN_JSON") - - jq --arg v "$VERSION" '.version = $v' "$PLUGIN_JSON" > "$PLUGIN_JSON.tmp" - mv "$PLUGIN_JSON.tmp" "$PLUGIN_JSON" - - jq --arg v "$VERSION" --arg name "$PLUGIN_NAME" ' - .metadata.version = $v - | .plugins |= map(if .name == $name then .version = $v else . end) - ' "$MARKETPLACE_JSON" > "$MARKETPLACE_JSON.tmp" - mv "$MARKETPLACE_JSON.tmp" "$MARKETPLACE_JSON" - - DATE=$(date -u +%Y-%m-%d) - - # Strip the heading + install footer from release-notes.md to get just - # the entry body. release-notes.md format: - # ## Git-Ape vX.Y.Z - # - # ## Install - # ... - ENTRY_BODY=$(awk ' - /^## Install$/ { exit } - /^## Git-Ape / { next } - { print } - ' release-notes.md | sed -e 's/[[:space:]]*$//' | awk 'NF || p { p=1; print }') - - NEW_ENTRY=$(printf '## [%s] - %s\n\n%s\n' "$VERSION" "$DATE" "$ENTRY_BODY") - - if [[ -f CHANGELOG.md ]]; then - # Insert new entry below the top-level header, preserving existing content. - awk -v entry="$NEW_ENTRY" ' - BEGIN { inserted = 0 } - { - print - if (!inserted && /^# /) { - print "" - print entry - inserted = 1 - } - } - ' CHANGELOG.md > CHANGELOG.md.tmp - mv CHANGELOG.md.tmp CHANGELOG.md - else - { - echo "# Changelog" - echo - echo "All notable changes to this project are documented here." - echo "This project follows [Semantic Versioning](https://semver.org/)." - echo - echo "$NEW_ENTRY" - } > CHANGELOG.md - fi - - if git diff --quiet CHANGELOG.md plugin.json .github/plugin/marketplace.json; then - echo "No version or changelog drift on main; skipping commit." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add CHANGELOG.md plugin.json .github/plugin/marketplace.json - git commit -m "chore(release): bump to $TAG and update changelog" - - # Push directly to main. If the push fails (someone else moved main), - # fall back to opening a PR so the bump + changelog still land. - if ! git push origin HEAD:main; then - echo "Direct push to main rejected; opening a PR instead." - BRANCH="release/${TAG}" - git push origin "HEAD:$BRANCH" - gh pr create \ - --base main \ - --head "$BRANCH" \ - --title "chore(release): bump to $TAG and update changelog" \ - --body "Automated post-release update for [$TAG](https://github.com/${{ github.repository }}/releases/tag/$TAG): bumps \`plugin.json\` + \`.github/plugin/marketplace.json\` to \`$VERSION\` and appends the changelog entry." - fi - ```