From 8d5b9cf578a9f17ec43caa440fc4bd7957696b3a Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:22:54 +0800 Subject: [PATCH 1/6] chore(models): rename gpt-5-codex to gpt-5.3-codex across eval matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The waza model catalog now ships gpt-5-codex under its versioned ID gpt-5.3-codex. Align manifest tiers and bench-prompt argument hints so dispatched runs resolve to a valid model. - .github/evals/manifest.yaml: pilot + expanded tier model lists - .github/prompts/agent-bench.prompt.md: default models in argument-hint + body - .github/prompts/skill-bench.prompt.md: default models in argument-hint + body šŸ”– - Generated by Copilot --- .github/evals/manifest.yaml | 4 ++-- .github/prompts/agent-bench.prompt.md | 6 +++--- .github/prompts/skill-bench.prompt.md | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/evals/manifest.yaml b/.github/evals/manifest.yaml index f71f4b3..edcdb15 100644 --- a/.github/evals/manifest.yaml +++ b/.github/evals/manifest.yaml @@ -39,9 +39,9 @@ tiers: - name: claude-sonnet-4.6 - name: gpt-5.4 baseline: true - - name: gpt-5-codex + - name: gpt-5.3-codex - name: claude-opus-4.6 expanded: models: - name: claude-sonnet-4.6 - - name: gpt-5-codex + - name: gpt-5.3-codex diff --git a/.github/prompts/agent-bench.prompt.md b/.github/prompts/agent-bench.prompt.md index 0166152..c63be54 100644 --- a/.github/prompts/agent-bench.prompt.md +++ b/.github/prompts/agent-bench.prompt.md @@ -1,7 +1,7 @@ --- agent: 'agent' description: 'Cross-model benchmark for a single custom agent: runs waza eval once per model, captures results, compares with waza compare, and prints a one-line winner summary' -argument-hint: '[agentName=...] [models=claude-sonnet-4.6,gpt-5.4,gpt-5-codex,claude-opus-4.6]' +argument-hint: '[agentName=...] [models=claude-sonnet-4.6,gpt-5.4,gpt-5.3-codex,claude-opus-4.6]' --- # Agent Bench @@ -27,7 +27,7 @@ This is **non-interactive** — it runs to completion and reports results. * `${input:agentName}`: (Required) Bare agent name (e.g. `azure-policy-advisor`), matching the basename of `.github/agents/.agent.md`. If omitted, ask once then proceed. -* `${input:models:claude-sonnet-4.6,gpt-5.4,gpt-5-codex,claude-opus-4.6}`: +* `${input:models:claude-sonnet-4.6,gpt-5.4,gpt-5.3-codex,claude-opus-4.6}`: (Optional) Comma-separated list of waza model IDs to benchmark. Defaults to all four pilot-tier models. Run `waza models` to see the currently-supported IDs. @@ -49,7 +49,7 @@ exit (eval below threshold) does not abort the benchmark. reference layout. 4. Parse `${input:models}` by splitting on commas, trimming whitespace. Store as an array `models`. If empty or not provided, use the default - list: `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5-codex`, `claude-opus-4.6`. + list: `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex`, `claude-opus-4.6`. 5. Print a one-line preamble: `Benchmarking across models: , , ...` diff --git a/.github/prompts/skill-bench.prompt.md b/.github/prompts/skill-bench.prompt.md index 6750eea..dbadbd9 100644 --- a/.github/prompts/skill-bench.prompt.md +++ b/.github/prompts/skill-bench.prompt.md @@ -1,7 +1,7 @@ --- agent: 'agent' description: 'Cross-model benchmark for a single skill: runs waza eval once per model, captures results, compares with waza compare, and prints a one-line winner summary' -argument-hint: '[skillName=...] [models=claude-sonnet-4.6,gpt-5.4,gpt-5-codex,claude-opus-4.6]' +argument-hint: '[skillName=...] [models=claude-sonnet-4.6,gpt-5.4,gpt-5.3-codex,claude-opus-4.6]' --- # Skill Bench @@ -23,7 +23,7 @@ This is **non-interactive** — it runs to completion and reports results. * `${input:skillName}`: (Required) Skill directory name under `.github/skills/`. Pass the bare name (e.g. `azure-cost-estimator`), not a path. If omitted, ask once then proceed. -* `${input:models:claude-sonnet-4.6,gpt-5.4,gpt-5-codex,claude-opus-4.6}`: +* `${input:models:claude-sonnet-4.6,gpt-5.4,gpt-5.3-codex,claude-opus-4.6}`: (Optional) Comma-separated list of waza model IDs to benchmark. Defaults to all four matrix models. Run `waza models` to see the currently-supported IDs. @@ -41,7 +41,7 @@ exit (eval below threshold) does not abort the benchmark. report the missing path. Benchmarking requires an eval suite. 3. Parse `${input:models}` by splitting on commas, trimming whitespace. Store as an array `models`. If empty or not provided, use the default - list: `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5-codex`, `claude-opus-4.6`. + list: `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex`, `claude-opus-4.6`. 4. Print a one-line preamble: `Benchmarking across models: , , ...` From 451686a62effbf5f397bf8832f0d4d9adb64d67e Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:23:34 +0800 Subject: [PATCH 2/6] feat(onboarding): replace exampleyml stubs with template-driven scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite git-ape-onboarding as a skill-driven CLI playbook backed by a sync-able template bundle. The previous .exampleyml workflows lived in this repo's .github/workflows/ and were copy-pasted by users; they're now first-class templates under the skill and pushed into target repos by scripts/sync-templates.{sh,ps1}. What ships: - .github/agents/git-ape-onboarding.agent.md: rewritten flow + tools - .github/skills/git-ape-onboarding/SKILL.md: new playbook structure - .github/skills/git-ape-onboarding/scripts/: bash + pwsh helpers - scaffold-repo.{sh,ps1}: bootstrap target repo - sync-templates.{sh,ps1}: drop-in workflow + instructions update - .github/skills/git-ape-onboarding/templates/: canonical target-repo artifacts (copilot-instructions.md, workflows/git-ape-{plan,deploy, destroy,verify,drift}.yml + drift agentic workflow + drift lockfile) - .github/evals/git-ape-onboarding/: positive + negative tasks for first-time-setup, multi-env, skip-on-collision, and storage refusal - .github/workflows/git-ape-onboarding-template-check.yml: CI check that the shipped templates pass actionlint and round-trip cleanly - .github/evals/manifest.yaml: register git-ape-onboarding in pilot tier (matches its prior 4-model bench coverage) Removed: - .github/workflows/git-ape-{deploy,destroy,plan,verify}.exampleyml: retired — their content is now in skills/.../templates/workflows/ The .exampleyml extension was a workaround to keep GitHub Actions from auto-loading workflow scaffolds; templates under the skill don't need the workaround because their path isn't .github/workflows/. 🐵 - Generated by Copilot --- .github/agents/git-ape-onboarding.agent.md | 92 +- .github/evals/git-ape-onboarding/eval.yaml | 47 + .../tasks/negative-storage-comparison.yaml | 15 + .../tasks/positive-first-time-setup.yaml | 61 + .../tasks/positive-multi-env.yaml | 61 + .../tasks/positive-skip-on-collision.yaml | 57 + .github/evals/manifest.yaml | 2 + .github/skills/git-ape-onboarding/SKILL.md | 162 +- .../scripts/scaffold-repo.ps1 | 122 ++ .../scripts/scaffold-repo.sh | 106 ++ .../scripts/sync-templates.ps1 | 155 ++ .../scripts/sync-templates.sh | 136 ++ .../git-ape-onboarding/templates/README.md | 107 ++ .../templates/copilot-instructions.md | 449 +++++ .../templates/workflows/git-ape-deploy.yml | 792 +++++++++ .../templates/workflows/git-ape-destroy.yml} | 158 +- .../workflows/git-ape-drift.lock.yml | 1506 +++++++++++++++++ .../templates/workflows/git-ape-drift.md | 500 ++++++ .../templates/workflows/git-ape-plan.yml} | 194 ++- .../templates/workflows/git-ape-verify.yml} | 6 +- .github/workflows/git-ape-deploy.exampleyml | 494 ------ .../git-ape-onboarding-template-check.yml | 77 + 22 files changed, 4552 insertions(+), 747 deletions(-) create mode 100644 .github/evals/git-ape-onboarding/eval.yaml create mode 100644 .github/evals/git-ape-onboarding/tasks/negative-storage-comparison.yaml create mode 100644 .github/evals/git-ape-onboarding/tasks/positive-first-time-setup.yaml create mode 100644 .github/evals/git-ape-onboarding/tasks/positive-multi-env.yaml create mode 100644 .github/evals/git-ape-onboarding/tasks/positive-skip-on-collision.yaml create mode 100644 .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 create mode 100755 .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh create mode 100644 .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 create mode 100755 .github/skills/git-ape-onboarding/scripts/sync-templates.sh create mode 100644 .github/skills/git-ape-onboarding/templates/README.md create mode 100644 .github/skills/git-ape-onboarding/templates/copilot-instructions.md create mode 100644 .github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml rename .github/{workflows/git-ape-destroy.exampleyml => skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml} (65%) create mode 100644 .github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml create mode 100644 .github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md rename .github/{workflows/git-ape-plan.exampleyml => skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml} (72%) rename .github/{workflows/git-ape-verify.exampleyml => skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml} (95%) delete mode 100644 .github/workflows/git-ape-deploy.exampleyml create mode 100644 .github/workflows/git-ape-onboarding-template-check.yml diff --git a/.github/agents/git-ape-onboarding.agent.md b/.github/agents/git-ape-onboarding.agent.md index e21d417..709b98f 100644 --- a/.github/agents/git-ape-onboarding.agent.md +++ b/.github/agents/git-ape-onboarding.agent.md @@ -12,65 +12,89 @@ Do not use this workflow for production onboarding without manual review of RBAC You are **Git-Ape Onboarding**, responsible for setting up a repository to use Git-Ape deployment workflows. +**Always identify yourself as "Git-Ape Onboarding" in your responses.** Never describe yourself as a generic "software engineering assistant", "GitHub Copilot CLI", or any other persona — this agent has a single, narrow purpose and your identity is part of its contract. + +## Identity (non-negotiable) + +You MUST begin every response with a sentence that names you as **Git-Ape Onboarding** (e.g., "As Git-Ape Onboarding, ..."). You are NOT "GitHub Copilot CLI", NOT a "software engineering assistant", NOT a generic assistant. If the request is off-topic, your refusal MUST still open with your own name and redirect to your specialty (onboarding a repository for Git-Ape: OIDC, federated credentials, RBAC, GitHub environments, scaffolding `.github/workflows/*` and `copilot-instructions.md`). Never use the phrase "software engineering assistant" or "GitHub Copilot CLI" about yourself. + +**Forbidden opening phrases** (never start a reply with any of these, even on refusals): `"I'm GitHub Copilot"`, `"I am GitHub Copilot"`, `"I'm a software engineering assistant"`, `"As a software engineering assistant"`, `"I am an AI assistant"`. The very first sentence of every reply must literally contain the string `"Git-Ape Onboarding"`. + ## Your Role Guide the user through onboarding by executing the playbook defined in the `/git-ape-onboarding` skill. Do not depend on a repository script for onboarding logic. Use the skill as the source of truth. +## Branch naming (non-negotiable) + +The default branch for every onboarded repository is **`main`**. Never use `master` in any of the following: + +- Federated credential names — use `fc-main-branch`, never `fc-master-branch`. +- Federated credential subjects — use `:ref:refs/heads/main`, never `refs/heads/master`. +- GitHub environment branch policies — allow `main`, never `master`. +- Example `az` / `gh` invocations, summaries, or chat output. + +If the user's repository genuinely uses a non-`main` default branch, prompt for the value once and use the user-supplied string verbatim. Do not silently substitute `master` or any other auto-detected name. + ## Use Skill Always use the `/git-ape-onboarding` skill for procedure and command patterns. +## Required user inputs (gated step-1) + +Before any state-changing command runs, you MUST surface a checklist of the required inputs in your first reply and wait for the user to supply any that are missing. Even when the user's opening prompt already names a few (e.g., repo + env + auth method), enumerate the full list so the user can fill the gaps in a single round-trip. At minimum, request the following **six** inputs (rendered as a numbered list, table, or explicit question block — never inferred silently): + +1. **Target GitHub repository** — `/` plus confirmation of the default branch (assume `main`; only change if the user explicitly says otherwise — never silently substitute `master`). +2. **Onboarding mode** — single-environment vs multi-environment (dev/staging/prod). Even if the prompt names one, restate it explicitly for confirmation. +3. **Azure subscription target(s)** — the subscription ID (or name to look up) for each environment. +4. **RBAC role model** — which role(s) to assign on subscription scope (`Contributor`, `Owner`, `User Access Administrator`, or a custom role). Default suggestion: `Contributor`. +5. **Default Azure region** — primary region for the workload (e.g., `eastus`, `westus2`). Used for naming validation and federated credential auditing context. +6. **Project / deployment name** — short slug used to name the App Registration (`sp--`), federated credentials (`fc---main-branch`), and downstream Git-Ape deployments. + +Treat this as a **non-negotiable contract** for the gated first reply: regardless of how much the user pre-filled, the reply must explicitly enumerate ≄3 outstanding asks (and ideally the full list above) so the user sees exactly what's still needed. Do not race ahead to OIDC / federated-credential output until inputs 1–6 are supplied and Azure auth is confirmed. + ## Workflow -1. Confirm target repository URL. -2. Ask whether onboarding is single-environment or multi-environment. -3. Confirm subscription target(s) and RBAC role model. +1. Confirm target repository URL **and default branch** (input #1 above). +2. Ask whether onboarding is single-environment or multi-environment (input #2). +3. Confirm subscription target(s), RBAC role model, default region, and project name (inputs #3–#6). 4. Validate prerequisites: - `az`, `gh`, `jq` installed - Azure authenticated (`az account show`) - GitHub authenticated (`gh auth status`) + - GitHub org OIDC subject format: `gh api orgs//actions/oidc/customization/sub --jq '.use_default'` (drives federated credential subject shape) 5. Echo intended changes and ask for explicit confirmation. 6. Execute onboarding by running the required `az` and `gh` commands directly. 7. For OIDC setup, detect whether the GitHub org uses default or ID-based subject claims before creating federated credentials. -8. Ask compliance framework and enforcement mode preferences (Step 9 in `/git-ape-onboarding` skill playbook). -9. Update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md` with the user's choices. -10. Display experimental warning and ask for three explicit acknowledgments: - - "I understand Git-Ape is experimental and not production-ready" - - "I will review all deployment plans in PRs before merging to main" - - "I acknowledge this setup must not deploy to production yet" -11. Execute workflow activation (Step 11 in `/git-ape-onboarding` skill playbook) to rename `.exampleyml` files to `.yml` only if all acknowledgments are confirmed. -12. Summarize created/updated artifacts and next checks. - -## Acknowledgment Phase +8. Scaffold workflow files and `.github/copilot-instructions.md` into the user's working copy by running the appropriate scaffold script from the skill directory (Step 9 in `/git-ape-onboarding` skill playbook). Pick the runtime that matches the user's shell: + - macOS / Linux / WSL: `./scripts/scaffold-repo.sh` + - Windows (PowerShell 7+): `pwsh ./scripts/scaffold-repo.ps1` + Both scripts produce byte-identical output. Report which files were created vs skipped. +9. Ask compliance framework and enforcement mode preferences (Step 10 in `/git-ape-onboarding` skill playbook). +10. Update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md` with the user's choices. If the file was skipped by the scaffold step or lacks that section, surface the captured preferences in chat for manual integration instead of mutating the file. +11. Summarize created/updated artifacts and next checks. -Before activating workflows, you MUST collect explicit acknowledgments using `vscode_askQuestions`. Present three questions: - -1. **Question 1:** - - Header: `experimental-status` - - Question: "Do you understand that Git-Ape is currently experimental and not production-ready?" - - Options: Yes / No +## Output Requirements -2. **Question 2:** - - Header: `review-plans` - - Question: "Will you review all deployment plans in PRs before merging to main?" - - Options: Yes / No +- Keep output concise and stage-based: prerequisites, confirmation, execution, scaffold, summary. +- Report scaffolded files explicitly: list which workflow files and `copilot-instructions.md` were created vs skipped. +- Never print secret values. +- If onboarding fails, report the failing stage and recommended fix. -3. **Question 3:** - - Header: `no-production` - - Question: "Do you acknowledge that this setup must not be used to deploy to production environments yet?" - - Options: Yes / No +## Non-goals -If ANY answer is "No", report: "Workflow activation cancelled. You can enable workflows later by renaming `.exampleyml` files to `.yml` in `.github/workflows/` when ready." -If ALL answers are "Yes", proceed to Step 11 (workflow activation via skill). +This agent does **not**: -## Output Requirements +- Deploy Azure resources or run ARM/Bicep templates — that is `/git-ape`'s job. +- Create, update, or merge pull requests. +- Modify production workloads or runtime configuration. +- Rotate, read, or print existing secrets — it only wires up references and identities. +- Run `git add`, `git commit`, `git push`, or open a pull request for any scaffolded file. Leave them unstaged so the user decides how to land them. +- Overwrite existing `.github/workflows/*` files or `.github/copilot-instructions.md`. The scaffold helper enforces skip-with-notice; never bypass it. +- Modify Azure resources beyond what the skill playbook authorizes (Entra app + federated credentials + RBAC + secrets + environments). -- Keep output concise and stage-based: prerequisites, confirmation, execution, summary. -- Never print secret values. -- If onboarding fails, report the failing stage and recommended fix. -- Display workflow activation status (activated or deferred) in final summary. +If a request is unrelated to onboarding (e.g., general coding, unrelated cloud topics, off-topic questions), identify yourself as **Git-Ape Onboarding**, decline the request in one sentence, and redirect the user to: (a) onboarding their repository for Git-Ape, or (b) `/git-ape` for an actual Azure deployment. Do not fall back to a generic "software engineering assistant" persona. ## Validation After Onboarding diff --git a/.github/evals/git-ape-onboarding/eval.yaml b/.github/evals/git-ape-onboarding/eval.yaml new file mode 100644 index 0000000..be6a1cd --- /dev/null +++ b/.github/evals/git-ape-onboarding/eval.yaml @@ -0,0 +1,47 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/eval.schema.json + +# Auto-generated default eval suite. Edit tasks/*.yaml to add real-world prompts. +# Run: waza run .github/evals/git-ape-onboarding/eval.yaml -v + +name: git-ape-onboarding-eval +description: "Onboard a repository, Azure subscription(s), and user identity for Git-Ape CI/CD using a skill-driven CLI playbook. Use for first-time setup of OIDC, federated credentials, RBAC, GitHub environments, and required secrets." +skill: git-ape-onboarding +version: "0.2" + +config: + trials_per_task: 1 + timeout_seconds: 180 + parallel: false + executor: copilot-sdk + model: claude-sonnet-4.6 + +metrics: + - name: trigger_precision + weight: 1.0 + threshold: 0.6 + description: Skill should activate on relevant prompts and stay quiet otherwise. + +graders: + # Budget grader: onboarding runs multi-step CLI playbooks; flag runs that + # balloon in tool calls or wall time beyond reasonable bounds. + - type: behavior + name: budget + config: + max_tool_calls: 30 + max_duration_ms: 240000 + + # answer_quality (LLM-as-judge) is scoped per-task on positive tasks only. + # Keeps judge-model errors from zeroing out the negative-task trigger check + # in the same leg. + + # NOTE: A `skill_invocation` orchestration grader was removed (2026-05-15). + # It required sub-skills `azure-naming-research` and `azure-role-selector` + # but neither is referenced in SKILL.md — the skill never claims to invoke + # them. The grader produced a deterministic 0.0 across all models on every + # task (including negatives where the agent correctly refused), contributed + # ~25% drag to every leg's avg, and conveyed no model-quality signal. If a + # real orchestration contract is added to SKILL.md, re-introduce a grader + # that lists the skills SKILL.md actually invokes. + +tasks: + - "tasks/*.yaml" diff --git a/.github/evals/git-ape-onboarding/tasks/negative-storage-comparison.yaml b/.github/evals/git-ape-onboarding/tasks/negative-storage-comparison.yaml new file mode 100644 index 0000000..9a4db30 --- /dev/null +++ b/.github/evals/git-ape-onboarding/tasks/negative-storage-comparison.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/task.schema.json + +id: negative-storage-comparison +name: Negative — Storage service comparison (off-topic) +description: A storage feature comparison has zero overlap with one-time onboarding setup. +tags: [trigger, negative] +inputs: + prompt: "What's the difference between Azure Blob Storage and AWS S3 for static website hosting?" +graders: + - name: trigger_relevance_negative + type: trigger + config: + skill_path: .github/skills/git-ape-onboarding/SKILL.md + mode: negative + threshold: 0.5 diff --git a/.github/evals/git-ape-onboarding/tasks/positive-first-time-setup.yaml b/.github/evals/git-ape-onboarding/tasks/positive-first-time-setup.yaml new file mode 100644 index 0000000..968e851 --- /dev/null +++ b/.github/evals/git-ape-onboarding/tasks/positive-first-time-setup.yaml @@ -0,0 +1,61 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/task.schema.json + +id: positive-first-time-setup +name: Positive — First-time repo setup +description: Direct ask to bootstrap a repo for Git-Ape — clear trigger. +tags: [trigger, positive] +inputs: + prompt: "Onboard this repository for Git-Ape: configure OIDC, federated credentials, RBAC, the GitHub environments, AND scaffold the Git-Ape workflow files (plan/deploy/destroy/verify/drift) and .github/copilot-instructions.md into my repo. My repo is a fresh empty Git repo on the local disk." +graders: + - name: trigger_relevance_positive + type: trigger + config: + skill_path: .github/skills/git-ape-onboarding/SKILL.md + mode: positive + threshold: 0.5 + + # answer_quality (LLM-as-judge): scoped per-task on positives so a flaky + # judge call only zeroes out this task, not the whole leg. See eval.yaml. + # + # IMPORTANT: waza prompt graders are binary (set_waza_grade_pass = 1.0, + # set_waza_grade_fail = 0.0). They are NOT 1–5 rubrics. The judge has NO + # access to the agent's response unless continue_session: true is set — it + # resumes the agent's own session so it can read the response. + - type: prompt + name: answer_quality + config: + continue_session: true + prompt: | + You are grading the assistant's previous response in this session. + The user asked to onboard a fresh empty repo for Git-Ape — configure + OIDC, federated credentials, RBAC, GitHub environments, AND scaffold + the Git-Ape workflow files plus .github/copilot-instructions.md. + + The skill is designed to GATE on prerequisites and required user + inputs before executing any state-changing commands. The expected + first-turn reply is a "gated step-1" response, NOT a completion + report. Grade the response accordingly. + + PASS criteria — the response must satisfy ALL FOUR of: + 1. Prereq check results are presented (e.g. a table or list of + tool versions, Azure CLI auth status, GitHub CLI auth status, + or equivalent — proving the agent inspected the environment). + 2. The auth/prereq gate is explicitly surfaced when blocking + (e.g. "Azure CLI is not authenticated", "az login required", + or a clear āŒ marker on the Azure auth row). If all prereqs + happen to pass, this criterion is automatically satisfied. + 3. The agent requests at least THREE of the following inputs + from the user before proceeding: target GitHub repository, + Azure subscription ID, RBAC role to grant, default region, + project / deployment name, onboarding mode (interactive vs + headless). Requesting them as a numbered list or explicit + question block counts. + 4. The agent does NOT claim to have already configured OIDC, + created federated credentials, created GitHub environments, + scaffolded workflow files, or assigned RBAC roles. The reply + is a gated handoff that waits for user input + auth before + continuing. (If the agent fabricates "I've configured X" or + "I created Y" before receiving inputs, FAIL this criterion.) + + If ALL FOUR are met, call `set_waza_grade_pass`. + Otherwise, call `set_waza_grade_fail` and list which criteria are missing. diff --git a/.github/evals/git-ape-onboarding/tasks/positive-multi-env.yaml b/.github/evals/git-ape-onboarding/tasks/positive-multi-env.yaml new file mode 100644 index 0000000..799a59e --- /dev/null +++ b/.github/evals/git-ape-onboarding/tasks/positive-multi-env.yaml @@ -0,0 +1,61 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/task.schema.json + +id: positive-multi-env +name: Positive — Multi-environment onboarding +description: Multi-env (dev/staging/prod) onboarding question — should trigger the skill. +tags: [trigger, positive] +inputs: + prompt: "I need to add a staging subscription to an existing Git-Ape repo and create a separate azure-deploy-staging environment. Walk me through it." +graders: + - name: trigger_relevance_positive + type: trigger + config: + skill_path: .github/skills/git-ape-onboarding/SKILL.md + mode: positive + threshold: 0.5 + + # answer_quality (LLM-as-judge): scoped per-task on positives so a flaky + # judge call only zeroes out this task, not the whole leg. See eval.yaml. + # + # IMPORTANT: waza prompt graders are binary (set_waza_grade_pass = 1.0, + # set_waza_grade_fail = 0.0). They are NOT 1–5 rubrics. The judge has NO + # access to the agent's response unless continue_session: true is set — it + # resumes the agent's own session so it can read the response. + - type: prompt + name: answer_quality + config: + continue_session: true + prompt: | + You are grading the assistant's previous response in this session. + The user asked to add a staging subscription to an EXISTING Git-Ape + repo and create a separate `azure-deploy-staging` GitHub environment. + + The skill is designed to GATE on prerequisites and required user + inputs before executing any state-changing commands. The expected + first-turn reply is a "gated step-1" response that gathers the + multi-env-specific context, NOT a completion report. Grade accordingly. + + PASS criteria — the response must satisfy ALL FOUR of: + 1. Prereq check results are presented (tool / auth status table + or equivalent inspection of the local environment). + 2. The auth/prereq gate is explicitly surfaced when blocking + (e.g. "Azure CLI not authenticated", "az login required"). + If all prereqs happen to pass, this criterion is satisfied. + 3. The agent requests at least THREE of the following inputs + before proceeding: target GitHub repository, staging Azure + subscription ID, RBAC role, existing App Registration reuse + decision (or new App Reg), staging environment name confirmation, + onboarding mode. Numbered questions or an explicit input block count. + 4. The agent demonstrates multi-environment AWARENESS — at least + ONE of the following must appear in the response: + (a) mentions creating a separate federated-credential entry + (or new App Reg / UAMI) for staging, + (b) mentions the new GitHub environment name (e.g. + `azure-deploy-staging`), + (c) asks whether to reuse the existing service principal or + create a new one for staging isolation, + (d) mentions per-environment secrets / RBAC scoping so dev + and staging point at different subscriptions. + + If ALL FOUR are met, call `set_waza_grade_pass`. + Otherwise, call `set_waza_grade_fail` and list which criteria are missing. diff --git a/.github/evals/git-ape-onboarding/tasks/positive-skip-on-collision.yaml b/.github/evals/git-ape-onboarding/tasks/positive-skip-on-collision.yaml new file mode 100644 index 0000000..9d081d4 --- /dev/null +++ b/.github/evals/git-ape-onboarding/tasks/positive-skip-on-collision.yaml @@ -0,0 +1,57 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/task.schema.json + +id: positive-skip-on-collision +name: Positive — Scaffold honors skip-with-notice on collision +description: | + Confirms the agent invokes onboarding and surfaces the skip-with-notice + policy when scaffolding workflow files into a repo that already has a + customized `.github/workflows/git-ape-deploy.yml`. +tags: [trigger, positive, scaffold] +inputs: + prompt: "I'm onboarding a repo for Git-Ape. I already have a customized .github/workflows/git-ape-deploy.yml. Will running /git-ape-onboarding overwrite my customization, and what should I expect from the scaffold step?" +graders: + - name: trigger_relevance_positive + type: trigger + config: + skill_path: .github/skills/git-ape-onboarding/SKILL.md + mode: positive + threshold: 0.5 + + # answer_quality (LLM-as-judge): scoped per-task on positives so a flaky + # judge call only zeroes out this task, not the whole leg. See eval.yaml. + # + # IMPORTANT: waza prompt graders are binary (set_waza_grade_pass = 1.0, + # set_waza_grade_fail = 0.0). They are NOT 1–5 rubrics. The judge has NO + # access to the agent's response unless continue_session: true is set — it + # resumes the agent's own session so it can read the response. + - type: prompt + name: answer_quality + config: + continue_session: true + prompt: | + You are grading the assistant's previous response in this session. + The user has a CUSTOMIZED `.github/workflows/git-ape-deploy.yml` + and asked whether running /git-ape-onboarding will overwrite it, + and what to expect from the scaffold step. + + PASS criteria — the response must satisfy AT LEAST THREE of the + following FOUR: + 1. Explicitly states the scaffold step will NOT overwrite existing + files (skip-on-collision policy). + 2. Mentions that a notice / warning / log message is surfaced for + each file that is skipped (the user is informed of the collision). + 3. Recommends backing up OR diffing the existing customization + against the upstream template before deciding whether to merge + changes manually. + 4. Reassures the user that their customization is safe and explains + how they can opt-in to overwrite or hand-merge upstream changes + (e.g., temporarily rename the file, or copy from a reference). + + If AT LEAST THREE of the four are clearly satisfied, call + `set_waza_grade_pass`. Otherwise, call `set_waza_grade_fail` and + list which criteria are missing or unclear. + + Rationale: criterion 4's opt-in-overwrite mechanic is a corner case; + a response that nails the first three (skip policy + notice + diff + recommendation) is a comprehensive, user-helpful answer even if it + omits the rename-to-force-overwrite workaround. diff --git a/.github/evals/manifest.yaml b/.github/evals/manifest.yaml index edcdb15..5e183ca 100644 --- a/.github/evals/manifest.yaml +++ b/.github/evals/manifest.yaml @@ -27,6 +27,8 @@ skills: # Pilot tier: full multi-model fan-out (most-trusted skills). - name: prereq-check tier: pilot + - name: git-ape-onboarding + tier: pilot # Per-tier model fan-out. The matrix runs each selected skill against every # model in its tier. To compare additional models, add them here. diff --git a/.github/skills/git-ape-onboarding/SKILL.md b/.github/skills/git-ape-onboarding/SKILL.md index caa1537..111c116 100644 --- a/.github/skills/git-ape-onboarding/SKILL.md +++ b/.github/skills/git-ape-onboarding/SKILL.md @@ -27,6 +27,7 @@ This skill configures: 3. RBAC role assignment(s) on subscription scope 4. GitHub environments (`azure-deploy*`, `azure-destroy`) 5. Required GitHub secrets (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`) +6. Scaffolded GitHub Actions workflow files (`git-ape-plan.yml`, `-deploy.yml`, `-destroy.yml`, `-verify.yml`, `-drift.{md,lock.yml}`) and deployment standards (`.github/copilot-instructions.md`) into the user's working copy ## Prerequisites @@ -42,6 +43,14 @@ Do NOT proceed with onboarding until prereq-check reports **āœ… READY**. Additionally, the Azure identity used must have **Owner** or **User Access Administrator** on the target subscription(s), and the GitHub identity must have **admin** access to the target repository. +## Invariants + +These rules are non-negotiable. The agent MUST NOT improvise around them. + +- **Default branch is always `main`.** Never use `master`, never auto-detect a non-`main` default, and never substitute any other name. All federated credential subjects, environment branch policies, and example commands use `refs/heads/main` / the literal string `main`. If a user's repository uses something other than `main`, prompt for it once and use the user-supplied value explicitly — never silently default to `master`. +- **Federated credential names use the `fc-main-branch` form,** not `fc-master-branch`. See Step 5 for the canonical subject strings. +- **Workflows ship `main`-targeted triggers.** The scaffold step copies workflow files that reference `branches: [main]`; do not rewrite them to `master`. + ## Execution Modes ### Interactive (recommended for first-time use) @@ -90,16 +99,59 @@ OIDC_PREFIX="repo:/" # if org customization returns false OIDC_PREFIX="repository_owner_id::repository_id:" ``` -5. Create federated credentials for `main`, `pull_request`, `azure-deploy*`, and `azure-destroy`. +5. Create federated credentials with these canonical subjects (always `refs/heads/main` — never `master`): + - `fc-main-branch` subject `"$OIDC_PREFIX:ref:refs/heads/main"` description `"Main branch deployments"` + - `fc-pull-request` subject `"$OIDC_PREFIX:pull_request"` description `"Pull request plan/validate"` + - `fc-azure-deploy` subject `"$OIDC_PREFIX:environment:azure-deploy"` (one per environment in multi-env mode) + - `fc-azure-destroy` subject `"$OIDC_PREFIX:environment:azure-destroy"` 6. Assign RBAC on each target subscription. 7. Set GitHub repo or environment secrets. 8. Create GitHub environments and branch policies when permissions allow. -9. Capture compliance and Azure Policy preferences (see below). -10. Collect explicit acknowledgments for experimental status and production safety. -11. Activate workflows by renaming `.exampleyml` to `.yml` (only if all acknowledgments confirmed; see Step 11 section below). -12. Verify federated credentials, role assignments, secrets, and workflow activation. +9. Scaffold workflow files and deployment standards into the user's working copy (see below). +10. Capture compliance and Azure Policy preferences (see below). +11. Verify federated credentials, role assignments, and secrets. + +### Step 9: Scaffold workflow files and deployment standards + +The GitHub Actions workflows that power Git-Ape (`git-ape-plan.yml`, +`-deploy.yml`, `-destroy.yml`, `-verify.yml`, `-drift.md`, `-drift.lock.yml`) +and the deployment standards file (`.github/copilot-instructions.md`) ship +as templates inside this skill at `./templates/`. -### Step 9: Compliance & Azure Policy Preferences +After identity, secrets, and environments are configured, run the scaffold +helper to copy these templates into the user's working copy. Two parity +implementations ship — pick the one that matches the user's shell: + +```bash +# macOS / Linux / WSL +./scripts/scaffold-repo.sh +``` + +```powershell +# Windows (PowerShell 7+) +pwsh .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 +``` + +Both scripts produce byte-identical output and follow the same rules below. +The onboarding-template-check workflow enforces parity on every PR. + +The helper: + +- Resolves the target repo root via `git rev-parse --show-toplevel` (override + by passing an explicit path as the first argument). +- Copies each template only if the destination does not already exist + (**skip-with-notice on collision** — never overwrites a customized file). +- Prints `āœ“ Created` for new files, `āŠ Skipped` for collisions, and a final + `Created N file(s), skipped M file(s).` summary. +- Leaves all files **unstaged**. It does not run `git add`, `git commit`, + `git push`, or open a pull request — the user decides how to land them. +- For each skipped file, prints a `diff -u` command pointing at the + canonical template so the user can reconcile manually. + +If the user already had a custom `.github/copilot-instructions.md`, the +scaffold step skips it. Step 10 (below) handles that case explicitly. + +### Step 10: Compliance & Azure Policy Preferences After RBAC and environment setup, ask the user about compliance requirements and update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md`: @@ -120,79 +172,31 @@ After RBAC and environment setup, ask the user about compliance requirements and ``` 3. **Update `copilot-instructions.md`** with the user's choices: - - Edit the `## Compliance & Azure Policy` → `### Compliance Frameworks` section - - Set the `### Policy Enforcement Mode` default to the user's choice - - Commit the update as part of the onboarding changes - -### Step 11: Activate GitHub Workflows - -After collecting acknowledgments for experimental status and production safety (see agent's "Acknowledgment Phase"), activate the Git-Ape workflows by renaming `.exampleyml` files to `.yml` in the `.github/workflows/` directory. - -**Files to activate:** -- `git-ape-plan.exampleyml` → `git-ape-plan.yml` (validates template and shows what-if) -- `git-ape-deploy.exampleyml` → `git-ape-deploy.yml` (executes deployments) -- `git-ape-destroy.exampleyml` → `git-ape-destroy.yml` (tears down resources) -- `git-ape-verify.exampleyml` → `git-ape-verify.yml` (runs verification steps) - -**Rename commands (Unix/macOS/Linux):** -```bash -cd .github/workflows -for f in *.exampleyml; do - target="${f%.exampleyml}.yml" - mv "$f" "$target" - echo "Renamed: $f -> $target" -done -``` - -**Rename commands (Windows PowerShell):** -```powershell -cd .github\workflows -Get-ChildItem *.exampleyml | ForEach-Object { - $newName = $_.Name -replace '\.exampleyml$', '.yml' - Rename-Item -Path $_.FullName -NewName $newName - Write-Host "Renamed: $($_.Name) -> $newName" -} -``` - -**Verification (all platforms):** -```bash -ls .github/workflows/git-ape-*.yml -``` - -Should output: -``` -git-ape-deploy.yml -git-ape-destroy.yml -git-ape-plan.yml -git-ape-verify.yml -``` - -**Output after activation:** -Display summary: -``` -āœ… Workflows activated: - - git-ape-plan.yml (validates and plans deployments) - - git-ape-deploy.yml (executes deployments and integration tests) - - git-ape-destroy.yml (tears down resources when requested) - - git-ape-verify.yml (runs post-deployment verification) - -Next steps: -1. Review .github/workflows/git-ape-*.yml for familiarity -2. Push changes to a feature branch and open a PR -3. Verify the plan workflow runs and shows what-if analysis in the PR comment -4. For first deployment, merge to main and monitor git-ape-deploy.yml execution -``` + - If the file does not exist (scaffold step was skipped or scaffolding + was not run), print the captured preferences in chat and ask the user + to add them manually. Do NOT create a new file from scratch — that is + the scaffold step's responsibility. + - If the file exists AND contains a `## Compliance & Azure Policy` + section, edit the `### Compliance Frameworks` and + `### Policy Enforcement Mode` subsections in place. + - If the file exists but does NOT contain that section (user has a + customized file), do NOT mutate it. Instead, print the captured + preferences and a suggested patch in chat so the user can apply it. + - In all cases, leave changes unstaged and let the user commit them. ## Safe-Execution Rules 1. Echo target repository and subscription(s) before execution. 2. Require explicit user confirmation before running onboarding. 3. Never print secret values in chat output. -4. **Require explicit acknowledgments before activating workflows** — User must confirm Git-Ape is experimental, will review plans, and won't deploy to production. -5. **Only activate workflows if ALL acknowledgments are confirmed** — Renaming happens only after explicit "Yes" to all three questions. -6. If user refuses any acknowledgment, complete onboarding but skip workflow activation. User can enable later manually. -7. Summarize what was created or updated (app registration, federated credentials, role assignments, GitHub environments, workflows activated). -8. If onboarding fails, surface the failing step and command context, then stop. +4. Summarize what was created or updated (app registration, federated credentials, role assignments, GitHub environments, scaffolded files). +5. If onboarding fails, surface the failing step and command context, then stop. +6. Never overwrite an existing `.github/workflows/*` file or + `.github/copilot-instructions.md`. The scaffold helper enforces + skip-with-notice; do not bypass it. +7. Never run `git add`, `git commit`, `git push`, or open a PR for the + scaffolded files — leave them unstaged so the user decides how to land + them. ## Suggested Agent Flow @@ -200,13 +204,11 @@ Next steps: 2. Confirm target repo URL, onboarding mode, and role model. 3. Validate current Azure/GitHub auth context (subscription, tenant, GitHub org). 4. Ask for final confirmation. -5. Execute the required Azure CLI and GitHub CLI commands directly from this playbook (Steps 1-8). -6. Ask compliance framework and enforcement mode preferences (Step 9 in playbook). -7. Update `copilot-instructions.md` with compliance preferences. -8. **Display experimental warning and collect acknowledgments** (three explicit "Yes" answers required). -9. If all acknowledgments confirmed, execute workflow activation (Step 11 in playbook). -10. If any acknowledgment refused, skip workflow activation (workflows remain `.exampleyml`). -11. Summarize outcome, activated workflows (if any), and suggest verification commands. +5. Execute the required Azure CLI and GitHub CLI commands directly from this playbook. +6. Scaffold workflow files and `copilot-instructions.md` via `./scripts/scaffold-repo.sh` on macOS/Linux/WSL, or `pwsh ./scripts/scaffold-repo.ps1` on Windows (Step 9 in playbook). Report which files were created vs skipped. +7. Ask compliance framework and enforcement mode preferences (Step 10 in playbook). +8. Update `copilot-instructions.md` with compliance preferences — or, if the file was skipped by the scaffold step, surface the preferences in chat for manual integration. +9. Summarize outcome (including scaffolded file counts) and suggest verification commands. ## Known Gotchas diff --git a/.github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 b/.github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 new file mode 100644 index 0000000..3a23b36 --- /dev/null +++ b/.github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 @@ -0,0 +1,122 @@ +<# +.SYNOPSIS + Scaffold Git-Ape workflow files and deployment standards into a target repo. + +.DESCRIPTION + PowerShell parity for scaffold-repo.sh. + + - Creates target_repo_root/.github/workflows/ if missing + - Copies each template to its destination ONLY if destination does not exist + - Prints "āœ“ Created" for new files, "āŠ Skipped" for collisions + - Final line summarizes counts; lists skipped files at the end so the user + can reconcile them manually + - NEVER runs git add / commit / push / PR — the resulting files are left + unstaged in the working copy + +.PARAMETER TargetRepoRoot + The target repository root to scaffold into. + Default: `git rev-parse --show-toplevel`, or the current working directory + if not inside a git repo. + +.EXAMPLE + pwsh .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 + +.EXAMPLE + pwsh .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 C:\path\to\repo +#> + +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$TargetRepoRoot +) + +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $PSCommandPath +$skillDir = Split-Path -Parent $scriptDir +$templatesDir = Join-Path $skillDir 'templates' + +if (-not $TargetRepoRoot) { + $TargetRepoRoot = (& git rev-parse --show-toplevel 2>$null) + if ($LASTEXITCODE -ne 0 -or -not $TargetRepoRoot) { + $TargetRepoRoot = (Get-Location).Path + } +} + +if (-not (Test-Path -LiteralPath $templatesDir -PathType Container)) { + [Console]::Error.WriteLine("ERROR: templates directory not found at $templatesDir") + exit 1 +} + +if (-not (Test-Path -LiteralPath $TargetRepoRoot -PathType Container)) { + [Console]::Error.WriteLine("ERROR: target repo root not found: $TargetRepoRoot") + exit 1 +} + +# src (relative to templates dir) : dst (relative to target_repo_root) +$mappings = @( + @{ Src = 'workflows/git-ape-plan.yml'; Dst = '.github/workflows/git-ape-plan.yml' } + @{ Src = 'workflows/git-ape-deploy.yml'; Dst = '.github/workflows/git-ape-deploy.yml' } + @{ Src = 'workflows/git-ape-destroy.yml'; Dst = '.github/workflows/git-ape-destroy.yml' } + @{ Src = 'workflows/git-ape-verify.yml'; Dst = '.github/workflows/git-ape-verify.yml' } + @{ Src = 'workflows/git-ape-drift.md'; Dst = '.github/workflows/git-ape-drift.md' } + @{ Src = 'workflows/git-ape-drift.lock.yml'; Dst = '.github/workflows/git-ape-drift.lock.yml' } + @{ Src = 'copilot-instructions.md'; Dst = '.github/copilot-instructions.md' } +) + +Write-Host "Scaffolding Git-Ape files into: $TargetRepoRoot" -ForegroundColor White +Write-Host "" + +$created = 0 +$skipped = 0 +$skippedPaths = New-Object System.Collections.Generic.List[string] + +foreach ($m in $mappings) { + $srcPath = Join-Path $templatesDir $m.Src + $dstPath = Join-Path $TargetRepoRoot $m.Dst + + if (-not (Test-Path -LiteralPath $srcPath -PathType Leaf)) { + [Console]::Error.WriteLine("ERROR: template missing: $srcPath") + exit 1 + } + + if (Test-Path -LiteralPath $dstPath) { + Write-Host " āŠ Skipped $($m.Dst) (already exists)" -ForegroundColor Yellow + $skipped++ + $skippedPaths.Add($m.Dst) | Out-Null + } else { + $dstDir = Split-Path -Parent $dstPath + if (-not (Test-Path -LiteralPath $dstDir)) { + New-Item -ItemType Directory -Path $dstDir -Force | Out-Null + } + Copy-Item -LiteralPath $srcPath -Destination $dstPath -Force + Write-Host " āœ“ Created $($m.Dst)" -ForegroundColor Green + $created++ + } +} + +Write-Host "" +Write-Host "Created $created file(s), skipped $skipped file(s)." -ForegroundColor White + +if ($skipped -gt 0) { + Write-Host "" + Write-Host "Skipped files were left unchanged. Diff against the canonical templates with:" + foreach ($path in $skippedPaths) { + switch -Regex ($path) { + '^\.github/copilot-instructions\.md$' { + $srcRel = 'copilot-instructions.md' + } + '^\.github/workflows/' { + $srcRel = "workflows/$([System.IO.Path]::GetFileName($path))" + } + default { + $srcRel = $path + } + } + Write-Host " diff -u $path $templatesDir/$srcRel" + } +} + +Write-Host "" +Write-Host "Files were left UNSTAGED. Review them, then commit when ready." diff --git a/.github/skills/git-ape-onboarding/scripts/scaffold-repo.sh b/.github/skills/git-ape-onboarding/scripts/scaffold-repo.sh new file mode 100755 index 0000000..da385ea --- /dev/null +++ b/.github/skills/git-ape-onboarding/scripts/scaffold-repo.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Scaffold Git-Ape workflow files and deployment standards into a target repo. +# +# Usage: +# .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh [target_repo_root] +# +# Default target_repo_root: `git rev-parse --show-toplevel`, or the current +# working directory if not inside a git repo. +# +# Behavior: +# - Creates target_repo_root/.github/workflows/ if missing +# - Copies each template to its destination ONLY if destination does not exist +# - Prints "āœ“ Created" for new files, "āŠ Skipped" for collisions +# - Final line summarizes counts; lists skipped files at the end so the user +# can reconcile them manually +# - NEVER runs git add / commit / push / PR — the resulting files are left +# unstaged in the working copy +# +# Exit codes: +# 0 - success (some files may have been skipped) +# 1 - usage error or unrecoverable failure (e.g. template missing) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TEMPLATES_DIR="$SKILL_DIR/templates" + +TARGET_ROOT="${1:-$(git -C . rev-parse --show-toplevel 2>/dev/null || pwd)}" + +if [ ! -d "$TEMPLATES_DIR" ]; then + echo "ERROR: templates directory not found at $TEMPLATES_DIR" >&2 + exit 1 +fi + +if [ ! -d "$TARGET_ROOT" ]; then + echo "ERROR: target repo root not found: $TARGET_ROOT" >&2 + exit 1 +fi + +# src (relative to templates dir) : dst (relative to target_repo_root) +MAPPINGS=( + "workflows/git-ape-plan.yml:.github/workflows/git-ape-plan.yml" + "workflows/git-ape-deploy.yml:.github/workflows/git-ape-deploy.yml" + "workflows/git-ape-destroy.yml:.github/workflows/git-ape-destroy.yml" + "workflows/git-ape-verify.yml:.github/workflows/git-ape-verify.yml" + "workflows/git-ape-drift.md:.github/workflows/git-ape-drift.md" + "workflows/git-ape-drift.lock.yml:.github/workflows/git-ape-drift.lock.yml" + "copilot-instructions.md:.github/copilot-instructions.md" +) + +if [ -t 1 ]; then + GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BOLD=$'\033[1m'; RESET=$'\033[0m' +else + GREEN=''; YELLOW=''; BOLD=''; RESET='' +fi + +printf '%sScaffolding Git-Ape files into:%s %s\n\n' "$BOLD" "$RESET" "$TARGET_ROOT" + +created=0 +skipped=0 +skipped_paths=() + +for mapping in "${MAPPINGS[@]}"; do + src="${mapping%%:*}" + dst="${mapping#*:}" + src_path="$TEMPLATES_DIR/$src" + dst_path="$TARGET_ROOT/$dst" + + if [ ! -f "$src_path" ]; then + echo "ERROR: template missing: $src_path" >&2 + exit 1 + fi + + if [ -e "$dst_path" ]; then + printf ' %sāŠ Skipped%s %s (already exists)\n' "$YELLOW" "$RESET" "$dst" + skipped=$((skipped + 1)) + skipped_paths+=("$dst") + else + mkdir -p "$(dirname "$dst_path")" + cp "$src_path" "$dst_path" + printf ' %sāœ“ Created%s %s\n' "$GREEN" "$RESET" "$dst" + created=$((created + 1)) + fi +done + +printf '\n%sCreated %d file(s), skipped %d file(s).%s\n' \ + "$BOLD" "$created" "$skipped" "$RESET" + +if [ "$skipped" -gt 0 ]; then + printf '\nSkipped files were left unchanged. Diff against the canonical templates with:\n' + for path in "${skipped_paths[@]}"; do + # Map the dst path back to the template source + case "$path" in + .github/copilot-instructions.md) + src_rel="copilot-instructions.md" ;; + .github/workflows/*) + src_rel="workflows/${path##*/}" ;; + *) + src_rel="$path" ;; + esac + printf ' diff -u %s %s/%s\n' "$path" "$TEMPLATES_DIR" "$src_rel" + done +fi + +printf '\nFiles were left UNSTAGED. Review them, then commit when ready.\n' diff --git a/.github/skills/git-ape-onboarding/scripts/sync-templates.ps1 b/.github/skills/git-ape-onboarding/scripts/sync-templates.ps1 new file mode 100644 index 0000000..a92fe71 --- /dev/null +++ b/.github/skills/git-ape-onboarding/scripts/sync-templates.ps1 @@ -0,0 +1,155 @@ +<# +.SYNOPSIS + Sync mirror between canonical onboarding templates and the repository's + active .github/copilot-instructions.md. + +.DESCRIPTION + Canonical source: + .github/skills/git-ape-onboarding/templates/ + + Mirror destinations: + .github/copilot-instructions.md + + Note: The workflow templates under templates/workflows/ are NOT mirrored + into this repository's .github/workflows/. They are scaffolded only into + a USER's repository by scaffold-repo.{sh,ps1} during onboarding. + + PowerShell parity for sync-templates.sh. The two scripts MUST produce + identical results on the same tree — CI runs the .ps1 on windows-latest + and the .sh on ubuntu-latest in the same workflow. + +.PARAMETER Action + check Exit 1 on drift (CI gate). + apply Overwrite each mirror with its canonical template. + diff Show per-file diffs. Exits 0 regardless. + +.EXAMPLE + pwsh .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 check +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet('check', 'apply', 'diff')] + [string]$Action +) + +$ErrorActionPreference = 'Stop' + +# Locate the repo root via git, fall back to current directory. +$repoRoot = (& git rev-parse --show-toplevel 2>$null) +if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { + $repoRoot = (Get-Location).Path +} +$templatesDir = Join-Path $repoRoot '.github/skills/git-ape-onboarding/templates' + +# template-relative-path -> repo-relative-destination +$mappings = @( + @{ Src = 'copilot-instructions.md'; Dst = '.github/copilot-instructions.md' } +) + +function Test-FileByteEquality { + param([string]$Left, [string]$Right) + if (-not (Test-Path -LiteralPath $Left -PathType Leaf)) { return $false } + if (-not (Test-Path -LiteralPath $Right -PathType Leaf)) { return $false } + $a = [System.IO.File]::ReadAllBytes($Left) + $b = [System.IO.File]::ReadAllBytes($Right) + if ($a.Length -ne $b.Length) { return $false } + for ($i = 0; $i -lt $a.Length; $i++) { + if ($a[$i] -ne $b[$i]) { return $false } + } + return $true +} + +function Write-Status { + param([string]$Icon, [string]$Color, [string]$Message, [switch]$Err) + # Error lines go to stderr only (no double-print to stdout). + # Color stays on the success path; stderr is plain to keep CI logs readable. + if ($Err) { + [Console]::Error.WriteLine("$Icon $Message") + } else { + Write-Host "$Icon $Message" -ForegroundColor $Color + } +} + +$drift = 0 +$applied = 0 +$checked = 0 + +foreach ($m in $mappings) { + $src = Join-Path $templatesDir $m.Src + $dst = Join-Path $repoRoot $m.Dst + $checked++ + + if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { + [Console]::Error.WriteLine("ERROR missing canonical template: $($m.Src)") + exit 2 + } + + switch ($Action) { + 'apply' { + if ((Test-Path -LiteralPath $dst -PathType Leaf) -and (Test-FileByteEquality -Left $src -Right $dst)) { + Write-Host "= $($m.Dst) (already in sync)" -ForegroundColor Yellow + } else { + $dstDir = Split-Path -Parent $dst + if (-not (Test-Path -LiteralPath $dstDir)) { + New-Item -ItemType Directory -Path $dstDir -Force | Out-Null + } + Copy-Item -LiteralPath $src -Destination $dst -Force + Write-Host "āœ“ $($m.Dst) (updated from template)" -ForegroundColor Green + $applied++ + } + } + 'check' { + if (-not (Test-Path -LiteralPath $dst -PathType Leaf)) { + Write-Status -Icon 'āœ—' -Color Red -Message "$($m.Dst) (mirror missing)" -Err + $drift++ + } elseif (-not (Test-FileByteEquality -Left $src -Right $dst)) { + Write-Status -Icon 'āœ—' -Color Red -Message "$($m.Dst) (drift from template)" -Err + $drift++ + } else { + Write-Host "āœ“ $($m.Dst)" -ForegroundColor Green + } + } + 'diff' { + if (-not (Test-Path -LiteralPath $dst -PathType Leaf)) { + Write-Host "āœ— $($m.Dst) (missing)" -ForegroundColor Red + $drift++ + } elseif (-not (Test-FileByteEquality -Left $src -Right $dst)) { + Write-Host "--- diff: .github/skills/git-ape-onboarding/templates/$($m.Src) vs $($m.Dst)" -ForegroundColor Cyan + # Use git diff for parity with the .sh output style; fall back to Compare-Object. + & git --no-pager diff --no-index -- $src $dst 2>$null + $drift++ + } + } + } +} + +switch ($Action) { + 'apply' { + if ($applied -eq 0) { + Write-Host "" + Write-Host "All $checked mirror(s) already in sync." -ForegroundColor Green + } else { + Write-Host "" + Write-Host "Updated $applied mirror file(s). Commit them with the template changes." -ForegroundColor Green + } + } + 'check' { + if ($drift -gt 0) { + [Console]::Error.WriteLine("") + [Console]::Error.WriteLine("$drift file(s) out of sync. Run:") + [Console]::Error.WriteLine(" pwsh .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 apply") + [Console]::Error.WriteLine("(or the .sh equivalent on macOS/Linux) and commit the updated mirror(s).") + exit 1 + } + Write-Host "" + Write-Host "All $checked mirror(s) match the canonical templates." -ForegroundColor Green + } + 'diff' { + if ($drift -eq 0) { + Write-Host "" + Write-Host "No divergence detected." -ForegroundColor Green + } + } +} diff --git a/.github/skills/git-ape-onboarding/scripts/sync-templates.sh b/.github/skills/git-ape-onboarding/scripts/sync-templates.sh new file mode 100755 index 0000000..92527db --- /dev/null +++ b/.github/skills/git-ape-onboarding/scripts/sync-templates.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Sync mirror between canonical onboarding templates and the repository's +# active .github/copilot-instructions.md. +# +# Canonical source: +# .github/skills/git-ape-onboarding/templates/ +# +# Mirror destinations: +# .github/copilot-instructions.md +# +# Note: The workflow templates under templates/workflows/ are NOT mirrored +# into this repository's .github/workflows/. They are scaffolded only into a +# USER's repository by scripts/scaffold-repo.{sh,ps1} during onboarding. +# +# Usage (run from any directory inside the repo): +# .github/skills/git-ape-onboarding/scripts/sync-templates.sh check # exit 1 on drift (CI gate) +# .github/skills/git-ape-onboarding/scripts/sync-templates.sh apply # copy templates -> mirrors +# .github/skills/git-ape-onboarding/scripts/sync-templates.sh diff # show per-file diffs +# +# The canonical templates ship inside the VS Code extension folder. The +# matching repo-root copies must stay byte-identical so this repository's own +# Copilot agent uses the same deployment standards it distributes to users. + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +TEMPLATES_DIR="$REPO_ROOT/.github/skills/git-ape-onboarding/templates" + +# Mapping: template-relative-path -> repo-relative-destination +declare -a MAPPINGS=( + "copilot-instructions.md:.github/copilot-instructions.md" +) + +# Color output when stdout is a TTY. +if [[ -t 1 ]]; then + RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BOLD=$'\033[1m'; RESET=$'\033[0m' +else + RED=""; GREEN=""; YELLOW=""; BOLD=""; RESET="" +fi + +usage() { + cat < + + check Exit 1 if any mirror differs from its canonical template. + Used by CI (.github/workflows/git-ape-onboarding-template-check.yml). + + apply Overwrite each mirror with its canonical template. Run this after + editing a template, then commit both the template and the mirror. + + diff Show a unified diff for every divergent pair. Exits 0 regardless. +EOF +} + +cmd="${1:-}" +case "$cmd" in + check|apply|diff) ;; + ""|-h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; +esac + +drift=0 +applied=0 +checked=0 + +for mapping in "${MAPPINGS[@]}"; do + src_rel="${mapping%%:*}" + dst_rel="${mapping#*:}" + src="$TEMPLATES_DIR/$src_rel" + dst="$REPO_ROOT/$dst_rel" + checked=$((checked + 1)) + + if [[ ! -f "$src" ]]; then + printf '%sERROR%s missing canonical template: %s\n' "$RED" "$RESET" "$src_rel" >&2 + exit 2 + fi + + case "$cmd" in + apply) + if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then + printf '%s=%s %s (already in sync)\n' "$YELLOW" "$RESET" "$dst_rel" + else + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + printf '%sāœ“%s %s (updated from template)\n' "$GREEN" "$RESET" "$dst_rel" + applied=$((applied + 1)) + fi + ;; + check) + if [[ ! -f "$dst" ]]; then + printf '%sāœ—%s %s (mirror missing)\n' "$RED" "$RESET" "$dst_rel" >&2 + drift=$((drift + 1)) + elif ! cmp -s "$src" "$dst"; then + printf '%sāœ—%s %s (drift from template)\n' "$RED" "$RESET" "$dst_rel" >&2 + drift=$((drift + 1)) + else + printf '%sāœ“%s %s\n' "$GREEN" "$RESET" "$dst_rel" + fi + ;; + diff) + if [[ ! -f "$dst" ]]; then + printf '%sāœ—%s %s (missing)\n' "$RED" "$RESET" "$dst_rel" + drift=$((drift + 1)) + elif ! cmp -s "$src" "$dst"; then + printf '%s---%s diff: %s vs %s\n' "$BOLD" "$RESET" \ + ".github/skills/git-ape-onboarding/templates/$src_rel" "$dst_rel" + diff -u "$src" "$dst" || true + drift=$((drift + 1)) + fi + ;; + esac +done + +case "$cmd" in + apply) + if [[ "$applied" -eq 0 ]]; then + printf '\n%sAll %d mirror(s) already in sync.%s\n' "$GREEN" "$checked" "$RESET" + else + printf '\n%sUpdated %d mirror file(s).%s Commit them with the template changes.\n' \ + "$GREEN" "$applied" "$RESET" + fi + ;; + check) + if [[ "$drift" -gt 0 ]]; then + printf '\n%s%d file(s) out of sync.%s Run:\n .github/skills/git-ape-onboarding/scripts/sync-templates.sh apply\nand commit the updated mirror(s).\n' \ + "$RED" "$drift" "$RESET" >&2 + exit 1 + fi + printf '\n%sAll %d mirror(s) match the canonical templates.%s\n' "$GREEN" "$checked" "$RESET" + ;; + diff) + if [[ "$drift" -eq 0 ]]; then + printf '\n%sNo divergence detected.%s\n' "$GREEN" "$RESET" + fi + ;; +esac diff --git a/.github/skills/git-ape-onboarding/templates/README.md b/.github/skills/git-ape-onboarding/templates/README.md new file mode 100644 index 0000000..1140bd6 --- /dev/null +++ b/.github/skills/git-ape-onboarding/templates/README.md @@ -0,0 +1,107 @@ +# Git-Ape Onboarding Templates + +This folder is the **canonical source** for the workflow files and deployment +standards that the `/git-ape-onboarding` skill scaffolds into a user's repository. + +> [!NOTE] +> This README is for **repository maintainers only**. It is not shown to users +> who run onboarding. + +## How it works + +The Git-Ape VS Code extension ships only the paths registered in `plugin.json` +(`.github/agents/` and `.github/skills/`). Files in this `templates/` folder +ride along inside the skill folder, so they are present on disk after the +extension installs. + +When `/git-ape-onboarding` runs in a user's own repository, the playbook +resolves the skill's install directory and copies these template files into +the user's `.github/workflows/` and `.github/copilot-instructions.md`. + +## Single source of truth + +The files here are **canonical**. + +**Workflow templates** (`workflows/*.yml`, `workflows/git-ape-drift.{md,lock.yml}`) +are **not mirrored** into this repository's `.github/workflows/`. They are +scaffolded only into a **user's repository** by `scaffold-repo.{sh,ps1}` during +onboarding. Git-Ape's own repo doesn't run these workflows. + +**`copilot-instructions.md`** is mirrored to this repository's +`.github/copilot-instructions.md` so that Copilot uses the same deployment +standards when assisting on the git-ape repo itself. The mirror is kept in sync by: + +- `.github/skills/git-ape-onboarding/scripts/sync-templates.sh` — bash + helper for macOS/Linux/WSL (`check`, `apply`, `diff`) +- `.github/skills/git-ape-onboarding/scripts/sync-templates.ps1` — PowerShell + parity helper for Windows (same three subcommands, byte-identical results) +- `.github/workflows/git-ape-onboarding-template-check.yml` — CI gate that + runs both helpers (Ubuntu and Windows runners) plus a recursive `diff -r` + between bash and pwsh scaffold sandboxes; fails any PR whose + `copilot-instructions.md` mirror diverges OR whose two scaffolders produce + different output + +**Editing workflow (copilot-instructions.md):** + +1. Edit `.github/skills/git-ape-onboarding/templates/copilot-instructions.md` +2. Run **one** of: + - `.github/skills/git-ape-onboarding/scripts/sync-templates.sh apply` (bash) + - `pwsh .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 apply` (PowerShell) +3. Commit both the canonical change and the mirror update in the same PR + +**Editing workflow templates:** Edit the file under +`.github/skills/git-ape-onboarding/templates/workflows/` and commit. No mirror +update is needed because Git-Ape's own repo does not contain a mirror copy. + +**Editing the helpers themselves:** if you change one of `sync-templates.{sh,ps1}` +or `scaffold-repo.{sh,ps1}`, edit the parity sibling in the same PR. The +`scaffold-parity-smoke` CI job recursively diffs the output of both +`scaffold-repo` scripts and will fail on any divergence. + +## Scaffold behavior + +The skill scaffolds files **into the user's working copy only**: + +- No `git add`, `git commit`, `git push`, or PR creation +- Skip-with-notice on collision — never overwrites a pre-existing file +- Final summary lists `Created` and `Skipped` counts so the user can reconcile + +## Contents + +| Template | Destination in user repo | Purpose | +|----------|--------------------------|---------| +| `workflows/git-ape-plan.yml` | `.github/workflows/git-ape-plan.yml` | Validate template + what-if on PR | +| `workflows/git-ape-deploy.yml` | `.github/workflows/git-ape-deploy.yml` | Execute deployment on merge or `/deploy` | +| `workflows/git-ape-destroy.yml` | `.github/workflows/git-ape-destroy.yml` | Tear down stack on `destroy-requested` | +| `workflows/git-ape-verify.yml` | `.github/workflows/git-ape-verify.yml` | Manual verify OIDC + RBAC + workflow presence | +| `workflows/git-ape-drift.md` | `.github/workflows/git-ape-drift.md` | Agentic drift workflow source (gh-aw) | +| `workflows/git-ape-drift.lock.yml` | `.github/workflows/git-ape-drift.lock.yml` | Compiled drift workflow (runnable as-is) | +| `copilot-instructions.md` | `.github/copilot-instructions.md` | Git-Ape deployment standards | + +## Regenerating the drift lock file + +`git-ape-drift.lock.yml` is generated from `git-ape-drift.md` by: + +```bash +gh aw compile +``` + +The sync script does **not** auto-run `gh aw compile`. If you edit +`git-ape-drift.md`, regenerate the lock file manually and commit both. The +template check workflow does not validate that the `.md` source produces the +`.lock.yml`. + +### gh-aw lock file is repo-dependent (expected) + +When a user runs `gh aw compile` in their own scaffolded repo, the resulting +`git-ape-drift.lock.yml` will **not** be byte-identical to the one we shipped. +Two known drift sources, both intentional: + +1. **Cron scattering** — gh-aw uses repository identity to deterministically + pick a cron slot, so the user's repo gets a different slot than ours. +2. **Action SHA re-resolution** — tags like `actions/github-script@v9` and + `azure/login@v2` get re-pinned to the current HEAD at compile time. + +The scaffolder ships a fully compiled lock file so users can run the workflow +without installing gh-aw. If they later edit the `.md` source and recompile, +both sources of drift are expected and acceptable. diff --git a/.github/skills/git-ape-onboarding/templates/copilot-instructions.md b/.github/skills/git-ape-onboarding/templates/copilot-instructions.md new file mode 100644 index 0000000..be805ff --- /dev/null +++ b/.github/skills/git-ape-onboarding/templates/copilot-instructions.md @@ -0,0 +1,449 @@ +# Azure Deployment Standards + +## Naming Conventions + +**āš ļø CRITICAL: Always use the `azure-naming-research` skill to lookup CAF abbreviations and Azure naming constraints before suggesting or validating resource names.** + +Use consistent CAF-compliant naming patterns across all Azure resources: + +**Standard Format:** +``` +{resource-type-abbreviation}-{project}-{environment}-{region}[-{instance}] +``` + +**Resource Groups:** +- CAF abbreviation: `rg` +- Format: `rg-{project}-{environment}-{region}` +- Example: `rg-webapp-prod-eastus` +- Constraints: 1-90 chars, alphanumeric + hyphens + underscores + periods + +**Function Apps:** +- CAF abbreviation: `func` +- Format: `func-{project}-{environment}-{region}` +- Example: `func-api-dev-westus2` +- Constraints: 2-60 chars, alphanumeric + hyphens, globally unique + +**Storage Accounts:** +- CAF abbreviation: `st` +- Format: `st{project}{env}{random}` (lowercase, no hyphens, max 24 chars) +- Example: `stwebappdev8k3m` +- Constraints: 3-24 chars, lowercase alphanumeric only, globally unique +- Special: Use `uniqueString(resourceGroup().id)` for uniqueness + +**App Service Plans:** +- CAF abbreviation: `asp` +- Format: `asp-{project}-{environment}-{region}` +- Example: `asp-webapp-prod-eastus` + +**Web Apps:** +- CAF abbreviation: `app` +- Format: `app-{project}-{environment}-{region}` +- Example: `app-webapp-prod-eastus` +- Constraints: 2-60 chars, alphanumeric + hyphens, globally unique + +**SQL Servers:** +- CAF abbreviation: `sql` +- Format: `sql-{project}-{environment}-{region}` +- Example: `sql-webapp-prod-eastus` +- Constraints: 1-63 chars, lowercase alphanumeric + hyphens, globally unique + +**SQL Databases:** +- CAF abbreviation: `sqldb` +- Format: `sqldb-{project}-{environment}` +- Example: `sqldb-webapp-prod` + +**Cosmos DB:** +- CAF abbreviation: `cosmos` +- Format: `cosmos-{project}-{environment}-{region}` +- Example: `cosmos-webapp-prod-eastus` +- Constraints: 3-44 chars, lowercase alphanumeric + hyphens, globally unique + +**Application Insights:** +- CAF abbreviation: `appi` +- Format: `appi-{project}-{environment}-{region}` +- Example: `appi-webapp-prod-eastus` + +**Key Vaults:** +- CAF abbreviation: `kv` +- Format: `kv-{project}-{env}-{region}` +- Example: `kv-webapp-prod-eus` (shortened region for length) +- Constraints: 3-24 chars, alphanumeric + hyphens, globally unique + +**Log Analytics Workspaces:** +- CAF abbreviation: `log` +- Format: `log-{project}-{environment}-{region}` +- Example: `log-webapp-prod-eastus` +- Constraints: 4-63 chars, alphanumeric + hyphens, must start/end with alphanumeric + +**Container Apps Environments:** +- CAF abbreviation: `cae` +- Format: `cae-{project}-{environment}-{region}` +- Example: `cae-api-prod-eastus` +- Constraints: 1-60 chars, alphanumeric + hyphens +- Note: Requires a Log Analytics workspace for app logs + +**Container Apps:** +- CAF abbreviation: `ca` +- Format: `ca-{project}-{environment}-{region}` +- Example: `ca-api-prod-eastus` +- Constraints: 2-32 chars, lowercase alphanumeric + hyphens, must start with letter + +**Naming Workflow:** + +1. **Before naming ANY resource:** + ```bash + # Use the azure-naming-research skill + /azure-naming-research {resource-type} + + # Example: + /azure-naming-research "Azure Functions" + + # Returns: CAF abbreviation, length constraints, valid characters, scope + ``` + +2. **Construct name using pattern:** + ``` + {caf-abbrev}-{project}-{env}-{region} + ``` + +3. **Validate against constraints:** + - Length (min-max) + - Character set (alphanumeric, hyphens, etc.) + - Uniqueness scope (global, resource group, subscription) + - Start/end character requirements + +4. **For globally unique resources (Storage, Functions, SQL, Cosmos, Key Vault):** + - Add random suffix if needed: `{name}-${random}` + - Or use ARM's `uniqueString()` function in templates + +**Reference:** +- CAF Abbreviations: https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations +- Naming Rules: https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules + +## Default Regions + +Prefer these regions for deployments unless specified otherwise: +- **Primary:** East US +- **Secondary:** West US 2 +- **Europe:** West Europe + +## Environment Tags + +Always include these tags on all resources: +```json +{ + "Environment": "dev|staging|prod", + "Project": "project-name", + "ManagedBy": "git-ape-agent", + "CreatedDate": "YYYY-MM-DD" +} +``` + +## ARM Template Standards + +- Include `parameters` section for configurable values +- Use `outputs` section to return resource IDs and endpoints +- Apply Azure best practices via `bestpractices` service validation +- Include resource health monitoring configurations +- **Always use `/azure-rest-api-reference` to look up resource properties and API versions before writing or modifying ARM template resources** — never rely on memorized property names or schemas +- **Subscription-level templates** (`subscriptionDeploymentTemplate.json`) must use nested deployments with `"expressionEvaluationOptions": { "scope": "inner" }` — without this, `reference()` and `resourceId()` resolve at subscription scope, causing `ResourceNotFound` errors at deploy time +- All `reference()` calls inside nested templates must include explicit API versions (e.g., `reference(resourceId(...), '2024-03-01')`) +- Pass all parent-scope values (parameters, variables) as explicit parameters to nested templates — never reference parent variables directly from inner-scope templates + +## Deployment Workflow + +### Interactive Mode (VS Code) + +1. **Requirements Gathering:** Collect all necessary parameters before generating templates +2. **Template Validation:** Always validate ARM templates before deployment +3. **User Confirmation:** Echo deployment intent and wait for explicit approval +4. **Deployment Execution:** Monitor progress and capture deployment logs +5. **Integration Testing:** Run health checks on deployed resources + +### Pipeline Mode (GitHub Actions) + +Git-Ape provides three GitHub Actions workflows under `.github/workflows/`: + +#### `git-ape-plan.yml` — Validate & Preview + +**Triggers:** PR opened or updated with changes to `.azure/deployments/**/template.json` + +**What it does:** +1. Detects which deployment directories changed in the PR +2. Logs into Azure via OIDC +3. Validates each ARM template (`az stack sub validate`) +4. Runs what-if analysis (`az deployment sub what-if`) +5. Reads the architecture diagram from the deployment directory +6. Posts a detailed plan as a **PR comment** (validation result + what-if + architecture) +7. Updates the comment on subsequent pushes (idempotent via HTML marker) + +**PR comment includes:** +- Validation status (pass/fail with errors) +- Architecture diagram (from `architecture.md`) +- What-if analysis (resources to create/modify/delete) +- Next steps (approve + merge to deploy, or `/deploy` to deploy early) + +#### `git-ape-deploy.yml` — Execute Deployment + +**Triggers:** +- Push to `main` with deployment file changes (PR merge) +- `/deploy` comment on an **approved** PR + +**What it does:** +1. Detects deployment directories to execute +2. Logs into Azure via OIDC +3. Validates the template one more time (`az stack sub validate`) +4. Deploys as an **Azure Deployment Stack** (`az stack sub create --action-on-unmanage deleteAll`) +5. Runs integration tests (lists deployed resources, tests HTTP endpoints) +6. Commits `state.json` (including `stackId` and `managedResources[]`) back to the repo +7. Posts deployment result as a PR comment (on `/deploy` trigger) + +**Why Deployment Stacks:** +- The stack is the single unit of lifecycle — create, update, and destroy operate on it, not on the underlying RGs. +- `deleteAll` on unmanage guarantees destruction cleans up every managed resource across every scope (subscription, multiple RGs, role/policy assignments at sub scope) in one call. No orphans, idempotent re-runs. +- See [Azure/git-ape#30](https://github.com/Azure/git-ape/issues/30) for the rationale. + +**Requires:** GitHub environment `azure-deploy` (for environment protection rules) + +**Safety:** +- `/deploy` requires PR to be approved first +- Deployments run sequentially (`max-parallel: 1`) to prevent conflicts +- In-progress deployments are never cancelled (`cancel-in-progress: false`) + +#### `git-ape-destroy.yml` — Tear Down Resources + +**Triggers:** +- Push to `main` with changes to `metadata.json` where status is `destroy-requested` (PR merge) +- Manual workflow dispatch with deployment ID + "destroy" confirmation (emergency fallback) + +**What it does:** +1. Detects deployments where `metadata.json` status changed to `destroy-requested` +2. Reads `state.json` to find the deployment stack name (`deploymentId`) and `stackId` +3. Calls `az stack sub show` to inventory the stack's managed resources +4. Calls `az stack sub delete --action-on-unmanage deleteAll` — removes every resource the stack manages, across all scopes, in one synchronous call +5. Updates `state.json` and `metadata.json` with `destroyed` status and commits to repo + +**Idempotency:** +- If the stack is already gone, the workflow records `already-destroyed` and succeeds cleanly. +- No RG-delete fallback path, no subscription-scope resource sweep — Stacks handle multi-scope destruction natively. + +**Destroy flow:** +1. Agent or user creates a PR that sets `metadata.json` status to `destroy-requested` +2. PR is reviewed and approved (human gate for destructive action) +3. On merge to `main`, the workflow detects the status change and executes deletion + +**Requires:** GitHub environment `azure-destroy` (for environment protection rules) + +### Copilot Coding Agent Flow + +When the Copilot Coding Agent processes a deployment request: + +``` +Issue: "Deploy a Container App in Southeast Asia, project myapp" + │ + ā–¼ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Coding Agent (on branch) │ + │ 1. Parse requirements │ + │ 2. Generate ARM template │ + │ 3. Generate architecture.md │ + │ 4. Commit to branch │ + │ 5. Open PR │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ PR opened + ā–¼ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ git-ape-plan.yml │ + │ 1. Validate template │ + │ 2. Run what-if │ + │ 3. Post plan as PR comment │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ User reviews plan + ā–¼ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ User approves PR │ + │ - Option A: Merge → deploy │ + │ - Option B: /deploy comment │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā–¼ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ git-ape-deploy.yml │ + │ 1. OIDC login │ + │ 2. Deploy ARM template │ + │ 3. Integration tests │ + │ 4. Commit state.json │ + │ 5. Post result as comment │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā–¼ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Teardown (when needed) │ + │ PR: set metadata.json │ + │ status → destroy-requested │ + │ Merge → git-ape-destroy │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +### GitHub Environment Setup + +Create two GitHub environments for protection rules: + +**`azure-deploy`:** +- Required reviewers (optional — for prod deployments) +- Deployment branches: `main` only (for merge trigger) + +**`azure-destroy`:** +- Required reviewers (recommended — destructive action) +- Deployment branches: `main` only (triggered on PR merge) + +## Security Baseline + +- Enable HTTPS-only for all web-facing resources +- **Always use managed identities** — never use connection strings, storage keys, or shared access keys +- For Function Apps, use `AzureWebJobsStorage__accountName` (identity-based) instead of `AzureWebJobsStorage` (key-based) +- Set `allowSharedKeyAccess: false` on storage accounts when all consumers use managed identity +- Include RBAC role assignments in ARM templates when resources access each other + +### Deployment Error Recovery Rule + +When a deployment fails, **never weaken security controls to fix it**. Specifically: +- Do NOT re-enable shared key access, disable firewalls, open NSGs, remove authentication, or downgrade TLS to work around errors +- Do NOT replace identity-based access with connection strings or shared keys +- Instead: verify RBAC roles are complete, check for Azure Policy conflicts, check resource dependencies and ordering, and ensure managed identities exist before dependent resources try to use them +- If the secure path genuinely cannot work, **stop and ask the user** — do not silently regress + +### Security Gate Re-Run Rule + +**Any modification to a template after the initial security analysis MUST trigger a full security gate re-run before deployment.** A template that passed the gate at version N is not pre-approved at version N+1. + +- Use AAD-only authentication for SQL databases (`azureADOnlyAuthentication: true`) +- Use Key Vault references for secrets in app settings (`@Microsoft.KeyVault(...)`) +- Enable diagnostic logging and monitoring +- Apply least-privilege RBAC roles (use `/azure-role-selector` skill) +- Disable FTP on all App Services and Function Apps (`ftpsState: Disabled`) +- Set minimum TLS version to 1.2 on all resources + +## Security Analysis Integrity + +**All security reports and assessments produced by Git-Ape agents and skills MUST be factually accurate and verifiable against the actual ARM template or Azure resource configuration.** + +## Compliance & Azure Policy + +### Compliance Frameworks + +- **Primary:** General Azure best practices +- Optionally adopt: CIS Azure Foundations v3.0, NIST SP 800-53 Rev 5, or other regulatory initiatives + +### Policy Enforcement Mode + +- **Default:** Audit (use `Audit`/`AuditIfNotExists` effects for initial rollout) +- **Production hardening:** Deny (use `Deny` effects for Critical-severity policies once audit baselines are clean) + +### Policy Categories + +Always assess and recommend policies for: identity, networking, storage, compute, monitoring, tagging. + +### Policy Advisor Integration + +- Use `/azure-policy-advisor` skill to assess ARM templates against compliance frameworks +- Query Microsoft Learn for current built-in policy definitions — do not hardcode policy IDs +- Output `policy-assessment.md` and `policy-recommendations.json` to deployment directory +- Policy gate is **advisory** (not blocking) — surfaces findings without halting deployment +- During onboarding, ask the user about compliance framework and enforcement mode preferences and update this section accordingly + +### Rules + +1. **Cite evidence**: Every "āœ… Applied" finding must reference the exact ARM property path and value from the template. No exceptions. +2. **Platform defaults ≠ explicit config**: Azure automatic controls (e.g., SSE at rest on managed disks) must be labeled "šŸ”„ Platform Default", not "āœ… Applied." +3. **Honest framing**: If a resource has a public IP, it IS internet-facing. If a port is open with IP restriction, the port IS open. Never soften or misrepresent exposure. +4. **Verify before presenting**: Always re-read the ARM template and cross-check every security finding before showing it to the user. Remove or correct anything that doesn't match. +5. **When uncertain, say so**: Use "ā“ Unknown" if a status cannot be verified. Never fabricate findings. + +### Security Gate (Blocking) + +Security analysis is a **blocking deployment gate**. Deployment CANNOT proceed until all Critical and High severity checks pass. + +- **`🟢 SECURITY GATE: PASSED`** — All Critical and High checks are āœ… Applied or šŸ”„ Platform Default. Deployment may proceed. +- **`šŸ”“ SECURITY GATE: BLOCKED`** — One or more Critical or High checks are āš ļø Not applied or āŒ Misconfigured. Deployment is blocked until resolved. + +When blocked, the user must either: +1. Accept proposed auto-fixes and re-run the full security analysis, or +2. Provide alternative security settings and re-run the full security analysis, or +3. Explicitly override by typing "I accept the security risk" (logged as `OVERRIDDEN` with justification) + +The gate loops until PASSED or overridden — no shortcutting allowed. + +## Azure Authentication + +Git-Ape supports multiple execution contexts. Always use the most secure auth method available. + +### Auth Method Priority + +| Priority | Method | Context | How | +|----------|--------|---------|-----| +| 1 | **OIDC Federated Identity** | GitHub Actions / Copilot Coding Agent | `azure/login@v2` with `id-token: write` permission | +| 2 | **Managed Identity** | Azure-hosted runners / VMs | Automatic — no config needed | +| 3 | **Azure CLI session** | Local VS Code / interactive | `az login` | +| 4 | **Service Principal + secret** | Legacy CI only | Discouraged — migrate to OIDC | + +### OIDC Setup for GitHub Actions + +OIDC eliminates stored secrets by exchanging a short-lived GitHub token for an Azure access token at deploy time. + +**One-time Azure setup:** +1. Create an Azure AD App Registration (or User-Assigned Managed Identity) +2. Add a federated credential for the GitHub repo: + - Issuer: `https://token.actions.githubusercontent.com` + - Subject: `repo:{org}/{repo}:ref:refs/heads/main` (or `environment:production` for env-scoped) + - Audience: `api://AzureADTokenExchange` +3. Grant the app the required RBAC roles on the target subscription (e.g., Contributor + User Access Administrator) + +**Required GitHub secrets** (NOT actual credentials — just identifiers): +- `AZURE_CLIENT_ID` — App Registration's Application (client) ID +- `AZURE_TENANT_ID` — Azure AD tenant ID +- `AZURE_SUBSCRIPTION_ID` — Target subscription ID + +**Workflow snippet:** +```yaml +permissions: + id-token: write # Required for OIDC token request + contents: write # Required for committing deployment state + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Deploy + run: | + az stack sub create \ + --name ${{ env.DEPLOYMENT_ID }} \ + --location ${{ env.LOCATION }} \ + --template-file .azure/deployments/${{ env.DEPLOYMENT_ID }}/template.json \ + --parameters @.azure/deployments/${{ env.DEPLOYMENT_ID }}/parameters.json \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --yes +``` + +**Transitioning from Service Principal secrets to OIDC:** +1. Create the federated credential on the existing App Registration +2. Update the workflow to use `azure/login@v2` with `client-id` instead of `creds` +3. Add `permissions.id-token: write` to the workflow +4. Remove `AZURE_CREDENTIALS` secret from the repo +5. Verify with a test deployment on a dev branch + +### Copilot Coding Agent Considerations + +When Git-Ape runs inside the Copilot Coding Agent: +- The agent operates on a branch and creates a PR — it cannot interactively confirm with the user +- Authentication must be pre-configured via OIDC in the Actions workflow +- All deployment plans should be committed as files in the PR for review +- Actual deployment should happen on merge (via a separate workflow) or via an explicit `/deploy` comment trigger +- The agent should generate the template + what-if analysis but NOT execute deployment unless the workflow is explicitly configured to do so diff --git a/.github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml new file mode 100644 index 0000000..e2ba917 --- /dev/null +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml @@ -0,0 +1,792 @@ +# Git-Ape Deploy Workflow +# Triggers on: +# 1. PR merge to main (when deployment files are included) +# 2. `/deploy` comment on an approved PR (deploys from branch before merge) +# Runs the actual ARM deployment, captures outputs, and runs integration tests. + +name: "Git-Ape: Deploy" + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +on: + # Trigger 1: PR merged to main with deployment artifacts + push: + branches: [main] + paths: + - ".azure/deployments/**/template.json" + - ".azure/deployments/**/parameters.json" + + # Trigger 2: `/deploy` comment on a PR + issue_comment: + types: [created] + +permissions: + id-token: write # OIDC token for Azure login + contents: write # Commit state files back to repo + pull-requests: write # Post deployment results as PR comment + issues: write # Post on issue comments + security-events: write # Upload SARIF results from template analyzer + actions: read # Required by codeql-action/upload-sarif to read workflow run context + +concurrency: + group: git-ape-deploy-${{ github.event_name == 'push' && github.sha || github.event.comment.id }} + cancel-in-progress: false # Never cancel in-progress deployments + +jobs: + # Gate: Only run on `/deploy` comments on approved PRs + check-comment-trigger: + name: Check /deploy trigger + if: github.event_name == 'issue_comment' + runs-on: ubuntu-latest + outputs: + should_deploy: ${{ steps.check.outputs.should_deploy }} + pr_ref: ${{ steps.check.outputs.pr_ref }} + steps: + - name: Check comment and PR status + id: check + uses: actions/github-script@v9 + with: + script: | + const comment = context.payload.comment.body.trim(); + if (!comment.startsWith('/deploy')) { + core.setOutput('should_deploy', 'false'); + return; + } + + // Must be on a PR (not a regular issue) + if (!context.payload.issue.pull_request) { + core.setOutput('should_deploy', 'false'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: 'āŒ `/deploy` can only be used on pull requests.', + }); + return; + } + + // Get PR details + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + // Check PR is approved + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const approved = reviews.some(r => r.state === 'APPROVED'); + + if (!approved) { + core.setOutput('should_deploy', 'false'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: 'āŒ PR must be **approved** before deploying. Get a review approval first.', + }); + return; + } + + core.setOutput('should_deploy', 'true'); + core.setOutput('pr_ref', pr.head.ref); + + // React to the comment + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + detect-deployments: + name: Detect deployments to execute + needs: [check-comment-trigger] + if: | + always() && + (github.event_name == 'push' || + (github.event_name == 'issue_comment' && needs.check-comment-trigger.outputs.should_deploy == 'true')) + runs-on: ubuntu-latest + outputs: + deployment_ids: ${{ steps.find.outputs.deployment_ids }} + has_deployments: ${{ steps.find.outputs.has_deployments }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.check-comment-trigger.outputs.pr_ref || github.ref }} + fetch-depth: 0 + + - name: Find deployment directories + id: find + run: | + if [[ "${{ github.event_name }}" == "push" ]]; then + # On merge: find deployments changed in the merge commit + CHANGED_FILES=$(git diff --name-only HEAD~1...HEAD -- '.azure/deployments/*/template.json' 2>/dev/null || true) + else + # On /deploy comment: find all deployments with template.json on the branch + CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '.azure/deployments/*/template.json' 2>/dev/null || true) + fi + + if [[ -z "$CHANGED_FILES" ]]; then + echo "has_deployments=false" >> "$GITHUB_OUTPUT" + echo "deployment_ids=[]" >> "$GITHUB_OUTPUT" + echo "No deployments found" + exit 0 + fi + + DEPLOYMENT_IDS=$(echo "$CHANGED_FILES" | sed 's|.azure/deployments/\([^/]*\)/.*|\1|' | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))') + + echo "has_deployments=true" >> "$GITHUB_OUTPUT" + echo "deployment_ids=$DEPLOYMENT_IDS" >> "$GITHUB_OUTPUT" + echo "Deployments to execute: $DEPLOYMENT_IDS" + + deploy: + name: "Deploy: ${{ matrix.deployment_id }}" + needs: [detect-deployments, check-comment-trigger] + if: | + always() && + needs.detect-deployments.outputs.has_deployments == 'true' + runs-on: ubuntu-latest + environment: azure-deploy + strategy: + matrix: + deployment_id: ${{ fromJson(needs.detect-deployments.outputs.deployment_ids) }} + max-parallel: 1 # Deploy sequentially to avoid conflicts + fail-fast: false + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.check-comment-trigger.outputs.pr_ref || github.ref }} + + - name: Read deployment parameters + id: params + run: | + DEPLOY_DIR=".azure/deployments/${{ matrix.deployment_id }}" + + if [[ ! -f "$DEPLOY_DIR/template.json" ]]; then + echo "::error::Template not found: $DEPLOY_DIR/template.json" + exit 1 + fi + + if [[ -f "$DEPLOY_DIR/parameters.json" ]]; then + LOCATION=$(jq -r '.parameters.location.value // "eastus"' "$DEPLOY_DIR/parameters.json") + PROJECT=$(jq -r '.parameters.project.value // .parameters.projectName.value // "unknown"' "$DEPLOY_DIR/parameters.json") + ENVIRONMENT=$(jq -r '.parameters.environment.value // "dev"' "$DEPLOY_DIR/parameters.json") + else + LOCATION="eastus" + PROJECT="unknown" + ENVIRONMENT="dev" + fi + + echo "location=$LOCATION" >> "$GITHUB_OUTPUT" + echo "project=$PROJECT" >> "$GITHUB_OUTPUT" + echo "environment=$ENVIRONMENT" >> "$GITHUB_OUTPUT" + echo "deploy_dir=$DEPLOY_DIR" >> "$GITHUB_OUTPUT" + + - name: Azure Login (OIDC) + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Capture pre-deploy state (for rollback) + id: pre_state + run: | + STACK_NAME="${{ matrix.deployment_id }}" + echo "::group::Pre-deploy state capture" + echo "[$(date -u +%H:%M:%S)] Checking if stack '$STACK_NAME' already exists…" + + # Does the stack currently exist? If yes, this is an UPDATE (rollback possible). + # If no, this is a NEW deployment (rollback = delete partial stack). + if PRIOR_STACK=$(az stack sub show --name "$STACK_NAME" --output json 2>/dev/null); then + PRIOR_STATE=$(echo "$PRIOR_STACK" | jq -r '.provisioningState // "unknown"') + PRIOR_ID=$(echo "$PRIOR_STACK" | jq -r '.id // empty') + echo "stack_existed=true" >> "$GITHUB_OUTPUT" + echo "prior_stack_id=$PRIOR_ID" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] Prior stack found — provisioningState=$PRIOR_STATE" + echo "[$(date -u +%H:%M:%S)] Prior stackId: $PRIOR_ID" + else + echo "stack_existed=false" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] No prior stack — this is a NEW deployment." + fi + + # Also snapshot the previous template from git (parent commit of this merge + # or origin/main for /deploy comment). Used to redeploy last-known-good on failure. + DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" + mkdir -p /tmp/rollback + if git show HEAD~1:"$DEPLOY_DIR/template.json" > /tmp/rollback/template.json 2>/dev/null; then + cp "$DEPLOY_DIR/parameters.json" /tmp/rollback/parameters.json 2>/dev/null || true + # Prefer the previous parameters if they exist at HEAD~1 + git show HEAD~1:"$DEPLOY_DIR/parameters.json" > /tmp/rollback/parameters.json 2>/dev/null || true + echo "prior_template_available=true" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] Previous template captured from HEAD~1 → /tmp/rollback/" + echo " template bytes: $(wc -c < /tmp/rollback/template.json)" + else + echo "prior_template_available=false" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] No previous template in git history (first deployment)" + fi + echo "::endgroup::" + + - name: Validate before deploy (stack) + run: | + echo "::group::Template validation" + echo "[$(date -u +%H:%M:%S)] Validating stack '${{ matrix.deployment_id }}' in '${{ steps.params.outputs.location }}'" + az stack sub validate \ + --name "${{ matrix.deployment_id }}" \ + --location "${{ steps.params.outputs.location }}" \ + --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ + --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --output json + echo "[$(date -u +%H:%M:%S)] Validation passed āœ“" + echo "::endgroup::" + + - name: Stage template for security scan + id: scan_stage + run: | + # WORKAROUND: see git-ape-plan.yml for full explanation. Template Analyzer's + # directory walker skips .azure/ on Linux (.NET treats dot-prefixed paths as + # Hidden), so we stage the template into a non-dotted dir at the workspace root. + STAGE_DIR="templateanalyzer-scan/${{ matrix.deployment_id }}" + mkdir -p "$STAGE_DIR" + cp "${{ steps.params.outputs.deploy_dir }}/template.json" "$STAGE_DIR/template.json" + if [[ -f "${{ steps.params.outputs.deploy_dir }}/parameters.json" ]]; then + cp "${{ steps.params.outputs.deploy_dir }}/parameters.json" "$STAGE_DIR/template.parameters.json" + fi + echo "stage_dir=$STAGE_DIR" >> "$GITHUB_OUTPUT" + ls -la "$STAGE_DIR" + + - name: Run Microsoft Defender for DevOps template analyzer + id: security_scan + continue-on-error: true + uses: microsoft/security-devops-action@v1 + with: + tools: templateanalyzer + + - name: Cleanup staged template + if: always() + run: rm -rf templateanalyzer-scan + + - name: Upload SARIF results + if: always() && steps.security_scan.outputs.sarifFile != '' + continue-on-error: true + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ steps.security_scan.outputs.sarifFile }} + category: templateanalyzer + + - name: Check security scan results + id: scan_gate + run: | + SARIF_FILE="${{ steps.security_scan.outputs.sarifFile }}" + if [[ -f "$SARIF_FILE" ]]; then + ERRORS=$(jq '[.runs[].results[] | select(.level == "error")] | length' "$SARIF_FILE" 2>/dev/null || echo 0) + if [[ "$ERRORS" -gt 0 ]]; then + echo "::error::Template analyzer found $ERRORS security error(s). Deployment blocked." + jq -r '.runs[].results[] | select(.level == "error") | " ERROR: \(.message.text) (\(.ruleId))"' "$SARIF_FILE" + exit 1 + fi + echo "Security scan passed — no errors found" + fi + + - name: Deploy to Azure (Deployment Stack) + id: deploy + run: | + STACK_NAME="${{ matrix.deployment_id }}" + echo "::group::Stack deployment" + echo "[$(date -u +%H:%M:%S)] šŸš€ Starting stack deployment: $STACK_NAME" + echo " location : ${{ steps.params.outputs.location }}" + echo " template : ${{ steps.params.outputs.deploy_dir }}/template.json" + echo " parameters : ${{ steps.params.outputs.deploy_dir }}/parameters.json" + echo " project : ${{ steps.params.outputs.project }}" + echo " environment : ${{ steps.params.outputs.environment }}" + echo " prior stack : ${{ steps.pre_state.outputs.stack_existed }}" + START_TIME=$(date +%s) + + # Enable verbose Azure CLI logging for this step + export AZURE_CORE_OUTPUT=json + + # Create/update the subscription-scope Deployment Stack. + # --action-on-unmanage deleteAll binds the whole stack (RG + contents) + # to a single lifecycle so destroy is idempotent across all scopes. + set +e + DEPLOY_OUTPUT=$(az stack sub create \ + --name "$STACK_NAME" \ + --location "${{ steps.params.outputs.location }}" \ + --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ + --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --description "Git-Ape deployment $STACK_NAME" \ + --tags "managedBy=git-ape" "deploymentId=$STACK_NAME" \ + --yes \ + --verbose \ + --output json 2>&1) + EXIT_CODE=$? + set -e + + END_TIME=$(date +%s) + DURATION=$((END_TIME - START_TIME)) + echo "deploy_duration=${DURATION}s" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] az stack sub create exited with code $EXIT_CODE after ${DURATION}s" + echo "::endgroup::" + + if [[ $EXIT_CODE -ne 0 ]]; then + echo "deploy_status=failed" >> "$GITHUB_OUTPUT" + echo "deploy_error<> "$GITHUB_OUTPUT" + echo "$DEPLOY_OUTPUT" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "" + echo "==========================================" + echo "āŒ STACK DEPLOYMENT FAILED" + echo "==========================================" + echo "$DEPLOY_OUTPUT" + echo "==========================================" + + # Surface the underlying deployment operation errors — the stack error + # is usually just a summary; the real root cause is in the operations list. + echo "::group::Underlying deployment operation errors" + echo "[$(date -u +%H:%M:%S)] Fetching failed operations from deployment '$STACK_NAME'…" + az deployment sub show --name "$STACK_NAME" --output json 2>/dev/null \ + | jq -r '.properties // {}' || echo "No subscription-scope deployment details available." + + # Enumerate per-operation failures with their error messages + az deployment operation sub list --name "$STACK_NAME" --output json 2>/dev/null \ + | jq -r '.[] | select(.properties.provisioningState == "Failed") | + "──────────\nResource : \(.properties.targetResource.resourceName // "n/a") (\(.properties.targetResource.resourceType // "n/a"))\nStatus : \(.properties.statusCode // "n/a")\nMessage : \(.properties.statusMessage.error.message // .properties.statusMessage // "n/a")"' \ + || echo "No operation details available (deployment may not have reached Azure)." + echo "::endgroup::" + + echo "::error::Stack deployment failed — see output above for details" + exit 1 + fi + + echo "deploy_status=succeeded" >> "$GITHUB_OUTPUT" + + # Capture the stack resource id — this is the single source of truth + # for destroy. Stored in state.json as `stackId`. + STACK_ID=$(echo "$DEPLOY_OUTPUT" | jq -r '.id // empty') + echo "stack_id=$STACK_ID" >> "$GITHUB_OUTPUT" + + # Extract template outputs from the stack + OUTPUTS=$(echo "$DEPLOY_OUTPUT" | jq -r '.outputs // .properties.outputs // {}') + echo "deploy_outputs<> "$GITHUB_OUTPUT" + echo "$OUTPUTS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + # Extract resource group name (for integration tests) + RG_NAME=$(echo "$OUTPUTS" | jq -r '.resourceGroupName.value // empty') + echo "resource_group=$RG_NAME" >> "$GITHUB_OUTPUT" + + # Capture the list of managed resources from the stack — this is the + # authoritative manifest for everything the stack will delete on destroy. + MANAGED=$(echo "$DEPLOY_OUTPUT" | jq -c '[(.resources // .properties.resources // [])[] | {id: .id, status: .status}]') + echo "managed_resources<> "$GITHUB_OUTPUT" + echo "$MANAGED" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + echo "āœ… Stack deployed in ${DURATION}s — stackId: $STACK_ID" + echo " Managed resources: $(echo "$MANAGED" | jq 'length')" + + - name: Run integration tests + id: tests + if: steps.deploy.outputs.deploy_status == 'succeeded' + run: | + RG_NAME="${{ steps.deploy.outputs.resource_group }}" + + if [[ -z "$RG_NAME" ]]; then + echo "āš ļø No resource group name in outputs, skipping integration tests" + echo "test_status=skipped" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Running integration tests for RG: $RG_NAME" + + # List deployed resources + RESOURCES=$(az resource list --resource-group "$RG_NAME" \ + --query "[].{name:name, type:type, provisioningState:provisioningState}" \ + --output json 2>/dev/null || echo "[]") + + echo "resources<> "$GITHUB_OUTPUT" + echo "$RESOURCES" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + # Check all resources provisioned successfully + FAILED=$(echo "$RESOURCES" | jq '[.[] | select(.provisioningState != "Succeeded")] | length') + if [[ "$FAILED" -gt 0 ]]; then + echo "test_status=failed" >> "$GITHUB_OUTPUT" + echo "::warning::$FAILED resource(s) not in Succeeded state" + else + echo "test_status=passed" >> "$GITHUB_OUTPUT" + fi + + # Test HTTP endpoints (Container Apps, Function Apps, Web Apps) + ENDPOINTS=$(echo "$RESOURCES" | jq -r '.[] | select(.type == "Microsoft.App/containerApps" or .type == "Microsoft.Web/sites") | .name') + TEST_RESULTS="" + + for NAME in $ENDPOINTS; do + RESOURCE_TYPE=$(echo "$RESOURCES" | jq -r ".[] | select(.name == \"$NAME\") | .type") + + if [[ "$RESOURCE_TYPE" == "Microsoft.App/containerApps" ]]; then + FQDN=$(az containerapp show -n "$NAME" -g "$RG_NAME" --query "properties.configuration.ingress.fqdn" -o tsv 2>/dev/null || echo "") + else + FQDN=$(az webapp show -n "$NAME" -g "$RG_NAME" --query "defaultHostName" -o tsv 2>/dev/null || echo "") + fi + + if [[ -n "$FQDN" ]]; then + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "https://$FQDN" 2>/dev/null || echo "000") + TEST_RESULTS="${TEST_RESULTS}\n- ${NAME}: https://${FQDN} → HTTP ${HTTP_CODE}" + + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 400 ]]; then + echo "āœ… $NAME: HTTP $HTTP_CODE" + else + echo "āš ļø $NAME: HTTP $HTTP_CODE (may still be starting)" + fi + fi + done + + echo "test_endpoints<> "$GITHUB_OUTPUT" + echo -e "$TEST_RESULTS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Rollback on failure + id: rollback + if: failure() && steps.deploy.outcome == 'failure' + run: | + STACK_NAME="${{ matrix.deployment_id }}" + STACK_EXISTED="${{ steps.pre_state.outputs.stack_existed }}" + PRIOR_TEMPLATE_AVAILABLE="${{ steps.pre_state.outputs.prior_template_available }}" + + echo "::group::Rollback decision" + echo "[$(date -u +%H:%M:%S)] Evaluating rollback strategy…" + echo " stack existed before : $STACK_EXISTED" + echo " prior template available : $PRIOR_TEMPLATE_AVAILABLE" + + ROLLBACK_ACTION="none" + ROLLBACK_STATUS="not-attempted" + + if [[ "$STACK_EXISTED" == "true" && "$PRIOR_TEMPLATE_AVAILABLE" == "true" ]]; then + ROLLBACK_ACTION="redeploy-previous" + echo "[$(date -u +%H:%M:%S)] Strategy: redeploy previous template (last-known-good)" + echo "::endgroup::" + + echo "::group::Rollback — redeploying previous template" + set +e + az stack sub create \ + --name "$STACK_NAME" \ + --location "${{ steps.params.outputs.location }}" \ + --template-file /tmp/rollback/template.json \ + --parameters @/tmp/rollback/parameters.json \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --description "Git-Ape ROLLBACK of failed deployment $STACK_NAME" \ + --tags "managedBy=git-ape" "deploymentId=$STACK_NAME" "rollback=true" \ + --yes --verbose --output json + RB_EXIT=$? + set -e + if [[ $RB_EXIT -eq 0 ]]; then + ROLLBACK_STATUS="succeeded" + echo "[$(date -u +%H:%M:%S)] āœ… Rollback succeeded — stack restored to previous template" + else + ROLLBACK_STATUS="failed" + echo "::error::Rollback to previous template FAILED (exit $RB_EXIT) — manual intervention required" + fi + echo "::endgroup::" + elif [[ "$STACK_EXISTED" == "false" ]]; then + ROLLBACK_ACTION="delete-failed-stack" + echo "[$(date -u +%H:%M:%S)] Strategy: delete the failed new stack (clean slate)" + echo "::endgroup::" + + echo "::group::Rollback — tearing down failed new stack" + set +e + az stack sub delete \ + --name "$STACK_NAME" \ + --action-on-unmanage deleteAll \ + --yes --output json + RB_EXIT=$? + set -e + if [[ $RB_EXIT -eq 0 ]]; then + ROLLBACK_STATUS="succeeded" + echo "[$(date -u +%H:%M:%S)] āœ… Failed stack deleted — no orphan resources" + else + ROLLBACK_STATUS="failed" + echo "::error::Failed-stack cleanup FAILED (exit $RB_EXIT) — manual intervention required" + fi + echo "::endgroup::" + else + echo "[$(date -u +%H:%M:%S)] āš ļø No rollback possible: prior stack existed but previous template is not in git history" + echo "::endgroup::" + ROLLBACK_ACTION="manual-required" + fi + + echo "rollback_action=$ROLLBACK_ACTION" >> "$GITHUB_OUTPUT" + echo "rollback_status=$ROLLBACK_STATUS" >> "$GITHUB_OUTPUT" + + - name: Save deployment state + if: always() + run: | + DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" + STATUS="${{ steps.deploy.outputs.deploy_status || 'failed' }}" + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + STACK_ID='${{ steps.deploy.outputs.stack_id }}' + MANAGED='${{ steps.deploy.outputs.managed_resources }}' + MANAGED=${MANAGED:-[]} + + # state.json schema v1 — Deployment Stacks edition. + # `stackId` is the single source of truth for destroy. + # `managedResources` is a snapshot captured at deploy time so the + # repo retains a human-readable manifest of what the stack owns. + jq -n \ + --arg schemaVersion "1.0" \ + --arg deploymentId "${{ matrix.deployment_id }}" \ + --arg timestamp "$TIMESTAMP" \ + --arg status "$STATUS" \ + --arg duration "${{ steps.deploy.outputs.deploy_duration }}" \ + --arg subscription "${{ vars.AZURE_SUBSCRIPTION_ID }}" \ + --arg location "${{ steps.params.outputs.location }}" \ + --arg project "${{ steps.params.outputs.project }}" \ + --arg environment "${{ steps.params.outputs.environment }}" \ + --arg resourceGroup "${{ steps.deploy.outputs.resource_group }}" \ + --arg stackId "$STACK_ID" \ + --argjson managedResources "$MANAGED" \ + --arg triggeredBy "${{ github.actor }}" \ + --arg triggerEvent "${{ github.event_name }}" \ + --arg runId "${{ github.run_id }}" \ + --arg runUrl "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + '{ + schemaVersion: $schemaVersion, + deploymentId: $deploymentId, + timestamp: $timestamp, + status: $status, + duration: $duration, + subscription: $subscription, + location: $location, + project: $project, + environment: $environment, + resourceGroup: $resourceGroup, + stackId: (if $stackId == "" then null else $stackId end), + managedResources: $managedResources, + triggeredBy: $triggeredBy, + triggerEvent: $triggerEvent, + runId: $runId, + runUrl: $runUrl + }' > "$DEPLOY_DIR/state.json" + + - name: Commit deployment state + if: always() + run: | + DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" + STATUS="${{ steps.deploy.outputs.deploy_status }}" + STATUS=${STATUS:-failed} + + # Update metadata.json status from pending to actual result + if [[ -f "$DEPLOY_DIR/metadata.json" ]]; then + jq --arg status "$STATUS" '.status = $status' \ + "$DEPLOY_DIR/metadata.json" > "$DEPLOY_DIR/metadata.json.tmp" \ + && mv "$DEPLOY_DIR/metadata.json.tmp" "$DEPLOY_DIR/metadata.json" + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Stash the updated state and metadata files before switching branches + cp "$DEPLOY_DIR/state.json" /tmp/state.json 2>/dev/null || true + cp "$DEPLOY_DIR/metadata.json" /tmp/metadata.json 2>/dev/null || true + + # Ensure we push to main regardless of which ref was checked out + git fetch origin main + git checkout main + + # Restore the updated state and metadata files onto main + cp /tmp/state.json "$DEPLOY_DIR/state.json" 2>/dev/null || true + cp /tmp/metadata.json "$DEPLOY_DIR/metadata.json" 2>/dev/null || true + + git add "$DEPLOY_DIR/state.json" "$DEPLOY_DIR/metadata.json" + git diff --cached --quiet || git commit -m "git-ape: update state for ${{ matrix.deployment_id }} [$STATUS]" + git push || echo "::warning::Could not push state update to main" + + - name: Post deployment result + if: always() + uses: actions/github-script@v9 + env: + DEPLOY_ERROR: ${{ steps.deploy.outputs.deploy_error }} + TEST_ENDPOINTS: ${{ steps.tests.outputs.test_endpoints }} + RESOURCES_JSON: ${{ steps.tests.outputs.resources }} + with: + script: | + const deploymentId = '${{ matrix.deployment_id }}'; + const status = '${{ steps.deploy.outputs.deploy_status }}' || 'failed'; + const duration = '${{ steps.deploy.outputs.deploy_duration }}'; + const rollbackAction = '${{ steps.rollback.outputs.rollback_action }}' || 'none'; + const rollbackStatus = '${{ steps.rollback.outputs.rollback_status }}' || 'not-attempted'; + const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`; + const resources = process.env.RESOURCES_JSON || ''; + const testEndpoints = process.env.TEST_ENDPOINTS || ''; + const deployError = process.env.DEPLOY_ERROR || ''; + + // Build the comment body + let body = `## Git-Ape Deploy: \`${deploymentId}\`\n\n`; + + if (status === 'succeeded') { + body += `### āœ… Deployment Succeeded\n\n`; + body += `- **Duration:** ${duration}\n`; + body += `- **Workflow Run:** [View logs](${runUrl})\n\n`; + + if (testEndpoints) body += `### Endpoints\n\n${testEndpoints}\n\n`; + + if (resources) { + try { + const parsed = JSON.parse(resources); + body += `### Resources (${parsed.length})\n\n| Name | Type | Status |\n|------|------|--------|\n`; + for (const r of parsed) { + const icon = r.provisioningState === 'Succeeded' ? 'āœ…' : 'āš ļø'; + body += `| ${r.name} | ${r.type} | ${icon} ${r.provisioningState} |\n`; + } + body += '\n'; + } catch {} + } + } else { + body += `### āŒ Deployment Failed\n\n`; + body += `- **Workflow Run:** [View logs](${runUrl})\n`; + + // Rollback summary + const rbIcon = rollbackStatus === 'succeeded' ? 'āœ…' + : rollbackStatus === 'failed' ? 'āŒ' + : 'āš ļø'; + const rbText = { + 'redeploy-previous': 'Redeployed previous template (last-known-good)', + 'delete-failed-stack': 'Deleted partially-provisioned stack (clean slate)', + 'manual-required': 'Manual intervention required — no previous template in git history', + 'none': 'No rollback attempted', + }[rollbackAction] || rollbackAction; + body += `- **Rollback:** ${rbIcon} \`${rollbackAction}\` — ${rbText} (*${rollbackStatus}*)\n\n`; + + if (deployError) { + body += `
Error output\n\n\`\`\`\n${deployError.substring(0, 4000)}\n\`\`\`\n
\n\n`; + } + + body += `### Next steps\n\n`; + if (rollbackStatus === 'succeeded' && rollbackAction === 'redeploy-previous') { + body += `- Environment is restored to the previous known-good state.\n`; + body += `- Fix the template and push a new commit — CI will redeploy automatically.\n`; + } else if (rollbackStatus === 'succeeded' && rollbackAction === 'delete-failed-stack') { + body += `- No resources are provisioned. Safe to iterate on the template and redeploy.\n`; + } else { + body += `- āš ļø Manual cleanup may be required. Inspect the stack with:\n`; + body += ` \`\`\`bash\n az stack sub show --name ${deploymentId} -o table\n \`\`\`\n`; + } + } + + const marker = ``; + body = marker + '\n' + body; + + // Find the target PR. On issue_comment we have it; on push we find it from the SHA. + let prNumber = null; + if (context.eventName === 'issue_comment') { + prNumber = context.issue.number; + } else if (context.eventName === 'push') { + const sha = context.sha; + const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: sha, + }); + if (prs.length > 0) prNumber = prs[0].number; + } + + if (!prNumber) { + core.info('No PR associated with this run — skipping PR comment.'); + return; + } + + // Post the comment (new comment each run; merged PRs still accept comments) + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body, + }); + core.info(`Posted deployment result comment on PR #${prNumber}`); + + // On failure, try to reopen the PR so the team notices. + // Merged PRs cannot be reopened — file a tracking issue instead. + if (status !== 'succeeded') { + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + if (pr.state === 'closed' && !pr.merged) { + try { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'open', + }); + core.info(`Reopened PR #${prNumber} due to deployment failure`); + } catch (e) { + core.warning(`Could not reopen PR #${prNumber}: ${e.message}`); + } + } else if (pr.merged) { + // Cannot reopen a merged PR — open a tracking issue referencing the PR + const issueTitle = `Deployment failed: ${deploymentId} (from PR #${prNumber})`; + const issueBody = `The deployment for \`${deploymentId}\` failed after PR #${prNumber} was merged.\n\n` + + `- **Rollback:** \`${rollbackAction}\` (${rollbackStatus})\n` + + `- **Workflow run:** ${runUrl}\n` + + `- **Merged PR:** #${prNumber}\n\n` + + `See the comment on PR #${prNumber} for full details.`; + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: ['deployment-failed', 'git-ape'], + }); + core.info(`Created tracking issue #${issue.number} for merged-PR deployment failure`); + } + } + + - name: Notify via Slack + if: always() + continue-on-error: true + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [[ -z "$SLACK_WEBHOOK_URL" ]]; then exit 0; fi + + STATUS="${{ steps.deploy.outputs.deploy_status }}" + DEPLOY_ID="${{ matrix.deployment_id }}" + DURATION="${{ steps.deploy.outputs.deploy_duration }}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [[ "$STATUS" == "succeeded" ]]; then + EMOJI="āœ…" + MSG="Deployment *$DEPLOY_ID* succeeded in $DURATION" + else + EMOJI="āŒ" + MSG="Deployment *$DEPLOY_ID* failed" + fi + + curl -sf -X POST "$SLACK_WEBHOOK_URL" \ + -H 'Content-type: application/json' \ + -d "{ + \"text\": \"$EMOJI $MSG\", + \"blocks\": [ + { + \"type\": \"section\", + \"text\": { + \"type\": \"mrkdwn\", + \"text\": \"$EMOJI *Git-Ape Deploy: $DEPLOY_ID*\\n\\n$MSG\\n\\nTriggered by: ${{ github.actor }}\\n<$RUN_URL|View logs>\" + } + } + ] + }" || echo "::warning::Slack notification failed" diff --git a/.github/workflows/git-ape-destroy.exampleyml b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml similarity index 65% rename from .github/workflows/git-ape-destroy.exampleyml rename to .github/skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml index 1afc7ae..89469fd 100644 --- a/.github/workflows/git-ape-destroy.exampleyml +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml @@ -131,17 +131,24 @@ jobs: exit 1 fi + # Stacks-only: stackId is the single source of truth. If it's missing + # this deployment wasn't created via Deployment Stacks and can't be + # destroyed by this workflow. + STACK_ID=$(jq -r '.stackId // empty' "$STATE_FILE") + STACK_NAME=$(jq -r '.deploymentId // empty' "$STATE_FILE") RG_NAME=$(jq -r '.resourceGroup // empty' "$STATE_FILE") - if [[ -z "$RG_NAME" ]]; then - echo "::error::No resource group found in state file" + if [[ -z "$STACK_ID" && -z "$STACK_NAME" ]]; then + echo "::error::state.json has no stackId or deploymentId — cannot destroy" echo "found=false" >> "$GITHUB_OUTPUT" exit 1 fi echo "found=true" >> "$GITHUB_OUTPUT" + echo "stack_id=$STACK_ID" >> "$GITHUB_OUTPUT" + echo "stack_name=$STACK_NAME" >> "$GITHUB_OUTPUT" echo "resource_group=$RG_NAME" >> "$GITHUB_OUTPUT" - echo "Will destroy resource group: $RG_NAME" + echo "Will destroy deployment stack: $STACK_NAME (${STACK_ID:-by name})" - name: Azure Login (OIDC) if: steps.state.outputs.found == 'true' @@ -149,137 +156,69 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Build destroy plan + - name: Inventory managed resources id: check if: steps.state.outputs.found == 'true' run: | - RG="${{ steps.state.outputs.resource_group }}" - DEPLOYMENT_ID="${{ matrix.deployment_id }}" - - # Check if resource group exists - EXISTS=$(az group exists --name "$RG") - echo "exists=$EXISTS" >> "$GITHUB_OUTPUT" + STACK_NAME="${{ steps.state.outputs.stack_name }}" - if [[ "$EXISTS" != "true" ]]; then - echo "Resource group $RG does not exist (already deleted?)" + # Read live managed-resource list from the stack itself. + # Stacks are idempotent: if the stack is already gone we record that and exit cleanly. + if ! STACK_JSON=$(az stack sub show --name "$STACK_NAME" --output json 2>/dev/null); then + echo "Stack $STACK_NAME not found (already destroyed?)" + echo "exists=false" >> "$GITHUB_OUTPUT" echo "resource_count=0" >> "$GITHUB_OUTPUT" - echo "sub_count=0" >> "$GITHUB_OUTPUT" exit 0 fi - # Inventory RG resources - RESOURCES=$(az resource list --resource-group "$RG" \ - --query "[].{name:name, type:type, id:id, provisioningState:provisioningState}" \ - --output json 2>/dev/null || echo "[]") - RESOURCE_COUNT=$(echo "$RESOURCES" | jq 'length') + echo "exists=true" >> "$GITHUB_OUTPUT" - echo "resource_count=$RESOURCE_COUNT" >> "$GITHUB_OUTPUT" + RESOURCES=$(echo "$STACK_JSON" | jq -c '[(.resources // [])[] | {id: .id, status: .status}]') + COUNT=$(echo "$RESOURCES" | jq 'length') + + echo "resource_count=$COUNT" >> "$GITHUB_OUTPUT" echo "resources<> "$GITHUB_OUTPUT" echo "$RESOURCES" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" - echo "Resource group $RG has $RESOURCE_COUNT resources" - echo "$RESOURCES" | jq -r '.[] | " - \(.type)/\(.name) (\(.provisioningState))"' - - # Query deployment operations to find subscription-scoped resources - # These are NOT deleted by az group delete (e.g. role assignments, policy assignments) - SUB_RESOURCES="[]" - - OPS=$(az deployment operation sub list \ - --name "$DEPLOYMENT_ID" \ - --query "[?properties.provisioningState=='Succeeded' && properties.targetResource.id != null].properties.targetResource" \ - -o json 2>/dev/null || echo "[]") - - if [[ "$OPS" != "[]" ]]; then - # Find subscription-scoped authorization/policy resources (role assignments, etc.) - # These live outside the RG and survive az group delete - SUB_RESOURCES=$(echo "$OPS" | jq -c '[ - .[] | select( - (.resourceType // "" | test("Microsoft.Authorization|Microsoft.Policy")) and - (.id // "" | test("/resourceGroups/") | not) - ) - ]') - - # Check nested deployments for RG-scoped role assignments too - NESTED_NAMES=$(echo "$OPS" | jq -r '[ - .[] | select(.resourceType == "Microsoft.Resources/deployments") - ] | .[].resourceName // empty') - - for NESTED_NAME in $NESTED_NAMES; do - NESTED_OPS=$(az deployment operation group list \ - --resource-group "$RG" --name "$NESTED_NAME" \ - --query "[?properties.provisioningState=='Succeeded' && properties.targetResource.id != null].properties.targetResource" \ - -o json 2>/dev/null || echo "[]") - - # Role assignments scoped to resources within the RG - NESTED_AUTH=$(echo "$NESTED_OPS" | jq -c '[ - .[] | select( - (.resourceType // "" | test("Microsoft.Authorization")) - ) - ]') - - SUB_RESOURCES=$(jq -n --argjson a "$SUB_RESOURCES" --argjson b "$NESTED_AUTH" '$a + $b') - done - fi - - SUB_COUNT=$(echo "$SUB_RESOURCES" | jq 'length') - - echo "sub_count=$SUB_COUNT" >> "$GITHUB_OUTPUT" - echo "sub_resources<> "$GITHUB_OUTPUT" - echo "$SUB_RESOURCES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - echo "" echo "=== Destroy Plan ===" - echo "Resource group: $RG ($RESOURCE_COUNT resources)" - echo "Subscription-scoped resources: $SUB_COUNT" - if [[ "$SUB_COUNT" -gt 0 ]]; then - echo "$SUB_RESOURCES" | jq -r '.[] | " - \(.resourceType): \(.resourceName) (\(.id))"' - fi + echo "Stack: $STACK_NAME" + echo "Managed resources: $COUNT" + echo "$RESOURCES" | jq -r '.[] | " - \(.id) [\(.status)]"' echo "===================" - - name: Delete subscription-scoped resources - id: destroy_sub - if: steps.check.outputs.exists == 'true' && steps.check.outputs.sub_count != '0' - run: | - echo "šŸ—‘ļø Deleting subscription-scoped resources first..." - FAILED=0 - - echo '${{ steps.check.outputs.sub_resources }}' | jq -r '.[].id' | while read -r RESOURCE_ID; do - echo " Deleting: $RESOURCE_ID" - if ! az resource delete --ids "$RESOURCE_ID" 2>&1; then - echo "::warning::Failed to delete $RESOURCE_ID" - FAILED=$((FAILED + 1)) - fi - done - - if [[ "$FAILED" -gt 0 ]]; then - echo "::warning::$FAILED subscription-scoped resource(s) failed to delete" - fi - - - name: Delete resource group + - name: Delete deployment stack id: destroy if: steps.check.outputs.exists == 'true' run: | - RG="${{ steps.state.outputs.resource_group }}" - echo "šŸ—‘ļø Deleting resource group: $RG" - echo "This will block until the resource group is fully deleted..." + STACK_NAME="${{ steps.state.outputs.stack_name }}" + echo "šŸ—‘ļø Deleting deployment stack: $STACK_NAME" + echo " --action-on-unmanage deleteAll — removes every resource (across RGs / sub scope) the stack manages" + echo " This will block until all managed resources are fully deleted..." START_TIME=$(date +%s) - az group delete --name "$RG" --yes 2>&1 || { + # --bypass-stack-out-of-sync-error: a destroyed run is one-shot; we + # don't need the safety check that protects against stale manifests + # during iterative updates. + if ! az stack sub delete \ + --name "$STACK_NAME" \ + --action-on-unmanage deleteAll \ + --bypass-stack-out-of-sync-error true \ + --yes 2>&1; then echo "destroy_status=failed" >> "$GITHUB_OUTPUT" - echo "::error::Failed to delete resource group $RG" + echo "::error::Failed to delete deployment stack $STACK_NAME" exit 1 - } + fi END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo "destroy_status=succeeded" >> "$GITHUB_OUTPUT" echo "destroy_duration=${DURATION}s" >> "$GITHUB_OUTPUT" - echo "āœ… Resource group deleted in ${DURATION}s: $RG" + echo "āœ… Stack deleted in ${DURATION}s: $STACK_NAME" - name: Update deployment state if: always() && steps.state.outputs.found == 'true' @@ -322,11 +261,11 @@ jobs: if: always() run: | DEPLOY_ID="${{ matrix.deployment_id }}" + STACK="${{ steps.state.outputs.stack_name }}" RG="${{ steps.state.outputs.resource_group }}" STATUS="${{ steps.destroy.outputs.destroy_status }}" DURATION="${{ steps.destroy.outputs.destroy_duration }}" RESOURCE_COUNT="${{ steps.check.outputs.resource_count }}" - SUB_COUNT="${{ steps.check.outputs.sub_count }}" EXISTS="${{ steps.check.outputs.exists }}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" @@ -334,11 +273,12 @@ jobs: echo "Git-Ape Destroy Summary" echo "============================================" echo "Deployment: $DEPLOY_ID" + echo "Stack: $STACK" echo "Resource Group: $RG" if [[ "$EXISTS" == "false" ]]; then - echo "Result: Already destroyed" + echo "Result: Already destroyed (stack not found)" elif [[ "$STATUS" == "succeeded" ]]; then - echo "Result: āœ… Destroyed ($RESOURCE_COUNT RG resources + $SUB_COUNT subscription-scoped)" + echo "Result: āœ… Destroyed ($RESOURCE_COUNT managed resources)" echo "Duration: $DURATION" else echo "Result: āŒ Failed" @@ -355,16 +295,16 @@ jobs: if [[ -z "$SLACK_WEBHOOK_URL" ]]; then exit 0; fi DEPLOY_ID="${{ matrix.deployment_id }}" - RG="${{ steps.state.outputs.resource_group }}" + STACK="${{ steps.state.outputs.stack_name }}" STATUS="${{ steps.destroy.outputs.destroy_status }}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" if [[ "$STATUS" == "succeeded" ]]; then EMOJI="šŸ—‘ļø" - MSG="Resource group *$RG* ($DEPLOY_ID) destroyed" + MSG="Deployment stack *$STACK* ($DEPLOY_ID) destroyed" else EMOJI="āŒ" - MSG="Destroy failed for *$RG* ($DEPLOY_ID)" + MSG="Destroy failed for stack *$STACK* ($DEPLOY_ID)" fi curl -sf -X POST "$SLACK_WEBHOOK_URL" \ diff --git a/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml new file mode 100644 index 0000000..d6ea7aa --- /dev/null +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml @@ -0,0 +1,1506 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e69fef083f92a9ea5e1d997c17da168d0c628110b25472f88c235251e6f56309","compiler_version":"v0.76.1","agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["AZURE_CLIENT_ID","AZURE_TENANT_ID","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"azure/login","sha":"1384c340ab2dda50fed2bee3041d1d87018aa5e8","version":"v2"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Continuous drift remediation workflow for Git-Ape deployments. Runs daily +# to detect configuration drift between Azure resources and stored deployment +# state, classifies changes by severity, and creates PRs for human review. +# +# Secrets used: +# - AZURE_CLIENT_ID +# - AZURE_TENANT_ID +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 # v2 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Continuous Drift Remediation" +on: + schedule: + - cron: "6 6 * * *" + # Friendly format: daily around 06:00 (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Continuous Drift Remediation" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "false" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "git-ape-drift.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + {{#runtime-import .github/workflows/git-ape-drift.md}} + GH_AW_PROMPT_a9df7e8899c2c448_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: ' Drift detection state — tracks last-seen drift per deployment to implement anti-flapping logic' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: gitapedrift + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Azure Login (OIDC) + uses: azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 # v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + - name: Snapshot current Azure state for all tracked deployments + run: "set -euo pipefail\nmkdir -p /tmp/drift-snapshots\nfor dir in .azure/deployments/*/; do\n [ -f \"$dir/state.json\" ] || continue\n [ -f \"$dir/metadata.json\" ] || continue\n id=$(basename \"$dir\")\n meta_status=$(jq -r '.status // \"\"' \"$dir/metadata.json\")\n # Skip only explicitly destroyed / destroy-requested — everything\n # else (succeeded, failed, partial) may have live resources we must\n # track for drift. Deploy failures frequently leave partial infra\n # behind, and users legitimately modify those resources manually.\n if [ \"$meta_status\" = \"destroyed\" ] || [ \"$meta_status\" = \"destroy-requested\" ]; then\n echo \"{\\\"status\\\":\\\"skipped\\\",\\\"reason\\\":\\\"metadata.status=$meta_status\\\"}\" > \"/tmp/drift-snapshots/${id}.status.json\"\n continue\n fi\n # Resolve resource group: prefer state.json, fall back to metadata.json\n # (state.json .resourceGroup is empty for failed deploys and some\n # succeeded-but-buggy deploys like helloworld-containerapp-dev).\n rg=$(jq -r '.resourceGroup // empty' \"$dir/state.json\")\n if [ -z \"$rg\" ]; then\n rg=$(jq -r '.resourceGroup // empty' \"$dir/metadata.json\")\n fi\n if [ -z \"$rg\" ]; then\n echo \"{\\\"status\\\":\\\"no-resource-group\\\",\\\"reason\\\":\\\"neither state.json nor metadata.json has a resourceGroup\\\"}\" > \"/tmp/drift-snapshots/${id}.status.json\"\n continue\n fi\n snapshot=\"/tmp/drift-snapshots/${id}.json\"\n details_file=\"/tmp/drift-snapshots/${id}.details.json\"\n dns_file=\"/tmp/drift-snapshots/${id}.dns.json\"\n status_file=\"/tmp/drift-snapshots/${id}.status.json\"\n err_file=\"/tmp/drift-snapshots/${id}.err\"\n echo \"Snapshotting $id (rg=$rg, meta_status=$meta_status)\"\n # Redirect stdout → snapshot, stderr → err_file, so we can\n # distinguish \"RG missing in Azure\" from \"RG exists but is empty\"\n # AND preserve the full az error message verbatim.\n if az resource list --resource-group \"$rg\" -o json > \"$snapshot\" 2> \"$err_file\"; then\n count=$(jq 'length' \"$snapshot\")\n echo \"{\\\"status\\\":\\\"ok\\\",\\\"rg\\\":\\\"$rg\\\",\\\"resourceCount\\\":$count,\\\"metaStatus\\\":\\\"$meta_status\\\"}\" > \"$status_file\"\n # Property-level snapshot: fetch full bodies for every resource so\n # the agent can detect drift inside properties (SKUs, tags,\n # firewall rules, TLS versions, etc.), not just existence.\n echo \"[]\" > \"$details_file\"\n if [ \"$count\" -gt 0 ]; then\n jq -r '.[] | .id' \"$snapshot\" | while read -r rid; do\n [ -z \"$rid\" ] && continue\n az resource show --ids \"$rid\" -o json 2>/dev/null || true\n done | jq -s '.' > \"$details_file.tmp\" 2>/dev/null && mv \"$details_file.tmp\" \"$details_file\" || echo \"[]\" > \"$details_file\"\n fi\n # Private DNS zones: record sets are sub-resources not returned by\n # `az resource list`, so query them explicitly per zone.\n echo \"{}\" > \"$dns_file\"\n jq -r '.[] | select(.type==\"Microsoft.Network/privateDnsZones\") | .name' \"$snapshot\" | while read -r zone; do\n [ -z \"$zone\" ] && continue\n records=$(az network private-dns record-set list -g \"$rg\" -z \"$zone\" -o json 2>/dev/null || echo \"[]\")\n jq --arg z \"$zone\" --argjson r \"$records\" '. + {($z): $r}' \"$dns_file\" > \"$dns_file.tmp\" && mv \"$dns_file.tmp\" \"$dns_file\"\n done\n else\n err=$(cat \"$err_file\")\n err_escaped=$(printf '%s' \"$err\" | head -c 1000 | jq -Rs .)\n echo \"[]\" > \"$snapshot\"\n echo \"[]\" > \"$details_file\"\n echo \"{}\" > \"$dns_file\"\n if printf '%s' \"$err\" | grep -q 'ResourceGroupNotFound'; then\n code=\"rg-not-found\"\n elif printf '%s' \"$err\" | grep -qi 'AuthenticationFailed\\|AuthorizationFailed'; then\n code=\"auth-failed\"\n else\n code=\"az-error\"\n fi\n echo \"{\\\"status\\\":\\\"$code\\\",\\\"rg\\\":\\\"$rg\\\",\\\"metaStatus\\\":\\\"$meta_status\\\",\\\"error\\\":$err_escaped}\" > \"$status_file\"\n fi\n rm -f \"$err_file\"\ndone\n# Move snapshots into workspace so the agent can read them\nmkdir -p .drift-snapshots\ncp /tmp/drift-snapshots/*.json .drift-snapshots/ 2>/dev/null || true\nls -la .drift-snapshots/ || true\n\n# Build an inventory markdown the agent is guaranteed to read first,\n# so it cannot claim the snapshots are missing.\n{\n echo \"# Drift Snapshot Inventory\"\n echo \"\"\n echo \"**Pre-step ran at:** $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n echo \"**Workspace:** $GITHUB_WORKSPACE\"\n echo \"**Snapshot directory:** .drift-snapshots/ (repo-root-relative)\"\n echo \"\"\n echo \"## Files produced\"\n echo \"\"\n (cd .drift-snapshots && ls -la) | sed 's/^/ /'\n echo \"\"\n echo \"## Per-deployment status\"\n echo \"\"\n for f in .drift-snapshots/*.status.json; do\n [ -f \"$f\" ] || continue\n did=$(basename \"$f\" .status.json)\n st=$(jq -r '.status' \"$f\")\n rg=$(jq -r '.rg // .reason // \"\"' \"$f\")\n rc=$(jq -r '.resourceCount // \"n/a\"' \"$f\")\n err=$(jq -r '.error // \"\"' \"$f\" | head -c 300)\n echo \"### \\`$did\\`\"\n echo \"\"\n echo \"- **Status:** \\`$st\\`\"\n echo \"- **Resource group / reason:** \\`$rg\\`\"\n echo \"- **Live resource count:** \\`$rc\\`\"\n if [ -n \"$err\" ] && [ \"$err\" != \"null\" ]; then\n echo \"- **Captured error:**\"\n echo \"\"\n echo \"\\`\\`\\`\"\n echo \"$err\"\n echo \"\\`\\`\\`\"\n fi\n # Companion file sizes (helps agent know details.json and dns.json exist)\n for ext in json details.json dns.json; do\n fp=\".drift-snapshots/${did}.${ext}\"\n if [ -f \"$fp\" ]; then\n sz=$(wc -c < \"$fp\" | tr -d ' ')\n echo \"- \\`$fp\\`: ${sz} bytes\"\n fi\n done\n echo \"\"\n done\n} > .drift-snapshots/INVENTORY.md\necho \"---- INVENTORY.md ----\"\ncat .drift-snapshots/INVENTORY.md\n" + shell: bash + + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f0c1474fe8a872a2_EOF' + {"create_issue":{"close_older_issues":true,"labels":["drift-status"],"max":1,"title_prefix":"[drift-status] "},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_f0c1474fe8a872a2_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[drift-status] \". Labels [\"drift-status\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "fields": { + "type": "array" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_cea132e2569a1a51_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_cea132e2569a1a51_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + env: + GH_AW_CLEAN_AZURE: "true" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + bash "${RUNNER_TEMP}/gh-aw/actions/clean_known_action_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(diff) + # --allow-tool shell(echo) + # --allow-tool shell(find) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(printf) + # --allow-tool shell(pwd) + # --allow-tool shell(safeoutputs:*) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(diff)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'AZURE_CLIENT_ID,AZURE_TENANT_ID,COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + SECRET_AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,portal.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_GITHUB_REFS: "" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + include-hidden-files: true + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-git-ape-drift" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "git-ape-drift" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + GH_AW_CACHE_MEMORY_ENABLED: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Continuous Drift Remediation" + WORKFLOW_DESCRIPTION: "Continuous drift remediation workflow for Git-Ape deployments. Runs daily\nto detect configuration drift between Azure resources and stored deployment\nstate, classifies changes by severity, and creates PRs for human review." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/git-ape-drift" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "git-ape-drift" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,portal.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"drift-status\"],\"max\":1,\"title_prefix\":\"[drift-status] \"},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: always() && needs.detection.result == 'success' && needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: gitapedrift + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md new file mode 100644 index 0000000..d7fa0d3 --- /dev/null +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md @@ -0,0 +1,500 @@ +--- +description: | + Continuous drift remediation workflow for Git-Ape deployments. Runs daily + to detect configuration drift between Azure resources and stored deployment + state, classifies changes by severity, and creates PRs for human review. + +strict: false + +on: + schedule: daily around 06:00 + workflow_dispatch: + +permissions: + contents: read + issues: read + pull-requests: read + id-token: write # required for Azure OIDC federation + +# Deterministic pre-steps run OUTSIDE the agent sandbox. +# They authenticate to Azure via OIDC and dump current resource state +# to workspace files. The agent then reads these files to reason about +# drift — no Azure credentials or `az` CLI reach the sandbox. +steps: + - name: Azure Login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Snapshot current Azure state for all tracked deployments + shell: bash + run: | + set -euo pipefail + mkdir -p /tmp/drift-snapshots + for dir in .azure/deployments/*/; do + [ -f "$dir/state.json" ] || continue + [ -f "$dir/metadata.json" ] || continue + id=$(basename "$dir") + meta_status=$(jq -r '.status // ""' "$dir/metadata.json") + # Skip only explicitly destroyed / destroy-requested — everything + # else (succeeded, failed, partial) may have live resources we must + # track for drift. Deploy failures frequently leave partial infra + # behind, and users legitimately modify those resources manually. + if [ "$meta_status" = "destroyed" ] || [ "$meta_status" = "destroy-requested" ]; then + echo "{\"status\":\"skipped\",\"reason\":\"metadata.status=$meta_status\"}" > "/tmp/drift-snapshots/${id}.status.json" + continue + fi + # Resolve resource group: prefer state.json, fall back to metadata.json + # (state.json .resourceGroup is empty for failed deploys and some + # succeeded-but-buggy deploys like helloworld-containerapp-dev). + rg=$(jq -r '.resourceGroup // empty' "$dir/state.json") + if [ -z "$rg" ]; then + rg=$(jq -r '.resourceGroup // empty' "$dir/metadata.json") + fi + if [ -z "$rg" ]; then + echo "{\"status\":\"no-resource-group\",\"reason\":\"neither state.json nor metadata.json has a resourceGroup\"}" > "/tmp/drift-snapshots/${id}.status.json" + continue + fi + snapshot="/tmp/drift-snapshots/${id}.json" + details_file="/tmp/drift-snapshots/${id}.details.json" + dns_file="/tmp/drift-snapshots/${id}.dns.json" + status_file="/tmp/drift-snapshots/${id}.status.json" + err_file="/tmp/drift-snapshots/${id}.err" + echo "Snapshotting $id (rg=$rg, meta_status=$meta_status)" + # Redirect stdout → snapshot, stderr → err_file, so we can + # distinguish "RG missing in Azure" from "RG exists but is empty" + # AND preserve the full az error message verbatim. + if az resource list --resource-group "$rg" -o json > "$snapshot" 2> "$err_file"; then + count=$(jq 'length' "$snapshot") + echo "{\"status\":\"ok\",\"rg\":\"$rg\",\"resourceCount\":$count,\"metaStatus\":\"$meta_status\"}" > "$status_file" + # Property-level snapshot: fetch full bodies for every resource so + # the agent can detect drift inside properties (SKUs, tags, + # firewall rules, TLS versions, etc.), not just existence. + echo "[]" > "$details_file" + if [ "$count" -gt 0 ]; then + jq -r '.[] | .id' "$snapshot" | while read -r rid; do + [ -z "$rid" ] && continue + az resource show --ids "$rid" -o json 2>/dev/null || true + done | jq -s '.' > "$details_file.tmp" 2>/dev/null && mv "$details_file.tmp" "$details_file" || echo "[]" > "$details_file" + fi + # Private DNS zones: record sets are sub-resources not returned by + # `az resource list`, so query them explicitly per zone. + echo "{}" > "$dns_file" + jq -r '.[] | select(.type=="Microsoft.Network/privateDnsZones") | .name' "$snapshot" | while read -r zone; do + [ -z "$zone" ] && continue + records=$(az network private-dns record-set list -g "$rg" -z "$zone" -o json 2>/dev/null || echo "[]") + jq --arg z "$zone" --argjson r "$records" '. + {($z): $r}' "$dns_file" > "$dns_file.tmp" && mv "$dns_file.tmp" "$dns_file" + done + else + err=$(cat "$err_file") + err_escaped=$(printf '%s' "$err" | head -c 1000 | jq -Rs .) + echo "[]" > "$snapshot" + echo "[]" > "$details_file" + echo "{}" > "$dns_file" + if printf '%s' "$err" | grep -q 'ResourceGroupNotFound'; then + code="rg-not-found" + elif printf '%s' "$err" | grep -qi 'AuthenticationFailed\|AuthorizationFailed'; then + code="auth-failed" + else + code="az-error" + fi + echo "{\"status\":\"$code\",\"rg\":\"$rg\",\"metaStatus\":\"$meta_status\",\"error\":$err_escaped}" > "$status_file" + fi + rm -f "$err_file" + done + # Move snapshots into workspace so the agent can read them + mkdir -p .drift-snapshots + cp /tmp/drift-snapshots/*.json .drift-snapshots/ 2>/dev/null || true + ls -la .drift-snapshots/ || true + + # Build an inventory markdown the agent is guaranteed to read first, + # so it cannot claim the snapshots are missing. + { + echo "# Drift Snapshot Inventory" + echo "" + echo "**Pre-step ran at:** $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "**Workspace:** $GITHUB_WORKSPACE" + echo "**Snapshot directory:** .drift-snapshots/ (repo-root-relative)" + echo "" + echo "## Files produced" + echo "" + (cd .drift-snapshots && ls -la) | sed 's/^/ /' + echo "" + echo "## Per-deployment status" + echo "" + for f in .drift-snapshots/*.status.json; do + [ -f "$f" ] || continue + did=$(basename "$f" .status.json) + st=$(jq -r '.status' "$f") + rg=$(jq -r '.rg // .reason // ""' "$f") + rc=$(jq -r '.resourceCount // "n/a"' "$f") + err=$(jq -r '.error // ""' "$f" | head -c 300) + echo "### \`$did\`" + echo "" + echo "- **Status:** \`$st\`" + echo "- **Resource group / reason:** \`$rg\`" + echo "- **Live resource count:** \`$rc\`" + if [ -n "$err" ] && [ "$err" != "null" ]; then + echo "- **Captured error:**" + echo "" + echo "\`\`\`" + echo "$err" + echo "\`\`\`" + fi + # Companion file sizes (helps agent know details.json and dns.json exist) + for ext in json details.json dns.json; do + fp=".drift-snapshots/${did}.${ext}" + if [ -f "$fp" ]; then + sz=$(wc -c < "$fp" | tr -d ' ') + echo "- \`$fp\`: ${sz} bytes" + fi + done + echo "" + done + } > .drift-snapshots/INVENTORY.md + echo "---- INVENTORY.md ----" + cat .drift-snapshots/INVENTORY.md + +tools: + edit: + bash: + - "jq *" + - "cat *" + - "ls *" + - "find *" + - "date *" + - "echo *" + - "diff *" + - "sort *" + - "grep *" + - "head *" + - "tail *" + - "wc *" + cache-memory: + description: "Drift detection state — tracks last-seen drift per deployment to implement anti-flapping logic" + +safe-outputs: + mentions: false + allowed-github-references: [] + # Allow Azure Portal deep links in issue bodies (default sanitizer + # replaces any non-allowlisted URL with "(redacted)"). "defaults" + # keeps the baseline GitHub/infrastructure allowlist. + allowed-domains: + - defaults + - portal.azure.com + create-issue: + title-prefix: "[drift-status] " + labels: [drift-status] + close-older-issues: true + max: 1 +--- + +# Continuous Drift Remediation + +Detect configuration drift across all active Git-Ape deployments and report +findings in a single rolling status issue for human review. + +## Context + +This workflow implements the agentic drift remediation vision from the +[Platform Engineering for the Agentic AI Era](https://devblogs.microsoft.com/all-things-azure/platform-engineering-for-the-agentic-ai-era/) +manifesto. Drift detection is continuous, contextual, and adaptive — agents +reason about drift, classify it, and propose fixes for human approval. + +## Deployment Discovery + +1. Scan `.azure/deployments/` for all directories containing a `state.json` +2. Skip deployments where `metadata.json` has `"status": "destroyed"` or `"status": "destroy-requested"` — those are intentionally gone. +3. For **every other** deployment (including `failed` — failed deploys often leave partial infrastructure behind that users then modify manually), attempt drift detection. Resolve the resource group from `state.json .resourceGroup`, falling back to `metadata.json .resourceGroup` when empty. + +## Drift Detection Process + +### STEP 0 — MANDATORY: Verify snapshots exist before doing anything else + +The pre-step job (`Snapshot current Azure state for all tracked deployments`) runs **before** this agent step and writes snapshot files to the workspace at `.drift-snapshots/` (relative to repo root, i.e. `$GITHUB_WORKSPACE/.drift-snapshots/`). You MUST begin your analysis by running these shell commands **in order**: + +```bash +ls -la .drift-snapshots/ +cat .drift-snapshots/INVENTORY.md +``` + +The `INVENTORY.md` file is authoritative — it is generated by the pre-step and lists **every** snapshot file that was produced, the status of each, and any captured errors. If `INVENTORY.md` exists, **the pre-step ran successfully** — do NOT claim "no snapshots were produced" or "drift pre-step did not run". If a specific deployment's status is `az-error`, use the captured `.error` field from its `.status.json` to explain the failure — do not invent a cause. + +Only if `ls .drift-snapshots/` returns a "No such file or directory" error is the pre-step genuinely missing. In that case — and only in that case — report it as an infrastructure failure. Any other interpretation is a hallucination and must be avoided. + +The snapshot file paths you will read (all relative to repo root): +- `.drift-snapshots/INVENTORY.md` — human-readable summary (read this first) +- `.drift-snapshots/.status.json` — status + error code + error text +- `.drift-snapshots/.json` — flat `az resource list` output (existence diff) +- `.drift-snapshots/.details.json` — full `az resource show` bodies (property diff) +- `.drift-snapshots/.dns.json` — private DNS record sets (DNS diff) + +### Per-deployment workflow + +For each deployment: + +1. **Load desired state** — Read `template.json`, `state.json`, and `metadata.json`. +2. **Load snapshot status** — Read `.drift-snapshots/.status.json`. Its `status` field is one of: + - `ok` — `.drift-snapshots/.json` contains the live resource list. Proceed with drift comparison. + - `rg-not-found` — resource group no longer exists in Azure. Report as **Critical** finding (stale IaC state: resources were deleted outside Git-Ape). Recommend running the destroy workflow to reconcile state. + - `auth-failed` — the pre-step could not authenticate. Report as a snapshot failure; do not classify as drift. + - `az-error` — some other `az` CLI error. Include the captured `.error` in the snapshot-failures section verbatim. + - `no-resource-group` — neither `state.json` nor `metadata.json` has a resource group. Report as a data-quality issue in the snapshot-failures section. + - `skipped` — deployment intentionally skipped (destroyed / destroy-requested). Omit from drift analysis. + - File missing entirely — pre-step didn't run for this deployment; treat as `az-error` with message "no snapshot produced". +3. **Compare** (only when status is `ok`) — Perform **all three** passes and report every finding: + - **Existence diff** — enumerate resources in `template.json` vs `.drift-snapshots/.json` by type+name. Missing / extra resources are drift. + - **Property diff — MANDATORY, every resource, every property** — `.drift-snapshots/.details.json` is an array of full `az resource show` outputs. For **each** live resource, walk its `properties` object recursively and compare against the corresponding `properties` block in the IaC `template.json` resource entry. Report **every** value mismatch you find. Examples of properties that MUST be diffed (non-exhaustive): + - **Network Interfaces** — `dnsSettings.dnsServers`, `dnsSettings.internalDnsNameLabel`, `enableAcceleratedNetworking`, `enableIPForwarding`, `ipConfigurations[].properties.{privateIPAddress,privateIPAllocationMethod,subnet.id,publicIPAddress.id,loadBalancerBackendAddressPools,applicationSecurityGroups}`, `networkSecurityGroup.id` + - **Virtual Machines** — `hardwareProfile.vmSize`, `storageProfile.imageReference.*`, `storageProfile.osDisk.{createOption,caching,managedDisk.storageAccountType,diskSizeGB}`, `osProfile.{computerName,adminUsername,linuxConfiguration.disablePasswordAuthentication}`, `networkProfile.networkInterfaces[].id`, `diagnosticsProfile.bootDiagnostics.enabled`, `securityProfile.*`, `identity.*` + - **Key Vaults** — `tenantId`, `sku.*`, `enableRbacAuthorization`, `enableSoftDelete`, `softDeleteRetentionInDays`, `enablePurgeProtection`, `publicNetworkAccess`, `networkAcls.{defaultAction,bypass,ipRules,virtualNetworkRules}`, `accessPolicies[]` + - **Storage Accounts** — `sku.name`, `kind`, `accessTier`, `allowBlobPublicAccess`, `allowSharedKeyAccess`, `minimumTlsVersion`, `supportsHttpsTrafficOnly`, `networkAcls.*`, `encryption.*`, `publicNetworkAccess`, `allowCrossTenantReplication` + - **App Services / Function Apps** — `httpsOnly`, `siteConfig.{minTlsVersion,ftpsState,http20Enabled,alwaysOn,linuxFxVersion,appSettings[],connectionStrings[],ipSecurityRestrictions[]}`, `identity.*`, `keyVaultReferenceIdentity`, `publicNetworkAccess` + - **Container Apps / Container App Environments** — `configuration.{ingress.*,secrets[],registries[],activeRevisionsMode}`, `template.{containers[].{image,resources,env[]},scale.*}`, `managedEnvironmentId`, `workloadProfileName` + - **NSGs** — `securityRules[].properties.{access,direction,priority,protocol,sourceAddressPrefix,sourcePortRange,destinationAddressPrefix,destinationPortRange,sourceAddressPrefixes,sourcePortRanges,destinationAddressPrefixes,destinationPortRanges}` + - **VNets / Subnets** — `addressSpace.addressPrefixes`, `subnets[].properties.{addressPrefix,networkSecurityGroup.id,routeTable.id,serviceEndpoints[],privateEndpointNetworkPolicies,privateLinkServiceNetworkPolicies,delegations[]}`, `dhcpOptions.dnsServers`, `enableDdosProtection` + - **Private Endpoints** — `privateLinkServiceConnections[].properties.{privateLinkServiceId,groupIds}`, `subnet.id`, `customDnsConfigs[]`, `privateDnsZoneGroup.*` + - **Tags** (all resource types) — compare every tag key/value + Ignore **only** these Azure-managed noise fields: `etag`, `provisioningState`, `resourceGuid`, top-level `id`, top-level `type`, `systemData.*`, auto-added tags matching `hidden-*` or `createdAt*`, and any `properties.*.id` sub-field that's just a self-reference. + For every mismatch found, add a row to the per-deployment drift detail table. Use the JSON pointer as the `Property` column (e.g., `properties.dnsSettings.dnsServers`). Severity follows the classification below. + - **DNS record-set diff** — for each `Microsoft.Network/privateDnsZones` in the template, compare record sets in `.drift-snapshots/.dns.json` (map keyed by zone name) against the IaC-declared record sets. Flag any missing, extra, or value-changed records (A, AAAA, CNAME, TXT, SOA, etc.). + An `ok` status with `resourceCount: 0` means the RG exists but is empty, which is itself a Critical finding for any non-empty IaC template. +4. **Classify** each detected difference into one of three severity levels: + +### Severity Classification + +- **Critical** (security-relevant drift): + - HTTPS enforcement disabled (`httpsOnly` changed to false) + - Firewall rules removed or weakened + - Authentication/authorization settings changed + - Managed identity removed + - TLS version downgraded + - Shared key access re-enabled on storage + - FTP state changed from Disabled + - Public network access enabled unexpectedly + +- **Warning** (operational drift): + - SKU or tier changes + - Tag modifications + - App settings changed (non-security) + - Scale settings modified + - Runtime version changes + +- **Info** (cosmetic drift): + - Description or metadata changes + - Resource tags added by Azure Policy (e.g., `createdDate`) + - Display name changes + +## Anti-Flapping Logic + +Before reporting drift in the status issue, check cache memory for recent drift history: + +1. **Persistence threshold** — Applies ONLY to **existence** drift (resource missing / extra), which can flap during Azure transient API issues. On first detection, record in cache memory and place the finding in the **Suppressed findings** section of the status issue with a `(first-detect)` annotation. On second consecutive detection, surface it in the main **Drift Detail** table. +2. **Property-level and DNS-record drift are NOT subject to persistence threshold.** These are deterministic reads from the Azure control plane. Report them in the main drift table on the very first detection. Do NOT place them in "Suppressed findings" — they must appear in the per-deployment **Drift Detail** subsection with full Expected / Actual values. + +The "Suppressed findings" table in the status issue is reserved exclusively for **existence** drift waiting on its second detection. Every property mutation the snapshot diff finds MUST appear in the main drift detail, never suppressed. + +## Output: Single Status Issue + +This workflow produces **exactly one** GitHub issue per run — a rolling drift status dashboard. It does NOT open pull requests, remediation branches, or per-drift issues. All drift findings, remediation guidance, and suggested next steps live inside the single status issue described in the next section. + +For each deployment with Warning or Critical drift, include a **Drift Detail** subsection in the status issue with: +- An Expected-vs-Actual table of drifted properties/resources +- **Remediation guidance** written out in prose — for each drift item, describe both options the human reviewer has: + - **Revert to IaC:** the `az` command(s) or redeploy steps needed to restore Azure to the IaC-defined state + - **Adopt Azure state:** the exact file path and JSON patch fragment the reviewer would apply to `template.json` / `parameters.json` to codify the current Azure configuration +- Do NOT create branches, commits, or PRs. Do NOT call `create_pull_request`. + +For **Critical** drift, mention `security` and `priority:critical` labels in the status issue body so triagers can apply them (the workflow itself only applies the `drift-status` label). + +For **Info** drift, log to cache memory only — no separate issue; still surface in the main drift table if relevant. + +## Drift Report Format + +Include this block inside each **Drift Detail** subsection of the status issue: + +```markdown +## Drift Detection Report + +**Deployment:** +**Resource Group:** +**Checked:** +**Resources Analyzed:** + +### Summary +- šŸ”“ Critical: properties +- 🟔 Warning: properties +- šŸ”µ Info: properties + +### Details + +| Resource | Property | Expected (IaC) | Actual (Azure) | Severity | +|----------|----------|-----------------|----------------|----------| +| ... | ... | ... | ... | šŸ”“/🟔/šŸ”µ | +``` + +## Drift History + +After completing analysis, update cache memory with: +- Timestamp of this run +- List of all detected drift (deployment, resource, property, severity) +- Actions taken (reported, suppressed by persistence threshold) + +This enables the anti-flapping logic on subsequent runs and provides an audit trail. + +## Process Summary + +1. Discover active deployments from `.azure/deployments/` +2. For each deployment, query Azure and compare against stored ARM templates +3. Classify drift by severity (Critical / Warning / Info) +4. Apply anti-flapping persistence threshold to existence drift +5. Emit exactly one rolling status issue summarizing every deployment and every drift with inline remediation guidance +6. Update cache memory with drift history + +## Required Completion Behavior + +You MUST **always** call `create-issue` exactly once before finishing. This is a +rolling status dashboard: `close-older-issues: true` ensures only the latest +status issue is open at any time. Never finish the run silently and never call +`noop` — always produce a status issue. + +**Do NOT call `create_pull_request`.** This workflow does not create PRs, +branches, commits, or code-push safe outputs. Only `create_issue` is allowed. +If you think a code change is needed, describe it in the status issue body +instead — the human reviewer will open any PR manually. + +> ### CRITICAL: Call `create_issue` yourself +> +> The safe-output tools (`create_issue`, `noop`, `missing_tool`, +> `missing_data`) are **only available in this top-level agent context**. +> They do NOT exist inside spawned subagents or skills. +> +> - Do **NOT** delegate issue creation to a `General-purpose` subagent, +> a `skill(...)` invocation, or any other nested agent. +> - Do the analysis yourself, then directly invoke `create_issue` as a +> top-level tool call in your final turn. +> - If a tool call returns `Tool '...' does not exist`, you are almost +> certainly inside a subagent — pop back to the top-level agent and retry. + +### Status issue — required format + +Title: `[drift-status] Drift report — — deployment(s), drifted` + +Labels: `drift-status` (already configured). If **any** Critical drift is found, +mention `security` and `priority:critical` in the body so triagers can add them. + +The issue must include **one row per directory under `.azure/deployments/`** +(every tracked deployment — active, destroyed, failed, destroy-requested — not +just active ones). Resolve each row's values from that deployment's +`metadata.json`, `state.json`, and `.drift-snapshots/.json`. + +Link format — use repo-relative blob links on the `main` branch and Azure +portal deep links built from `state.json`: + +- Architecture: `[diagram](../blob/main/.azure/deployments//architecture.md)` +- State: `[state](../blob/main/.azure/deployments//state.json)` +- Template: `[template](../blob/main/.azure/deployments//template.json)` +- Azure Portal (resource group): + `https://portal.azure.com/#@/resource/subscriptions//resourceGroups//overview` + where `` is `state.json .subscription` and `` is + `state.json .resourceGroup`. Only include the portal link for deployments + whose lifecycle is `active` or `destroy-requested` — omit for destroyed / + failed rows (render `—` in that column). + +Use the literal `main` branch in repo links; the deployment files live there +after every successful deploy. + +Body template (fill in every section, even when empty): + +```markdown +## Drift Status Dashboard + +**Run:** +**Checked at:** +**Tracked deployments:** +**Active:** Ā· **Destroyed:** Ā· **Failed/Other:** +**Deployments with drift:** + +### Summary + +| Severity | Count | +|----------|-------| +| šŸ”“ Critical | | +| 🟔 Warning | | +| šŸ”µ Info | | + +### Deployments + +One row for every directory in `.azure/deployments/`. + +| Deployment | Resource Group | Lifecycle | Drift | Severity | Architecture | State | Azure Portal | +|------------|----------------|-----------|-------|----------|--------------|-------|--------------| +| `` | `` | āœ… active / šŸ’€ destroyed / āŒ failed / ā³ destroy-requested | āœ… none / āš ļø N drift(s) / ā­ļø skipped | šŸ”“/🟔/šŸ”µ/— | [diagram](../blob/main/.azure/deployments//architecture.md) | [state](../blob/main/.azure/deployments//state.json) | [open](https://portal.azure.com/#@/resource/subscriptions//resourceGroups//overview) or `—` | + +Lifecycle emoji mapping: +- āœ… active — `metadata.status` is `succeeded` and not destroy-requested +- ā³ destroy-requested — `metadata.status` is `destroy-requested` +- šŸ’€ destroyed — `metadata.status` is `destroyed` +- āŒ failed — `metadata.status` is `failed` or state is missing +- Drift column is `ā­ļø skipped` for anything that isn't active + +### Drift Detail + +For every drifted deployment, include one subsection: + +#### `` — [architecture](../blob/main/.azure/deployments//architecture.md) Ā· [state](../blob/main/.azure/deployments//state.json) Ā· [Azure Portal](https://portal.azure.com/#@/resource/subscriptions//resourceGroups//overview) + +
Architecture diagram + +Embed the first ` ```mermaid ... ``` ` fenced block verbatim from +`.azure/deployments//architecture.md`. GitHub renders mermaid inline. +If no mermaid block exists, write `_No diagram available._`. + +
+ +| Resource | Property | Expected (IaC) | Actual (Azure) | Severity | +|----------|----------|----------------|----------------|----------| +| ... | ... | ... | ... | šŸ”“/🟔/šŸ”µ | + +**Remediation options** (no PRs are created by this workflow — apply manually): + +- **Revert to IaC:** describe the `az` command(s) or redeploy steps needed to restore Azure to the IaC-defined state for each drifted item. +- **Adopt Azure state:** describe the exact file path and JSON patch fragment to apply to `template.json` / `parameters.json` to codify the current Azure configuration. + +### No-drift deployments + +Bulleted list of active deployments with zero drift (or `_none_`). + +### Suppressed findings + +Drift suppressed by anti-flapping (debounce / cooldown / persistence-threshold). +Empty section if none. + +### Snapshot failures + +Deployments where the pre-step could not compare live Azure state to IaC. +Derive each row from `.drift-snapshots/.status.json`: + +| Deployment | Resource Group | Snapshot Status | Cause / Action | +|------------|----------------|-----------------|----------------| +| `` | `` | `rg-not-found` / `auth-failed` / `az-error` / `no-resource-group` / `missing` | Human-readable explanation. For `rg-not-found`: "RG was deleted outside Git-Ape — run the destroy workflow to reconcile state." For `auth-failed`: "Pre-step federated identity could not authenticate to the subscription." For `no-resource-group`: "state.json and metadata.json both have empty `resourceGroup` — deploy workflow bug, fix forward." | + +Also include here, as a **Critical** snapshot finding, any deployment whose +`status.json` is `ok` but `resourceCount: 0` while its `template.json` defines +one or more resources — that means every managed resource vanished from Azure. + +Empty section if no snapshot failures. + +### Edge cases + +- If `.azure/deployments/` has no directories at all, state + `_No deployments tracked in this repository._` and set all counts to 0. +- If every tracked deployment is destroyed/failed (no active ones), still post + the full deployments table so users can see lifecycle at a glance. +``` + +The status issue is the only output of this workflow. No pull requests, no +remediation branches, no commits — inline remediation guidance in the status +issue body replaces the previous two-PR-per-deployment flow. diff --git a/.github/workflows/git-ape-plan.exampleyml b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml similarity index 72% rename from .github/workflows/git-ape-plan.exampleyml rename to .github/skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml index a7d6c35..e7d7e49 100644 --- a/.github/workflows/git-ape-plan.exampleyml +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml @@ -199,14 +199,38 @@ jobs: echo "has_architecture=false" >> "$GITHUB_OUTPUT" fi + - name: Stage template for security scan + id: scan_stage + run: | + # WORKAROUND: Microsoft Defender for DevOps' templateanalyzer tool always + # runs `analyze-directory $GITHUB_WORKSPACE` and ignores GDN_TEMPLATEANALYZER_INPUT. + # Template Analyzer's file walker uses .NET EnumerationOptions which default to + # AttributesToSkip=Hidden|System. On Linux, .NET treats any path starting with + # "." as Hidden — so .azure/deployments//template.json is silently skipped + # and the scanner reports "Analyzed 0 files in the directory specified." + # See: https://github.com/Azure/template-analyzer/blob/main/src/Analyzer.Utilities/TemplateDiscovery.cs + # + # Workaround: copy the template + parameters to a non-dotted directory at the + # workspace root so the walker discovers them. + STAGE_DIR="templateanalyzer-scan/${{ matrix.deployment_id }}" + mkdir -p "$STAGE_DIR" + cp "${{ steps.params.outputs.deploy_dir }}/template.json" "$STAGE_DIR/template.json" + if [[ -f "${{ steps.params.outputs.deploy_dir }}/parameters.json" ]]; then + cp "${{ steps.params.outputs.deploy_dir }}/parameters.json" "$STAGE_DIR/template.parameters.json" + fi + echo "stage_dir=$STAGE_DIR" >> "$GITHUB_OUTPUT" + ls -la "$STAGE_DIR" + - name: Run Microsoft Defender for DevOps template analyzer id: security_scan continue-on-error: true uses: microsoft/security-devops-action@v1 with: tools: templateanalyzer - env: - GDN_TEMPLATEANALYZER_INPUT: ${{ steps.params.outputs.deploy_dir }}/template.json + + - name: Cleanup staged template + if: always() + run: rm -rf templateanalyzer-scan - name: Upload SARIF results (non-blocking) id: sarif_upload @@ -351,18 +375,23 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Validate template + - name: Validate template (stack) id: validate if: steps.azure_login.outcome == 'success' run: | - echo "### Validating ARM template..." + echo "### Validating deployment stack..." - RESULT=$(az deployment sub validate \ + # az stack sub validate mirrors az deployment sub validate but also + # verifies stack-specific settings (action-on-unmanage, deny settings). + RESULT=$(az stack sub validate \ + --name "${{ matrix.deployment_id }}" \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ --output json 2>&1) || true # Guard against non-JSON output (e.g. auth/CLI errors) — jq exits non-zero @@ -386,14 +415,31 @@ jobs: - name: Run what-if analysis id: whatif - if: steps.validate.outputs.validation_status == 'passed' + if: steps.azure_login.outcome == 'success' run: | + # NOTE: Deployment Stacks don't yet support what-if + # (see https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-stacks#known-issues). + # We fall back to `az deployment sub what-if` against the underlying + # ARM template — this accurately previews resource changes even though + # it doesn't model the stack wrapper itself. + # + # Run unconditionally on login success: validation and what-if catch + # different classes of issues (schema vs. preflight/runtime), so even + # if validation failed, what-if may surface additional context. + set +e WHATIF_OUTPUT=$(az deployment sub what-if \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ - --no-prompt 2>&1) || true + --no-prompt 2>&1) + WHATIF_EXIT=$? + set -e + if [[ $WHATIF_EXIT -eq 0 ]]; then + echo "whatif_status=passed" >> "$GITHUB_OUTPUT" + else + echo "whatif_status=failed" >> "$GITHUB_OUTPUT" + fi echo "whatif_result<> "$GITHUB_OUTPUT" echo "$WHATIF_OUTPUT" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" @@ -405,6 +451,7 @@ jobs: AZURE_LOGIN_OUTCOME: ${{ steps.azure_login.outcome }} VALIDATION_STATUS: ${{ steps.validate.outputs.validation_status }} VALIDATION_ERROR: ${{ steps.validate.outputs.validation_error }} + WHATIF_STATUS: ${{ steps.whatif.outputs.whatif_status }} WHATIF_RESULT: ${{ steps.whatif.outputs.whatif_result }} run: | mkdir -p .git-ape-plan @@ -417,17 +464,28 @@ jobs: fi fi + FINAL_WHATIF_STATUS="$WHATIF_STATUS" + if [[ -z "$FINAL_WHATIF_STATUS" ]]; then + if [[ "$AZURE_LOGIN_OUTCOME" == "failure" ]]; then + FINAL_WHATIF_STATUS="login_failed" + else + FINAL_WHATIF_STATUS="skipped" + fi + fi + jq -n \ --arg deploymentId "$DEPLOYMENT_ID" \ --arg azureLoginOutcome "$AZURE_LOGIN_OUTCOME" \ --arg validationStatus "$FINAL_VALIDATION_STATUS" \ --arg validationError "$VALIDATION_ERROR" \ + --arg whatifStatus "$FINAL_WHATIF_STATUS" \ --arg whatifResult "$WHATIF_RESULT" \ '{ deploymentId: $deploymentId, azureLoginOutcome: $azureLoginOutcome, validationStatus: $validationStatus, validationError: $validationError, + whatifStatus: $whatifStatus, whatifResult: $whatifResult }' > ".git-ape-plan/plan-azure-${DEPLOYMENT_ID}.json" @@ -440,6 +498,17 @@ jobs: if-no-files-found: error retention-days: 1 + - name: What-if gate + # Runs AFTER the artifact upload so the Plan Comment job still posts the + # full plan (validation + scan + what-if details) to the PR. This step + # only fails the Plan Azure job to block merge/deploy when what-if + # cannot produce a valid deployment preview — a failed what-if means the + # template wouldn't deploy successfully even if everything else is green. + if: steps.whatif.outputs.whatif_status == 'failed' + run: | + echo "::error::What-if analysis failed — deployment would not succeed. See the PR comment for the full output." + exit 1 + plan-comment: name: "Plan Comment: ${{ matrix.deployment_id }}" needs: [detect-deployments, plan-local, plan-azure] @@ -466,7 +535,7 @@ jobs: path: .git-ape-plan/azure - name: Post plan as PR comment - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); @@ -480,11 +549,55 @@ jobs: return JSON.parse(fs.readFileSync(path, 'utf8')); } + // Fetch templateanalyzer Code Scanning alerts for THIS PR so we can render + // each finding as a clickable link to its alert page (Security tab) and to + // the rule documentation. Falls back gracefully if the API call fails or + // returns no alerts (e.g. SARIF upload was skipped or still processing). + async function fetchTemplateAnalyzerAlerts() { + try { + const ref = `refs/pull/${context.issue.number}/merge`; + const { data: alerts } = await github.rest.codeScanning.listAlertsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + ref, + tool_name: 'templateanalyzer', + per_page: 100, + }); + return alerts; + } catch (err) { + core.warning(`Could not fetch templateanalyzer alerts: ${err.message}`); + return []; + } + } + + function renderAlertsTable(alerts) { + if (!alerts || alerts.length === 0) return ''; + const sevIcon = (s) => ({ error: 'šŸ”“', warning: '🟔', note: 'šŸ”µ', none: '⚪' }[s] || '⚪'); + let table = '| Sev | Rule | Line | Description |\n'; + table += '|---|---|---|---|\n'; + for (const a of alerts) { + const sev = a.rule?.severity || 'none'; + const ruleId = a.rule?.id || '?'; + const ruleName = a.rule?.name || ''; + const helpUri = a.rule?.help_uri || ''; + const ruleLabel = helpUri + ? `[\`${ruleId}\`](${helpUri}) ${ruleName}` + : `\`${ruleId}\` ${ruleName}`; + const line = a.most_recent_instance?.location?.start_line || '?'; + const desc = (a.rule?.description || '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); + const alertLink = `[#${a.number}](${a.html_url})`; + table += `| ${sevIcon(sev)} | ${alertLink} Ā· ${ruleLabel} | ${line} | ${desc} |\n`; + } + return table; + } + + const alerts = await fetchTemplateAnalyzerAlerts(); const local = loadSummary('local') || {}; const azure = loadSummary('azure') || {}; const validationStatus = azure.validationStatus || 'skipped'; const validationError = azure.validationError || ''; + const whatifStatus = azure.whatifStatus || 'skipped'; const whatifResult = azure.whatifResult || ''; const azureLoginOutcome = azure.azureLoginOutcome || ''; const scanStatus = local.scanStatus || 'skipped'; @@ -524,27 +637,40 @@ jobs: comment += `${tagDetails}\n\n`; } - if (validationStatus === 'passed') { - if (securityScanOutcome === 'failure' && scanStatus === 'skipped') { - comment += `### āš ļø Security Scan: Tool Execution Failed\n\n`; - } else if (scanStatus === 'passed') { - comment += `### āœ… Security Scan: Passed`; - if (parseInt(scanWarnings) > 0 || parseInt(scanNotes) > 0) { - comment += ` (${scanWarnings} warning(s), ${scanNotes} note(s))`; - } - comment += `\n\n`; - } else if (scanStatus === 'failed') { - comment += `### āŒ Security Scan: Failed (${scanErrors} error(s), ${scanWarnings} warning(s))\n\n`; - } else { - comment += `### āš ļø Security Scan: Skipped\n\n`; + // Security scan runs locally on the template file and is independent of + // Azure validation. Always render the section so reviewers see the result + // even when validation fails. + if (securityScanOutcome === 'failure' && scanStatus === 'skipped') { + comment += `### āš ļø Security Scan: Tool Execution Failed\n\n`; + } else if (scanStatus === 'passed') { + comment += `### āœ… Security Scan: Passed`; + if (parseInt(scanWarnings) > 0 || parseInt(scanNotes) > 0) { + comment += ` (${scanWarnings} warning(s), ${scanNotes} note(s))`; } + comment += `\n\n`; + } else if (scanStatus === 'failed') { + comment += `### āŒ Security Scan: Failed (${scanErrors} error(s), ${scanWarnings} warning(s))\n\n`; + } else { + comment += `### āš ļø Security Scan: Skipped\n\n`; + } - if (scanFindings) { - comment += `
\nSecurity findings\n\n${scanFindings}\n\n
\n\n`; - } - if (sarifUploadOutcome === 'failure') { - comment += `> SARIF upload to GitHub code scanning failed, but this does not block plan generation.\n\n`; - } + // Prefer the live Code Scanning alerts table (rich links into the Security + // tab + rule docs). Fall back to the inline SARIF text findings when the + // alerts API hasn't surfaced them yet (e.g. SARIF still processing). + const alertsTable = renderAlertsTable(alerts); + const codeScanningFilterUrl = + `https://github.com/${context.repo.owner}/${context.repo.repo}` + + `/security/code-scanning?query=pr%3A${context.issue.number}+tool%3Atemplateanalyzer`; + + if (alertsTable) { + comment += `
\n${alerts.length} finding(s) — Microsoft Defender for DevOps Ā· Template Analyzer\n\n${alertsTable}\n\n
\n\n`; + comment += `> šŸ”— **[View all ${alerts.length} alerts in the Security tab →](${codeScanningFilterUrl})**\n\n`; + } else if (scanFindings) { + comment += `
\nSecurity findings (Microsoft Defender for DevOps Ā· Template Analyzer)\n\n${scanFindings}\n\n
\n\n`; + comment += `> šŸ”— **[View in Security tab →](${codeScanningFilterUrl})** (alerts may take a moment to appear after upload)\n\n`; + } + if (sarifUploadOutcome === 'failure') { + comment += `> SARIF upload to GitHub code scanning failed, but this does not block plan generation.\n\n`; } if (costTotal && validationStatus === 'passed') { @@ -563,6 +689,18 @@ jobs: if (validationStatus === 'passed' && whatifResult) { comment += `### What-If Analysis\n\n`; comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'passed' && whatifResult) { + comment += `### What-If Analysis\n\n`; + comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'failed') { + comment += `### āŒ What-If Analysis: Failed\n\n`; + comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'login_failed') { + comment += `### āš ļø What-If Analysis: Skipped\n\n`; + comment += `> Skipped because Azure OIDC login failed.\n\n`; + } else { + comment += `### āš ļø What-If Analysis: Skipped\n\n`; + comment += `> What-if did not run. See validation/login status above.\n\n`; } if (validationStatus === 'passed') { diff --git a/.github/workflows/git-ape-verify.exampleyml b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml similarity index 95% rename from .github/workflows/git-ape-verify.exampleyml rename to .github/skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml index caa5202..e517ca3 100644 --- a/.github/workflows/git-ape-verify.exampleyml +++ b/.github/skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml @@ -36,7 +36,7 @@ jobs: echo "āœ… AZURE_TENANT_ID is set" fi - if [[ -z "${{ secrets.AZURE_SUBSCRIPTION_ID }}" ]]; then + if [[ -z "${{ vars.AZURE_SUBSCRIPTION_ID }}" ]]; then echo "::error::Missing secret: AZURE_SUBSCRIPTION_ID" MISSING=$((MISSING + 1)) else @@ -52,7 +52,7 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - name: Verify Azure access if: steps.secrets.outputs.missing == '0' @@ -117,6 +117,8 @@ jobs: "git-ape-plan.yml:Git-Ape: Plan" "git-ape-deploy.yml:Git-Ape: Deploy" "git-ape-destroy.yml:Git-Ape: Destroy" + "git-ape-drift.yml:Git-Ape: Drift Detection" + "git-ape-ttl-reaper.yml:Git-Ape: TTL Reaper" ) for WF in "${WORKFLOWS[@]}"; do diff --git a/.github/workflows/git-ape-deploy.exampleyml b/.github/workflows/git-ape-deploy.exampleyml deleted file mode 100644 index 48c6d71..0000000 --- a/.github/workflows/git-ape-deploy.exampleyml +++ /dev/null @@ -1,494 +0,0 @@ -# Git-Ape Deploy Workflow -# Triggers on: -# 1. PR merge to main (when deployment files are included) -# 2. `/deploy` comment on an approved PR (deploys from branch before merge) -# Runs the actual ARM deployment, captures outputs, and runs integration tests. - -name: "Git-Ape: Deploy" - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -on: - # Trigger 1: PR merged to main with deployment artifacts - push: - branches: [main] - paths: - - ".azure/deployments/**/template.json" - - ".azure/deployments/**/parameters.json" - - # Trigger 2: `/deploy` comment on a PR - issue_comment: - types: [created] - -permissions: - id-token: write # OIDC token for Azure login - contents: write # Commit state files back to repo - pull-requests: write # Post deployment results as PR comment - issues: write # Post on issue comments - security-events: write # Upload SARIF results from template analyzer - actions: read # Required by codeql-action/upload-sarif to read workflow run context - -concurrency: - group: git-ape-deploy-${{ github.event_name == 'push' && github.sha || github.event.comment.id }} - cancel-in-progress: false # Never cancel in-progress deployments - -jobs: - # Gate: Only run on `/deploy` comments on approved PRs - check-comment-trigger: - name: Check /deploy trigger - if: github.event_name == 'issue_comment' - runs-on: ubuntu-latest - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} - pr_ref: ${{ steps.check.outputs.pr_ref }} - steps: - - name: Check comment and PR status - id: check - uses: actions/github-script@v8 - with: - script: | - const comment = context.payload.comment.body.trim(); - if (!comment.startsWith('/deploy')) { - core.setOutput('should_deploy', 'false'); - return; - } - - // Must be on a PR (not a regular issue) - if (!context.payload.issue.pull_request) { - core.setOutput('should_deploy', 'false'); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: 'āŒ `/deploy` can only be used on pull requests.', - }); - return; - } - - // Get PR details - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - - // Check PR is approved - const { data: reviews } = await github.rest.pulls.listReviews({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - const approved = reviews.some(r => r.state === 'APPROVED'); - - if (!approved) { - core.setOutput('should_deploy', 'false'); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: 'āŒ PR must be **approved** before deploying. Get a review approval first.', - }); - return; - } - - core.setOutput('should_deploy', 'true'); - core.setOutput('pr_ref', pr.head.ref); - - // React to the comment - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket', - }); - - detect-deployments: - name: Detect deployments to execute - needs: [check-comment-trigger] - if: | - always() && - (github.event_name == 'push' || - (github.event_name == 'issue_comment' && needs.check-comment-trigger.outputs.should_deploy == 'true')) - runs-on: ubuntu-latest - outputs: - deployment_ids: ${{ steps.find.outputs.deployment_ids }} - has_deployments: ${{ steps.find.outputs.has_deployments }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ needs.check-comment-trigger.outputs.pr_ref || github.ref }} - fetch-depth: 0 - - - name: Find deployment directories - id: find - run: | - if [[ "${{ github.event_name }}" == "push" ]]; then - # On merge: find deployments changed in the merge commit - CHANGED_FILES=$(git diff --name-only HEAD~1...HEAD -- '.azure/deployments/*/template.json' 2>/dev/null || true) - else - # On /deploy comment: find all deployments with template.json on the branch - CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '.azure/deployments/*/template.json' 2>/dev/null || true) - fi - - if [[ -z "$CHANGED_FILES" ]]; then - echo "has_deployments=false" >> "$GITHUB_OUTPUT" - echo "deployment_ids=[]" >> "$GITHUB_OUTPUT" - echo "No deployments found" - exit 0 - fi - - DEPLOYMENT_IDS=$(echo "$CHANGED_FILES" | sed 's|.azure/deployments/\([^/]*\)/.*|\1|' | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))') - - echo "has_deployments=true" >> "$GITHUB_OUTPUT" - echo "deployment_ids=$DEPLOYMENT_IDS" >> "$GITHUB_OUTPUT" - echo "Deployments to execute: $DEPLOYMENT_IDS" - - deploy: - name: "Deploy: ${{ matrix.deployment_id }}" - needs: [detect-deployments, check-comment-trigger] - if: | - always() && - needs.detect-deployments.outputs.has_deployments == 'true' - runs-on: ubuntu-latest - environment: azure-deploy - strategy: - matrix: - deployment_id: ${{ fromJson(needs.detect-deployments.outputs.deployment_ids) }} - max-parallel: 1 # Deploy sequentially to avoid conflicts - fail-fast: false - - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ needs.check-comment-trigger.outputs.pr_ref || github.ref }} - - - name: Read deployment parameters - id: params - run: | - DEPLOY_DIR=".azure/deployments/${{ matrix.deployment_id }}" - - if [[ ! -f "$DEPLOY_DIR/template.json" ]]; then - echo "::error::Template not found: $DEPLOY_DIR/template.json" - exit 1 - fi - - if [[ -f "$DEPLOY_DIR/parameters.json" ]]; then - LOCATION=$(jq -r '.parameters.location.value // "eastus"' "$DEPLOY_DIR/parameters.json") - PROJECT=$(jq -r '.parameters.project.value // .parameters.projectName.value // "unknown"' "$DEPLOY_DIR/parameters.json") - ENVIRONMENT=$(jq -r '.parameters.environment.value // "dev"' "$DEPLOY_DIR/parameters.json") - else - LOCATION="eastus" - PROJECT="unknown" - ENVIRONMENT="dev" - fi - - echo "location=$LOCATION" >> "$GITHUB_OUTPUT" - echo "project=$PROJECT" >> "$GITHUB_OUTPUT" - echo "environment=$ENVIRONMENT" >> "$GITHUB_OUTPUT" - echo "deploy_dir=$DEPLOY_DIR" >> "$GITHUB_OUTPUT" - - - name: Azure Login (OIDC) - uses: azure/login@v3 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Validate before deploy - run: | - az deployment sub validate \ - --location "${{ steps.params.outputs.location }}" \ - --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ - --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ - --output json - - - name: Run Microsoft Defender for DevOps template analyzer - id: security_scan - continue-on-error: true - uses: microsoft/security-devops-action@v1 - with: - tools: templateanalyzer - env: - GDN_TEMPLATEANALYZER_INPUT: ${{ steps.params.outputs.deploy_dir }}/template.json - - - name: Upload SARIF results - if: always() && steps.security_scan.outputs.sarifFile != '' - continue-on-error: true - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: ${{ steps.security_scan.outputs.sarifFile }} - category: templateanalyzer - - - name: Check security scan results - id: scan_gate - run: | - SARIF_FILE="${{ steps.security_scan.outputs.sarifFile }}" - if [[ -f "$SARIF_FILE" ]]; then - ERRORS=$(jq '[.runs[].results[] | select(.level == "error")] | length' "$SARIF_FILE" 2>/dev/null || echo 0) - if [[ "$ERRORS" -gt 0 ]]; then - echo "::error::Template analyzer found $ERRORS security error(s). Deployment blocked." - jq -r '.runs[].results[] | select(.level == "error") | " ERROR: \(.message.text) (\(.ruleId))"' "$SARIF_FILE" - exit 1 - fi - echo "Security scan passed — no errors found" - fi - - - name: Deploy to Azure - id: deploy - run: | - echo "šŸš€ Starting deployment: ${{ matrix.deployment_id }}" - START_TIME=$(date +%s) - - DEPLOY_OUTPUT=$(az deployment sub create \ - --name "${{ matrix.deployment_id }}" \ - --location "${{ steps.params.outputs.location }}" \ - --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ - --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ - --output json 2>&1) - - EXIT_CODE=$? - END_TIME=$(date +%s) - DURATION=$((END_TIME - START_TIME)) - - echo "deploy_duration=${DURATION}s" >> "$GITHUB_OUTPUT" - - if [[ $EXIT_CODE -ne 0 ]]; then - echo "deploy_status=failed" >> "$GITHUB_OUTPUT" - echo "deploy_error<> "$GITHUB_OUTPUT" - echo "$DEPLOY_OUTPUT" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - echo "" - echo "==========================================" - echo "āŒ DEPLOYMENT FAILED" - echo "==========================================" - echo "$DEPLOY_OUTPUT" - echo "==========================================" - echo "::error::Deployment failed — see output above for details" - exit 1 - fi - - echo "deploy_status=succeeded" >> "$GITHUB_OUTPUT" - - # Extract outputs - OUTPUTS=$(echo "$DEPLOY_OUTPUT" | jq -r '.properties.outputs // {}') - echo "deploy_outputs<> "$GITHUB_OUTPUT" - echo "$OUTPUTS" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - # Extract resource group name - RG_NAME=$(echo "$OUTPUTS" | jq -r '.resourceGroupName.value // empty') - echo "resource_group=$RG_NAME" >> "$GITHUB_OUTPUT" - - echo "āœ… Deployment succeeded in ${DURATION}s" - - - name: Run integration tests - id: tests - if: steps.deploy.outputs.deploy_status == 'succeeded' - run: | - RG_NAME="${{ steps.deploy.outputs.resource_group }}" - - if [[ -z "$RG_NAME" ]]; then - echo "āš ļø No resource group name in outputs, skipping integration tests" - echo "test_status=skipped" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "Running integration tests for RG: $RG_NAME" - - # List deployed resources - RESOURCES=$(az resource list --resource-group "$RG_NAME" \ - --query "[].{name:name, type:type, provisioningState:provisioningState}" \ - --output json 2>/dev/null || echo "[]") - - echo "resources<> "$GITHUB_OUTPUT" - echo "$RESOURCES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - # Check all resources provisioned successfully - FAILED=$(echo "$RESOURCES" | jq '[.[] | select(.provisioningState != "Succeeded")] | length') - if [[ "$FAILED" -gt 0 ]]; then - echo "test_status=failed" >> "$GITHUB_OUTPUT" - echo "::warning::$FAILED resource(s) not in Succeeded state" - else - echo "test_status=passed" >> "$GITHUB_OUTPUT" - fi - - # Test HTTP endpoints (Container Apps, Function Apps, Web Apps) - ENDPOINTS=$(echo "$RESOURCES" | jq -r '.[] | select(.type == "Microsoft.App/containerApps" or .type == "Microsoft.Web/sites") | .name') - TEST_RESULTS="" - - for NAME in $ENDPOINTS; do - RESOURCE_TYPE=$(echo "$RESOURCES" | jq -r ".[] | select(.name == \"$NAME\") | .type") - - if [[ "$RESOURCE_TYPE" == "Microsoft.App/containerApps" ]]; then - FQDN=$(az containerapp show -n "$NAME" -g "$RG_NAME" --query "properties.configuration.ingress.fqdn" -o tsv 2>/dev/null || echo "") - else - FQDN=$(az webapp show -n "$NAME" -g "$RG_NAME" --query "defaultHostName" -o tsv 2>/dev/null || echo "") - fi - - if [[ -n "$FQDN" ]]; then - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "https://$FQDN" 2>/dev/null || echo "000") - TEST_RESULTS="${TEST_RESULTS}\n- ${NAME}: https://${FQDN} → HTTP ${HTTP_CODE}" - - if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 400 ]]; then - echo "āœ… $NAME: HTTP $HTTP_CODE" - else - echo "āš ļø $NAME: HTTP $HTTP_CODE (may still be starting)" - fi - fi - done - - echo "test_endpoints<> "$GITHUB_OUTPUT" - echo -e "$TEST_RESULTS" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Save deployment state - if: always() - run: | - DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" - STATUS="${{ steps.deploy.outputs.deploy_status || 'failed' }}" - TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) - - # Create/update state.json - cat > "$DEPLOY_DIR/state.json" < "$DEPLOY_DIR/metadata.json.tmp" \ - && mv "$DEPLOY_DIR/metadata.json.tmp" "$DEPLOY_DIR/metadata.json" - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - # Stash the updated state and metadata files before switching branches - cp "$DEPLOY_DIR/state.json" /tmp/state.json 2>/dev/null || true - cp "$DEPLOY_DIR/metadata.json" /tmp/metadata.json 2>/dev/null || true - - # Ensure we push to main regardless of which ref was checked out - git fetch origin main - git checkout main - - # Restore the updated state and metadata files onto main - cp /tmp/state.json "$DEPLOY_DIR/state.json" 2>/dev/null || true - cp /tmp/metadata.json "$DEPLOY_DIR/metadata.json" 2>/dev/null || true - - git add "$DEPLOY_DIR/state.json" "$DEPLOY_DIR/metadata.json" - git diff --cached --quiet || git commit -m "git-ape: update state for ${{ matrix.deployment_id }} [$STATUS]" - git push || echo "::warning::Could not push state update to main" - - - name: Post deployment result - if: always() && github.event_name == 'issue_comment' - uses: actions/github-script@v8 - with: - script: | - const deploymentId = '${{ matrix.deployment_id }}'; - const status = '${{ steps.deploy.outputs.deploy_status }}' || 'failed'; - const duration = '${{ steps.deploy.outputs.deploy_duration }}'; - const outputs = `${{ steps.deploy.outputs.deploy_outputs }}`; - const resources = `${{ steps.tests.outputs.resources }}`; - const testEndpoints = `${{ steps.tests.outputs.test_endpoints }}`; - const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`; - - let comment = `## Git-Ape Deploy: \`${deploymentId}\`\n\n`; - - if (status === 'succeeded') { - comment += `### āœ… Deployment Succeeded\n\n`; - comment += `- **Duration:** ${duration}\n`; - comment += `- **Workflow Run:** [View logs](${runUrl})\n\n`; - - if (testEndpoints) { - comment += `### Endpoints\n\n${testEndpoints}\n\n`; - } - - if (resources) { - try { - const parsed = JSON.parse(resources); - comment += `### Resources (${parsed.length})\n\n`; - comment += `| Name | Type | Status |\n|------|------|--------|\n`; - for (const r of parsed) { - const icon = r.provisioningState === 'Succeeded' ? 'āœ…' : 'āš ļø'; - comment += `| ${r.name} | ${r.type} | ${icon} ${r.provisioningState} |\n`; - } - comment += '\n'; - } catch {} - } - } else { - comment += `### āŒ Deployment Failed\n\n`; - comment += `- **Workflow Run:** [View logs](${runUrl})\n\n`; - const error = `${{ steps.deploy.outputs.deploy_error }}`; - if (error) { - comment += `\`\`\`\n${error.substring(0, 2000)}\n\`\`\`\n\n`; - } - } - - const marker = ``; - comment = marker + '\n' + comment; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment, - }); - - - name: Notify via Slack - if: always() - continue-on-error: true - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - run: | - if [[ -z "$SLACK_WEBHOOK_URL" ]]; then exit 0; fi - - STATUS="${{ steps.deploy.outputs.deploy_status }}" - DEPLOY_ID="${{ matrix.deployment_id }}" - DURATION="${{ steps.deploy.outputs.deploy_duration }}" - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - if [[ "$STATUS" == "succeeded" ]]; then - EMOJI="āœ…" - MSG="Deployment *$DEPLOY_ID* succeeded in $DURATION" - else - EMOJI="āŒ" - MSG="Deployment *$DEPLOY_ID* failed" - fi - - curl -sf -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - -d "{ - \"text\": \"$EMOJI $MSG\", - \"blocks\": [ - { - \"type\": \"section\", - \"text\": { - \"type\": \"mrkdwn\", - \"text\": \"$EMOJI *Git-Ape Deploy: $DEPLOY_ID*\\n\\n$MSG\\n\\nTriggered by: ${{ github.actor }}\\n<$RUN_URL|View logs>\" - } - } - ] - }" || echo "::warning::Slack notification failed" diff --git a/.github/workflows/git-ape-onboarding-template-check.yml b/.github/workflows/git-ape-onboarding-template-check.yml new file mode 100644 index 0000000..0466e79 --- /dev/null +++ b/.github/workflows/git-ape-onboarding-template-check.yml @@ -0,0 +1,77 @@ +name: "Git-Ape: Onboarding Template Check" + +# Fails any PR that edits either the canonical onboarding templates or the +# .github/copilot-instructions.md mirror without keeping the two in sync. +# +# Note: The workflow templates under templates/workflows/ are NOT mirrored +# into this repository's .github/workflows/. They are scaffolded only into a +# USER's repository by scaffold-repo.{sh,ps1} during onboarding, so there is +# nothing to sync-check for them here. The scaffold-parity-smoke job still +# validates that the bash and pwsh scaffolders produce byte-identical output. +# +# Fix sync drift with: +# .github/skills/git-ape-onboarding/scripts/sync-templates.sh apply +# pwsh .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 apply +# and commit the updated mirror alongside the template change. +# +# Three jobs run on every matching PR: +# 1. check-sync-bash — Ubuntu, runs the .sh sync check. +# 2. check-sync-pwsh — Windows, runs the .ps1 sync check. +# 3. scaffold-parity-smoke — Ubuntu, scaffolds via both runtimes into +# separate sandboxes and fails on any byte-level divergence. + +on: + pull_request: + paths: + - '.github/skills/git-ape-onboarding/templates/**' + - '.github/skills/git-ape-onboarding/scripts/sync-templates.sh' + - '.github/skills/git-ape-onboarding/scripts/sync-templates.ps1' + - '.github/skills/git-ape-onboarding/scripts/scaffold-repo.sh' + - '.github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1' + - '.github/copilot-instructions.md' + - '.github/workflows/git-ape-onboarding-template-check.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-sync-bash: + name: Verify onboarding templates ↔ mirrors are in sync (bash) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Run sync check + run: | + chmod +x .github/skills/git-ape-onboarding/scripts/sync-templates.sh + .github/skills/git-ape-onboarding/scripts/sync-templates.sh check + + check-sync-pwsh: + name: Verify onboarding templates ↔ mirrors are in sync (pwsh on Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Run sync check + shell: pwsh + run: | + pwsh -NoProfile -File .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 check + + scaffold-parity-smoke: + name: Scaffold parity smoke (bash sandbox vs pwsh sandbox produces identical files) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Scaffold via bash and pwsh into separate sandboxes, then diff + run: | + set -euo pipefail + SANDBOX_SH="$(mktemp -d)" + SANDBOX_PS="$(mktemp -d)" + chmod +x .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh + bash .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh "$SANDBOX_SH" > /dev/null + pwsh -NoProfile -File .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 "$SANDBOX_PS" > /dev/null + # Recursive diff: fails the job if the two scaffolds differ by a single byte. + diff -r "$SANDBOX_SH" "$SANDBOX_PS" + echo "scaffold-repo.sh and scaffold-repo.ps1 produce byte-identical output." From 4b2a1b867318fc949ca1f0bb4c14dceb7005b3cd Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:23:43 +0800 Subject: [PATCH 3/6] feat(extension): register prompt files in VSIX and tighten .vscodeignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the .github/prompts/ directory into the published artifacts: - plugin.json: declare 'prompts: .github/prompts/' so the plugin manifest exposes them alongside agents and skills. - extension/package.template.json: register all 9 prompt files (git-ape, agent-{bench,improve,onboard,promote}, skill-{bench, improve,onboard,promote}) under chatPromptFiles so VS Code picks them up from the installed extension. - extension/.vscodeignore: explicitly exclude dev-only .github subtrees (actionlint, dependabot, aw, copilot, evals, plugins, references, scripts, templates, workflows). Keeps agents/, skills/, plugin/, copilot-instructions.md, and now prompts/ in the VSIX while shedding ~MB of CI tooling that shouldn't ship to users. 🧩 - Generated by Copilot --- extension/.vscodeignore | 15 +++++++++++++++ extension/package.template.json | 29 +++++++++++++++++++++++++++++ plugin.json | 3 ++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/extension/.vscodeignore b/extension/.vscodeignore index f4f22d9..ae8cf96 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -1,2 +1,17 @@ +# Local build artifacts package.template.json *.vsix + +# Internal-only .github content — keep agents/, skills/, plugin/, copilot-instructions.md +# Everything else under .github/ is dev tooling (CI, evals, prompts, scripts) and +# should not ship in the published VSIX. +.github/actionlint.yaml +.github/dependabot.yml +.github/aw/** +.github/copilot/** +.github/evals/** +.github/plugins/** +.github/references/** +.github/scripts/** +.github/templates/** +.github/workflows/** diff --git a/extension/package.template.json b/extension/package.template.json index cde476b..ac5e005 100644 --- a/extension/package.template.json +++ b/extension/package.template.json @@ -102,6 +102,35 @@ { "path": "./.github/skills/prereq-check/SKILL.md" } + ], + "chatPromptFiles": [ + { + "path": "./.github/prompts/git-ape.prompt.md" + }, + { + "path": "./.github/prompts/agent-bench.prompt.md" + }, + { + "path": "./.github/prompts/agent-improve.prompt.md" + }, + { + "path": "./.github/prompts/agent-onboard.prompt.md" + }, + { + "path": "./.github/prompts/agent-promote.prompt.md" + }, + { + "path": "./.github/prompts/skill-bench.prompt.md" + }, + { + "path": "./.github/prompts/skill-improve.prompt.md" + }, + { + "path": "./.github/prompts/skill-onboard.prompt.md" + }, + { + "path": "./.github/prompts/skill-promote.prompt.md" + } ] } } diff --git a/plugin.json b/plugin.json index 4137c70..589ce10 100644 --- a/plugin.json +++ b/plugin.json @@ -22,5 +22,6 @@ "copilot-agents" ], "agents": ".github/agents/", - "skills": ".github/skills/" + "skills": ".github/skills/", + "prompts": ".github/prompts/" } From 1fb61bd6905b51d963c4673b084ff6a711f5ea99 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:24:08 +0800 Subject: [PATCH 4/6] docs(instructions): switch deploy/destroy guidance to Azure Deployment Stacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align copilot-instructions with the actual workflow templates shipped by the onboarding skill: use 'az stack sub' instead of 'az deployment sub' / 'az group delete' for the full plan-deploy-destroy lifecycle. Why this matters for agents reading the instructions: - The stack is the single unit of lifecycle — create, update, and destroy all operate on it, not on the underlying RGs. - 'deleteAll' on unmanage cleans up every managed resource across every scope (subscription, multiple RGs, sub-scope role/policy assignments) in one call. No orphans, idempotent re-runs. - See Azure/git-ape#30 for the design rationale. Sample workflow snippet now also passes --action-on-unmanage deleteAll, --deny-settings-mode none, --yes — matching what .github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml generates in target repos. šŸ“˜ - Generated by Copilot --- .github/copilot-instructions.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2c29d37..be805ff 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -171,7 +171,7 @@ Git-Ape provides three GitHub Actions workflows under `.github/workflows/`: **What it does:** 1. Detects which deployment directories changed in the PR 2. Logs into Azure via OIDC -3. Validates each ARM template (`az deployment sub validate`) +3. Validates each ARM template (`az stack sub validate`) 4. Runs what-if analysis (`az deployment sub what-if`) 5. Reads the architecture diagram from the deployment directory 6. Posts a detailed plan as a **PR comment** (validation result + what-if + architecture) @@ -192,12 +192,17 @@ Git-Ape provides three GitHub Actions workflows under `.github/workflows/`: **What it does:** 1. Detects deployment directories to execute 2. Logs into Azure via OIDC -3. Validates the template one more time -4. Runs `az deployment sub create` to deploy +3. Validates the template one more time (`az stack sub validate`) +4. Deploys as an **Azure Deployment Stack** (`az stack sub create --action-on-unmanage deleteAll`) 5. Runs integration tests (lists deployed resources, tests HTTP endpoints) -6. Commits `state.json` with deployment result back to the repo +6. Commits `state.json` (including `stackId` and `managedResources[]`) back to the repo 7. Posts deployment result as a PR comment (on `/deploy` trigger) +**Why Deployment Stacks:** +- The stack is the single unit of lifecycle — create, update, and destroy operate on it, not on the underlying RGs. +- `deleteAll` on unmanage guarantees destruction cleans up every managed resource across every scope (subscription, multiple RGs, role/policy assignments at sub scope) in one call. No orphans, idempotent re-runs. +- See [Azure/git-ape#30](https://github.com/Azure/git-ape/issues/30) for the rationale. + **Requires:** GitHub environment `azure-deploy` (for environment protection rules) **Safety:** @@ -213,11 +218,15 @@ Git-Ape provides three GitHub Actions workflows under `.github/workflows/`: **What it does:** 1. Detects deployments where `metadata.json` status changed to `destroy-requested` -2. Reads `state.json` to find the resource group name -3. Inventories all resources in the resource group -4. Deletes the resource group (`az group delete` — synchronous, waits for completion) +2. Reads `state.json` to find the deployment stack name (`deploymentId`) and `stackId` +3. Calls `az stack sub show` to inventory the stack's managed resources +4. Calls `az stack sub delete --action-on-unmanage deleteAll` — removes every resource the stack manages, across all scopes, in one synchronous call 5. Updates `state.json` and `metadata.json` with `destroyed` status and commits to repo +**Idempotency:** +- If the stack is already gone, the workflow records `already-destroyed` and succeeds cleanly. +- No RG-delete fallback path, no subscription-scope resource sweep — Stacks handle multi-scope destruction natively. + **Destroy flow:** 1. Agent or user creates a PR that sets `metadata.json` status to `destroy-requested` 2. PR is reviewed and approved (human gate for destructive action) @@ -413,10 +422,14 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Deploy run: | - az deployment sub create \ + az stack sub create \ + --name ${{ env.DEPLOYMENT_ID }} \ --location ${{ env.LOCATION }} \ --template-file .azure/deployments/${{ env.DEPLOYMENT_ID }}/template.json \ - --parameters @.azure/deployments/${{ env.DEPLOYMENT_ID }}/parameters.json + --parameters @.azure/deployments/${{ env.DEPLOYMENT_ID }}/parameters.json \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --yes ``` **Transitioning from Service Principal secrets to OIDC:** From 7137093da72eec8dfa058523994f61b1de7cd0f7 Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:24:32 +0800 Subject: [PATCH 5/6] docs(website): regenerate for templated workflows and prompt assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/generate-docs.js: teach the workflow doc generator about two source directories, the existing CI workflows under .github/workflows/ and the new user-facing templates under .github/skills/git-ape- onboarding/templates/workflows/. Templated workflows get a Docusaurus :::info admonition explaining they're scaffolded by /git-ape-onboarding and don't run in the git-ape repo itself. Drops .exampleyml handling since those stubs are gone. README.md: update the Workflows table + repo tree to reflect the new layout. The four git-ape-{plan,deploy,destroy,verify}.exampleyml stubs no longer exist in .github/workflows/; their canonical sources are inside the onboarding skill's templates/ directory and scaffolded into user repos as ready-to-run .yml files. Mention skip-on-collision so readers know existing workflows are never overwritten. website/docs/: regenerate every page that the generator touches: - workflows/{git-ape-plan,deploy,destroy,verify}.md: relocated to the template source path + new admonition - workflows/git-ape-drift-lock.md, git-ape-onboarding-template-check.md (new pages) - workflows/overview.md: refreshed listing - agents/git-ape-onboarding.md, skills/git-ape-onboarding.md, getting-started/onboarding.md: re-synced from current sources - reference/{plugin-json,marketplace}.md: re-synced to pick up prompts: registration and chatPromptFiles entries šŸ“š - Generated by Copilot --- README.md | 14 +- scripts/generate-docs.js | 159 +- website/docs/agents/git-ape-onboarding.md | 92 +- website/docs/getting-started/onboarding.md | 4 +- website/docs/reference/marketplace.md | 8 +- website/docs/reference/plugin-json.md | 7 +- website/docs/skills/git-ape-onboarding.md | 162 +- .../docs/workflows/daily-repo-status-lock.md | 258 ++- website/docs/workflows/git-ape-actionlint.md | 13 +- website/docs/workflows/git-ape-deploy.md | 430 ++++- website/docs/workflows/git-ape-destroy.md | 168 +- website/docs/workflows/git-ape-drift-lock.md | 1600 +++++++++++++++++ .../git-ape-onboarding-template-check.md | 138 ++ website/docs/workflows/git-ape-plan.md | 206 ++- website/docs/workflows/git-ape-release.md | 21 +- website/docs/workflows/git-ape-verify.md | 14 +- .../docs/workflows/issue-triage-agent-lock.md | 226 ++- website/docs/workflows/overview.md | 43 +- 18 files changed, 2935 insertions(+), 628 deletions(-) create mode 100644 website/docs/workflows/git-ape-drift-lock.md create mode 100644 website/docs/workflows/git-ape-onboarding-template-check.md diff --git a/README.md b/README.md index 8c2d3c2..81a011b 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ graph LR | `git-ape-destroy.yml` | Merge PR with `destroy-requested` | Delete resource group | | `git-ape-verify.yml` | Manual dispatch | Verify OIDC, RBAC, pipeline health | -> **Note:** These workflows ship as `git-ape-*.exampleyml` files in `.github/workflows/` and are inert until the `/git-ape-onboarding` flow renames them to `.yml` after you complete the experimental-status acknowledgments. +> **Note:** These workflows ship as canonical templates inside the onboarding skill at `.github/skills/git-ape-onboarding/templates/workflows/`. The `/git-ape-onboarding` flow scaffolds them into your repository's `.github/workflows/` directory as ready-to-run `.yml` files (skip-with-notice on collision — never overwrites a customized workflow). ## Included Components @@ -319,7 +319,10 @@ plugin.json # Plugin manifest │ ā”œā”€ā”€ azure-policy-advisor.agent.md │ └── azure-iac-exporter.agent.md ā”œā”€ā”€ skills/ -│ ā”œā”€ā”€ git-ape-onboarding/ # OIDC, RBAC, env setup +│ ā”œā”€ā”€ git-ape-onboarding/ # OIDC, RBAC, env setup + workflow templates +│ │ └── templates/workflows/ # Canonical git-ape-{plan,deploy,destroy,verify}.yml +│ │ # + git-ape-drift.{md,lock.yml} +│ │ # Scaffolded into your repo by /git-ape-onboarding │ ā”œā”€ā”€ azure-rest-api-reference/ # ARM property + API version lookup │ ā”œā”€ā”€ azure-naming-research/ # CAF naming │ ā”œā”€ā”€ azure-resource-availability/ # SKU & quota checks @@ -332,11 +335,8 @@ plugin.json # Plugin manifest │ ā”œā”€ā”€ azure-integration-tester/ # Post-deploy tests │ ā”œā”€ā”€ azure-resource-visualizer/ # Architecture diagrams │ └── prereq-check/ # CLI tool + auth session verification -└── workflows/ - ā”œā”€ā”€ git-ape-plan.exampleyml # Activated to .yml by /git-ape-onboarding - ā”œā”€ā”€ git-ape-deploy.exampleyml - ā”œā”€ā”€ git-ape-destroy.exampleyml - └── git-ape-verify.exampleyml +└── workflows/ # CI workflows for the git-ape repo itself + └── git-ape-onboarding-template-check.yml # Verifies scaffold parity ``` See [plugin.json](plugin.json) and [.github/plugin/marketplace.json](.github/plugin/marketplace.json) for packaging details. diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js index 185bb95..85b20b3 100644 --- a/scripts/generate-docs.js +++ b/scripts/generate-docs.js @@ -354,67 +354,92 @@ function generateWorkflowDocs() { const outDir = path.join(DOCS_OUT, 'workflows'); ensureDir(outDir); - const workflowFiles = fs.readdirSync(WORKFLOWS_DIR).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.exampleyml')); + // Two sources of workflows: + // - Repo CI: .github/workflows/ — runs in the git-ape repo itself + // - User-facing templates: .github/skills/git-ape-onboarding/templates/workflows/ + // scaffolded into a user's repo by /git-ape-onboarding + const SOURCES = [ + { + kind: 'repo', + label: 'Repo CI', + dir: WORKFLOWS_DIR, + sourcePrefix: '.github/workflows', + filter: (f) => f.endsWith('.yml') || f.endsWith('.yaml'), + note: '', + }, + { + kind: 'template', + label: 'User-facing (scaffolded)', + dir: path.join(ROOT, '.github', 'skills', 'git-ape-onboarding', 'templates', 'workflows'), + sourcePrefix: '.github/skills/git-ape-onboarding/templates/workflows', + filter: (f) => f.endsWith('.yml') || f.endsWith('.yaml'), + note: '\n:::info[Scaffolded by `/git-ape-onboarding`]\nThis workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository\'s `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself.\n:::\n', + }, + ]; + const workflows = []; - for (const file of workflowFiles) { - const filePath = path.join(WORKFLOWS_DIR, file); - const raw = readFile(filePath); - let wf; - try { - wf = yaml.load(raw); - } catch (e) { - console.warn(` ⚠ Failed to parse ${file}: ${e.message}`); - continue; - } + for (const source of SOURCES) { + if (!fs.existsSync(source.dir)) continue; + const workflowFiles = fs.readdirSync(source.dir).filter(source.filter); + + for (const file of workflowFiles) { + const filePath = path.join(source.dir, file); + const raw = readFile(filePath); + let wf; + try { + wf = yaml.load(raw); + } catch (e) { + console.warn(` ⚠ Failed to parse ${file}: ${e.message}`); + continue; + } - const name = wf.name || file; - // Normalise slug: strip both .yml/.yaml and .exampleyml extensions - const slug = slugify(file.replace(/\.(example)?ya?ml$/, '')); - const triggers = wf.on || wf.true || {}; - const jobs = wf.jobs || {}; - const jobNames = Object.keys(jobs); - const permissions = wf.permissions || {}; - - workflows.push({ name, slug, file, triggers, jobNames }); - - // Format triggers - let triggerSection = ''; - if (typeof triggers === 'string') { - triggerSection = `- \`${triggers}\``; - } else if (Array.isArray(triggers)) { - triggerSection = triggers.map((t) => `- \`${t}\``).join('\n'); - } else { - for (const [event, config] of Object.entries(triggers)) { - triggerSection += `- **\`${event}\`**`; - if (config && typeof config === 'object') { - if (config.branches) triggerSection += ` — branches: \`${JSON.stringify(config.branches)}\``; - if (config.paths) triggerSection += ` — paths: \`${config.paths.slice(0, 3).join(', ')}${config.paths.length > 3 ? '...' : ''}\``; - if (config.types) triggerSection += ` — types: \`${config.types.join(', ')}\``; + const name = wf.name || file; + const slug = slugify(file.replace(/\.ya?ml$/, '')); + const triggers = wf.on || wf.true || {}; + const jobs = wf.jobs || {}; + const jobNames = Object.keys(jobs); + const permissions = wf.permissions || {}; + + workflows.push({ name, slug, file, triggers, jobNames, kind: source.kind, sourcePrefix: source.sourcePrefix }); + + // Format triggers + let triggerSection = ''; + if (typeof triggers === 'string') { + triggerSection = `- \`${triggers}\``; + } else if (Array.isArray(triggers)) { + triggerSection = triggers.map((t) => `- \`${t}\``).join('\n'); + } else { + for (const [event, config] of Object.entries(triggers)) { + triggerSection += `- **\`${event}\`**`; + if (config && typeof config === 'object') { + if (config.branches) triggerSection += ` — branches: \`${JSON.stringify(config.branches)}\``; + if (config.paths) triggerSection += ` — paths: \`${config.paths.slice(0, 3).join(', ')}${config.paths.length > 3 ? '...' : ''}\``; + if (config.types) triggerSection += ` — types: \`${config.types.join(', ')}\``; + } + triggerSection += '\n'; } - triggerSection += '\n'; } - } - // Format jobs - let jobSection = ''; - for (const [jobId, jobConfig] of Object.entries(jobs)) { - const jobName = jobConfig.name || jobId; - const runsOn = jobConfig['runs-on'] || 'unknown'; - const env = jobConfig.environment || ''; - const needs = jobConfig.needs || []; - const stepCount = (jobConfig.steps || []).length; - - jobSection += `### \`${jobId}\`\n\n`; - jobSection += `| Property | Value |\n|----------|-------|\n`; - jobSection += `| **Display Name** | ${jobName} |\n`; - jobSection += `| **Runs On** | \`${runsOn}\` |\n`; - if (env) jobSection += `| **Environment** | \`${typeof env === 'string' ? env : env.name || JSON.stringify(env)}\` |\n`; - if (needs.length > 0) jobSection += `| **Depends On** | ${(Array.isArray(needs) ? needs : [needs]).map(n => `\`${n}\``).join(', ')} |\n`; - jobSection += `| **Steps** | ${stepCount} |\n\n`; - } + // Format jobs + let jobSection = ''; + for (const [jobId, jobConfig] of Object.entries(jobs)) { + const jobName = jobConfig.name || jobId; + const runsOn = jobConfig['runs-on'] || 'unknown'; + const env = jobConfig.environment || ''; + const needs = jobConfig.needs || []; + const stepCount = (jobConfig.steps || []).length; + + jobSection += `### \`${jobId}\`\n\n`; + jobSection += `| Property | Value |\n|----------|-------|\n`; + jobSection += `| **Display Name** | ${jobName} |\n`; + jobSection += `| **Runs On** | \`${runsOn}\` |\n`; + if (env) jobSection += `| **Environment** | \`${typeof env === 'string' ? env : env.name || JSON.stringify(env)}\` |\n`; + if (needs.length > 0) jobSection += `| **Depends On** | ${(Array.isArray(needs) ? needs : [needs]).map(n => `\`${n}\``).join(', ')} |\n`; + jobSection += `| **Steps** | ${stepCount} |\n\n`; + } - const content = `--- + const content = `--- title: "${name}" sidebar_label: "${name.replace('Git-Ape: ', '')}" description: "GitHub Actions workflow: ${name}" @@ -422,8 +447,8 @@ description: "GitHub Actions workflow: ${name}" # ${name} -**Workflow file:** \`.github/workflows/${file}\` -${file.endsWith('.exampleyml') ? '\n:::info[Activation required]\nThis workflow ships as `' + file + '` and is **inert** until renamed to `' + file.replace(/\.exampleyml$/, '.yml') + '`. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames every `.exampleyml` file in `.github/workflows/` to `.yml` after you complete the experimental-status acknowledgments.\n:::\n' : ''} +**Workflow file:** \`${source.sourcePrefix}/${file}\` +${source.note} ## Triggers ${triggerSection} @@ -450,9 +475,15 @@ ${raw} `; - writeAutoGenerated(path.join(outDir, `${slug}.md`), `.github/workflows/${file}`, content); + writeAutoGenerated(path.join(outDir, `${slug}.md`), `${source.sourcePrefix}/${file}`, content); + } } + // Partition for the overview + const templateWorkflows = workflows.filter((w) => w.kind === 'template'); + const repoWorkflows = workflows.filter((w) => w.kind === 'repo'); + const renderRow = (w) => `| [${w.name}](./${w.slug}) | \`${w.sourcePrefix}/${w.file}\` | ${Object.keys(typeof w.triggers === 'object' && !Array.isArray(w.triggers) ? w.triggers : {}).join(', ') || String(w.triggers)} | ${w.jobNames.join(', ')} |`; + // Generate overview const overviewContent = `--- title: "CI/CD Workflows Overview" @@ -465,15 +496,21 @@ description: "Overview of Git-Ape GitHub Actions workflows" Git-Ape provides GitHub Actions workflows for automated deployment lifecycle management. -:::info[Activation required] -Workflows ship as **\`*.exampleyml\`** files in \`.github/workflows/\` so they are inert when the plugin is first installed. The [\`/git-ape-onboarding\`](/docs/skills/git-ape-onboarding) flow renames each \`.exampleyml\` to \`.yml\` after you complete the experimental-status acknowledgments. Files still ending in \`.exampleyml\` in the inventory below are not yet active. +:::info[Workflows ship via the onboarding skill] +The user-facing workflows below are **shipped as templates** under \`.github/skills/git-ape-onboarding/templates/workflows/\` and **scaffolded into your repository** by the [\`/git-ape-onboarding\`](/docs/skills/git-ape-onboarding) flow. The scaffold step uses **skip-with-notice on collision** — it never overwrites an existing file. The workflows ship as ready-to-run \`.yml\` files (no manual rename needed) and do not run inside the git-ape repo itself. ::: -## Workflow Inventory +## User-facing workflows (scaffolded into your repo) + +| Workflow | Template file | Triggers | Jobs | +|----------|---------------|----------|------| +${templateWorkflows.map(renderRow).join('\n')} + +## Repo CI workflows (run inside the git-ape repo) | Workflow | File | Triggers | Jobs | |----------|------|----------|------| -${workflows.map((w) => `| [${w.name}](./${w.slug}) | \`${w.file}\` | ${Object.keys(typeof w.triggers === 'object' && !Array.isArray(w.triggers) ? w.triggers : {}).join(', ') || String(w.triggers)} | ${w.jobNames.join(', ')} |`).join('\n')} +${repoWorkflows.map(renderRow).join('\n')} ## Pipeline Architecture diff --git a/website/docs/agents/git-ape-onboarding.md b/website/docs/agents/git-ape-onboarding.md index edfb87b..eef2c6e 100644 --- a/website/docs/agents/git-ape-onboarding.md +++ b/website/docs/agents/git-ape-onboarding.md @@ -39,65 +39,89 @@ Do not use this workflow for production onboarding without manual review of RBAC You are **Git-Ape Onboarding**, responsible for setting up a repository to use Git-Ape deployment workflows. +**Always identify yourself as "Git-Ape Onboarding" in your responses.** Never describe yourself as a generic "software engineering assistant", "GitHub Copilot CLI", or any other persona — this agent has a single, narrow purpose and your identity is part of its contract. + +## Identity (non-negotiable) + +You MUST begin every response with a sentence that names you as **Git-Ape Onboarding** (e.g., "As Git-Ape Onboarding, ..."). You are NOT "GitHub Copilot CLI", NOT a "software engineering assistant", NOT a generic assistant. If the request is off-topic, your refusal MUST still open with your own name and redirect to your specialty (onboarding a repository for Git-Ape: OIDC, federated credentials, RBAC, GitHub environments, scaffolding `.github/workflows/*` and `copilot-instructions.md`). Never use the phrase "software engineering assistant" or "GitHub Copilot CLI" about yourself. + +**Forbidden opening phrases** (never start a reply with any of these, even on refusals): `"I'm GitHub Copilot"`, `"I am GitHub Copilot"`, `"I'm a software engineering assistant"`, `"As a software engineering assistant"`, `"I am an AI assistant"`. The very first sentence of every reply must literally contain the string `"Git-Ape Onboarding"`. + ## Your Role Guide the user through onboarding by executing the playbook defined in the `/git-ape-onboarding` skill. Do not depend on a repository script for onboarding logic. Use the skill as the source of truth. +## Branch naming (non-negotiable) + +The default branch for every onboarded repository is **`main`**. Never use `master` in any of the following: + +- Federated credential names — use `fc-main-branch`, never `fc-master-branch`. +- Federated credential subjects — use `:ref:refs/heads/main`, never `refs/heads/master`. +- GitHub environment branch policies — allow `main`, never `master`. +- Example `az` / `gh` invocations, summaries, or chat output. + +If the user's repository genuinely uses a non-`main` default branch, prompt for the value once and use the user-supplied string verbatim. Do not silently substitute `master` or any other auto-detected name. + ## Use Skill Always use the `/git-ape-onboarding` skill for procedure and command patterns. +## Required user inputs (gated step-1) + +Before any state-changing command runs, you MUST surface a checklist of the required inputs in your first reply and wait for the user to supply any that are missing. Even when the user's opening prompt already names a few (e.g., repo + env + auth method), enumerate the full list so the user can fill the gaps in a single round-trip. At minimum, request the following **six** inputs (rendered as a numbered list, table, or explicit question block — never inferred silently): + +1. **Target GitHub repository** — `/` plus confirmation of the default branch (assume `main`; only change if the user explicitly says otherwise — never silently substitute `master`). +2. **Onboarding mode** — single-environment vs multi-environment (dev/staging/prod). Even if the prompt names one, restate it explicitly for confirmation. +3. **Azure subscription target(s)** — the subscription ID (or name to look up) for each environment. +4. **RBAC role model** — which role(s) to assign on subscription scope (`Contributor`, `Owner`, `User Access Administrator`, or a custom role). Default suggestion: `Contributor`. +5. **Default Azure region** — primary region for the workload (e.g., `eastus`, `westus2`). Used for naming validation and federated credential auditing context. +6. **Project / deployment name** — short slug used to name the App Registration (`sp--`), federated credentials (`fc---main-branch`), and downstream Git-Ape deployments. + +Treat this as a **non-negotiable contract** for the gated first reply: regardless of how much the user pre-filled, the reply must explicitly enumerate ≄3 outstanding asks (and ideally the full list above) so the user sees exactly what's still needed. Do not race ahead to OIDC / federated-credential output until inputs 1–6 are supplied and Azure auth is confirmed. + ## Workflow -1. Confirm target repository URL. -2. Ask whether onboarding is single-environment or multi-environment. -3. Confirm subscription target(s) and RBAC role model. +1. Confirm target repository URL **and default branch** (input #1 above). +2. Ask whether onboarding is single-environment or multi-environment (input #2). +3. Confirm subscription target(s), RBAC role model, default region, and project name (inputs #3–#6). 4. Validate prerequisites: - `az`, `gh`, `jq` installed - Azure authenticated (`az account show`) - GitHub authenticated (`gh auth status`) + - GitHub org OIDC subject format: `gh api orgs//actions/oidc/customization/sub --jq '.use_default'` (drives federated credential subject shape) 5. Echo intended changes and ask for explicit confirmation. 6. Execute onboarding by running the required `az` and `gh` commands directly. 7. For OIDC setup, detect whether the GitHub org uses default or ID-based subject claims before creating federated credentials. -8. Ask compliance framework and enforcement mode preferences (Step 9 in `/git-ape-onboarding` skill playbook). -9. Update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md` with the user's choices. -10. Display experimental warning and ask for three explicit acknowledgments: - - "I understand Git-Ape is experimental and not production-ready" - - "I will review all deployment plans in PRs before merging to main" - - "I acknowledge this setup must not deploy to production yet" -11. Execute workflow activation (Step 11 in `/git-ape-onboarding` skill playbook) to rename `.exampleyml` files to `.yml` only if all acknowledgments are confirmed. -12. Summarize created/updated artifacts and next checks. - -## Acknowledgment Phase +8. Scaffold workflow files and `.github/copilot-instructions.md` into the user's working copy by running the appropriate scaffold script from the skill directory (Step 9 in `/git-ape-onboarding` skill playbook). Pick the runtime that matches the user's shell: + - macOS / Linux / WSL: `./scripts/scaffold-repo.sh` + - Windows (PowerShell 7+): `pwsh ./scripts/scaffold-repo.ps1` + Both scripts produce byte-identical output. Report which files were created vs skipped. +9. Ask compliance framework and enforcement mode preferences (Step 10 in `/git-ape-onboarding` skill playbook). +10. Update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md` with the user's choices. If the file was skipped by the scaffold step or lacks that section, surface the captured preferences in chat for manual integration instead of mutating the file. +11. Summarize created/updated artifacts and next checks. -Before activating workflows, you MUST collect explicit acknowledgments using `vscode_askQuestions`. Present three questions: - -1. **Question 1:** - - Header: `experimental-status` - - Question: "Do you understand that Git-Ape is currently experimental and not production-ready?" - - Options: Yes / No +## Output Requirements -2. **Question 2:** - - Header: `review-plans` - - Question: "Will you review all deployment plans in PRs before merging to main?" - - Options: Yes / No +- Keep output concise and stage-based: prerequisites, confirmation, execution, scaffold, summary. +- Report scaffolded files explicitly: list which workflow files and `copilot-instructions.md` were created vs skipped. +- Never print secret values. +- If onboarding fails, report the failing stage and recommended fix. -3. **Question 3:** - - Header: `no-production` - - Question: "Do you acknowledge that this setup must not be used to deploy to production environments yet?" - - Options: Yes / No +## Non-goals -If ANY answer is "No", report: "Workflow activation cancelled. You can enable workflows later by renaming `.exampleyml` files to `.yml` in `.github/workflows/` when ready." -If ALL answers are "Yes", proceed to Step 11 (workflow activation via skill). +This agent does **not**: -## Output Requirements +- Deploy Azure resources or run ARM/Bicep templates — that is `/git-ape`'s job. +- Create, update, or merge pull requests. +- Modify production workloads or runtime configuration. +- Rotate, read, or print existing secrets — it only wires up references and identities. +- Run `git add`, `git commit`, `git push`, or open a pull request for any scaffolded file. Leave them unstaged so the user decides how to land them. +- Overwrite existing `.github/workflows/*` files or `.github/copilot-instructions.md`. The scaffold helper enforces skip-with-notice; never bypass it. +- Modify Azure resources beyond what the skill playbook authorizes (Entra app + federated credentials + RBAC + secrets + environments). -- Keep output concise and stage-based: prerequisites, confirmation, execution, summary. -- Never print secret values. -- If onboarding fails, report the failing stage and recommended fix. -- Display workflow activation status (activated or deferred) in final summary. +If a request is unrelated to onboarding (e.g., general coding, unrelated cloud topics, off-topic questions), identify yourself as **Git-Ape Onboarding**, decline the request in one sentence, and redirect the user to: (a) onboarding their repository for Git-Ape, or (b) `/git-ape` for an actual Azure deployment. Do not fall back to a generic "software engineering assistant" persona. ## Validation After Onboarding diff --git a/website/docs/getting-started/onboarding.md b/website/docs/getting-started/onboarding.md index a1e5c61..bd666dd 100644 --- a/website/docs/getting-started/onboarding.md +++ b/website/docs/getting-started/onboarding.md @@ -24,8 +24,8 @@ Git-Ape can automate the entire setup for you, or you can run each step manually Both paths produce the same result: an Entra ID App Registration with OIDC federated credentials, RBAC role assignments, and GitHub environments with the required secrets. -:::info[Workflow activation is part of onboarding] -Git-Ape ships its CI/CD workflows as **`*.exampleyml`** files in `.github/workflows/` (`git-ape-plan.exampleyml`, `git-ape-deploy.exampleyml`, `git-ape-destroy.exampleyml`, `git-ape-verify.exampleyml`). These files are **inert** until the onboarding flow renames each one to `.yml`. The automated `/git-ape-onboarding` flow performs this rename only after you complete the experimental-status acknowledgments; the manual flow includes a final step to rename them yourself. +:::info[Workflow scaffolding is part of onboarding] +Git-Ape's CI/CD workflows ship as **canonical templates** inside the onboarding skill at `.github/skills/git-ape-onboarding/templates/workflows/`. After identity, secrets, and environments are configured, the `/git-ape-onboarding` flow runs the scaffold script (`scaffold-repo.sh` / `scaffold-repo.ps1`) to copy these templates into your repository's `.github/workflows/` directory as ready-to-run `.yml` files. The scaffold uses **skip-with-notice on collision** — it never overwrites a customized file. Alongside the workflows, the scaffold also drops a `.github/copilot-instructions.md` deployment-standards file. ::: ## Choose single or multi-environment mode {#choose-mode} diff --git a/website/docs/reference/marketplace.md b/website/docs/reference/marketplace.md index 65aa33f..f473c18 100644 --- a/website/docs/reference/marketplace.md +++ b/website/docs/reference/marketplace.md @@ -17,12 +17,12 @@ The marketplace manifest configures how Git-Ape appears in the Copilot CLI plugi |-------|-------| | **Name** | git-ape | | **Owner** | Microsoft | -| **Version** | 0.0.3 | +| **Version** | 0.1.1 | | **Description** | Git-Ape — Intelligent Azure deployment agent and skill suite for GitHub Copilot. Onboard any repository with guided ARM template generation, security analysis, cost estimation, drift detection, and automated CI/CD pipelines. | ## Plugins -- **git-ape** v0.0.3: Intelligent Azure deployment agent system for GitHub Copilot. Provides guided, safe, and validated Azure resource deployments using ARM templates, with built-in security analysis, cost estimation, drift detection, and CI/CD pipeline integration. +- **git-ape** v0.1.1: Intelligent Azure deployment agent system for GitHub Copilot. Provides guided, safe, and validated Azure resource deployments using ARM templates, with built-in security analysis, cost estimation, drift detection, and CI/CD pipeline integration. - **ape-context** v1.0.0: Extension for git-ape that provides enhanced context management, allowing platform teams to set up a baseline for Engineering context, tools use & intent ## Full Source @@ -36,13 +36,13 @@ The marketplace manifest configures how Git-Ape appears in the Copilot CLI plugi }, "metadata": { "description": "Git-Ape — Intelligent Azure deployment agent and skill suite for GitHub Copilot. Onboard any repository with guided ARM template generation, security analysis, cost estimation, drift detection, and automated CI/CD pipelines.", - "version": "0.0.3" + "version": "0.1.1" }, "plugins": [ { "name": "git-ape", "description": "Intelligent Azure deployment agent system for GitHub Copilot. Provides guided, safe, and validated Azure resource deployments using ARM templates, with built-in security analysis, cost estimation, drift detection, and CI/CD pipeline integration.", - "version": "0.0.3", + "version": "0.1.1", "source": "." }, { diff --git a/website/docs/reference/plugin-json.md b/website/docs/reference/plugin-json.md index 155b0fc..7f03cdd 100644 --- a/website/docs/reference/plugin-json.md +++ b/website/docs/reference/plugin-json.md @@ -16,7 +16,7 @@ The plugin manifest defines the Git-Ape plugin metadata. The same manifest is co | Field | Value | |-------|-------| | **Name** | git-ape | -| **Version** | 0.0.3 | +| **Version** | 0.1.1 | | **Description** | Intelligent agent system for deploying any Azure workload through GitHub Copilot. Provides guided, safe, and validated deployments using ARM templates, with built-in security analysis, cost estimation, and CI/CD pipeline integration. | | **Author** | Microsoft | | **License** | MIT | @@ -33,7 +33,7 @@ The plugin manifest defines the Git-Ape plugin metadata. The same manifest is co { "name": "git-ape", "description": "Intelligent agent system for deploying any Azure workload through GitHub Copilot. Provides guided, safe, and validated deployments using ARM templates, with built-in security analysis, cost estimation, and CI/CD pipeline integration.", - "version": "0.0.3", + "version": "0.1.1", "author": { "name": "Microsoft", "url": "https://github.com/Azure/git-ape" @@ -54,6 +54,7 @@ The plugin manifest defines the Git-Ape plugin metadata. The same manifest is co "copilot-agents" ], "agents": ".github/agents/", - "skills": ".github/skills/" + "skills": ".github/skills/", + "prompts": ".github/prompts/" } ``` diff --git a/website/docs/skills/git-ape-onboarding.md b/website/docs/skills/git-ape-onboarding.md index 82920d5..5718723 100644 --- a/website/docs/skills/git-ape-onboarding.md +++ b/website/docs/skills/git-ape-onboarding.md @@ -45,6 +45,7 @@ This skill configures: 3. RBAC role assignment(s) on subscription scope 4. GitHub environments (`azure-deploy*`, `azure-destroy`) 5. Required GitHub secrets (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`) +6. Scaffolded GitHub Actions workflow files (`git-ape-plan.yml`, `-deploy.yml`, `-destroy.yml`, `-verify.yml`, `-drift.{md,lock.yml}`) and deployment standards (`.github/copilot-instructions.md`) into the user's working copy ## Prerequisites @@ -60,6 +61,14 @@ Do NOT proceed with onboarding until prereq-check reports **āœ… READY**. Additionally, the Azure identity used must have **Owner** or **User Access Administrator** on the target subscription(s), and the GitHub identity must have **admin** access to the target repository. +## Invariants + +These rules are non-negotiable. The agent MUST NOT improvise around them. + +- **Default branch is always `main`.** Never use `master`, never auto-detect a non-`main` default, and never substitute any other name. All federated credential subjects, environment branch policies, and example commands use `refs/heads/main` / the literal string `main`. If a user's repository uses something other than `main`, prompt for it once and use the user-supplied value explicitly — never silently default to `master`. +- **Federated credential names use the `fc-main-branch` form,** not `fc-master-branch`. See Step 5 for the canonical subject strings. +- **Workflows ship `main`-targeted triggers.** The scaffold step copies workflow files that reference `branches: [main]`; do not rewrite them to `master`. + ## Execution Modes ### Interactive (recommended for first-time use) @@ -108,16 +117,59 @@ OIDC_PREFIX="repo:/" # if org customization returns false OIDC_PREFIX="repository_owner_id::repository_id:" ``` -5. Create federated credentials for `main`, `pull_request`, `azure-deploy*`, and `azure-destroy`. +5. Create federated credentials with these canonical subjects (always `refs/heads/main` — never `master`): + - `fc-main-branch` subject `"$OIDC_PREFIX:ref:refs/heads/main"` description `"Main branch deployments"` + - `fc-pull-request` subject `"$OIDC_PREFIX:pull_request"` description `"Pull request plan/validate"` + - `fc-azure-deploy` subject `"$OIDC_PREFIX:environment:azure-deploy"` (one per environment in multi-env mode) + - `fc-azure-destroy` subject `"$OIDC_PREFIX:environment:azure-destroy"` 6. Assign RBAC on each target subscription. 7. Set GitHub repo or environment secrets. 8. Create GitHub environments and branch policies when permissions allow. -9. Capture compliance and Azure Policy preferences (see below). -10. Collect explicit acknowledgments for experimental status and production safety. -11. Activate workflows by renaming `.exampleyml` to `.yml` (only if all acknowledgments confirmed; see Step 11 section below). -12. Verify federated credentials, role assignments, secrets, and workflow activation. +9. Scaffold workflow files and deployment standards into the user's working copy (see below). +10. Capture compliance and Azure Policy preferences (see below). +11. Verify federated credentials, role assignments, and secrets. + +### Step 9: Scaffold workflow files and deployment standards + +The GitHub Actions workflows that power Git-Ape (`git-ape-plan.yml`, +`-deploy.yml`, `-destroy.yml`, `-verify.yml`, `-drift.md`, `-drift.lock.yml`) +and the deployment standards file (`.github/copilot-instructions.md`) ship +as templates inside this skill at `./templates/`. -### Step 9: Compliance & Azure Policy Preferences +After identity, secrets, and environments are configured, run the scaffold +helper to copy these templates into the user's working copy. Two parity +implementations ship — pick the one that matches the user's shell: + +```bash +# macOS / Linux / WSL +./scripts/scaffold-repo.sh +``` + +```powershell +# Windows (PowerShell 7+) +pwsh .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 +``` + +Both scripts produce byte-identical output and follow the same rules below. +The onboarding-template-check workflow enforces parity on every PR. + +The helper: + +- Resolves the target repo root via `git rev-parse --show-toplevel` (override + by passing an explicit path as the first argument). +- Copies each template only if the destination does not already exist + (**skip-with-notice on collision** — never overwrites a customized file). +- Prints `āœ“ Created` for new files, `āŠ Skipped` for collisions, and a final + `Created N file(s), skipped M file(s).` summary. +- Leaves all files **unstaged**. It does not run `git add`, `git commit`, + `git push`, or open a pull request — the user decides how to land them. +- For each skipped file, prints a `diff -u` command pointing at the + canonical template so the user can reconcile manually. + +If the user already had a custom `.github/copilot-instructions.md`, the +scaffold step skips it. Step 10 (below) handles that case explicitly. + +### Step 10: Compliance & Azure Policy Preferences After RBAC and environment setup, ask the user about compliance requirements and update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md`: @@ -138,79 +190,31 @@ After RBAC and environment setup, ask the user about compliance requirements and ``` 3. **Update `copilot-instructions.md`** with the user's choices: - - Edit the `## Compliance & Azure Policy` → `### Compliance Frameworks` section - - Set the `### Policy Enforcement Mode` default to the user's choice - - Commit the update as part of the onboarding changes - -### Step 11: Activate GitHub Workflows - -After collecting acknowledgments for experimental status and production safety (see agent's "Acknowledgment Phase"), activate the Git-Ape workflows by renaming `.exampleyml` files to `.yml` in the `.github/workflows/` directory. - -**Files to activate:** -- `git-ape-plan.exampleyml` → `git-ape-plan.yml` (validates template and shows what-if) -- `git-ape-deploy.exampleyml` → `git-ape-deploy.yml` (executes deployments) -- `git-ape-destroy.exampleyml` → `git-ape-destroy.yml` (tears down resources) -- `git-ape-verify.exampleyml` → `git-ape-verify.yml` (runs verification steps) - -**Rename commands (Unix/macOS/Linux):** -```bash -cd .github/workflows -for f in *.exampleyml; do - target="${f%.exampleyml}.yml" - mv "$f" "$target" - echo "Renamed: $f -> $target" -done -``` - -**Rename commands (Windows PowerShell):** -```powershell -cd .github\workflows -Get-ChildItem *.exampleyml | ForEach-Object { - $newName = $_.Name -replace '\.exampleyml$', '.yml' - Rename-Item -Path $_.FullName -NewName $newName - Write-Host "Renamed: $($_.Name) -> $newName" -} -``` - -**Verification (all platforms):** -```bash -ls .github/workflows/git-ape-*.yml -``` - -Should output: -``` -git-ape-deploy.yml -git-ape-destroy.yml -git-ape-plan.yml -git-ape-verify.yml -``` - -**Output after activation:** -Display summary: -``` -āœ… Workflows activated: - - git-ape-plan.yml (validates and plans deployments) - - git-ape-deploy.yml (executes deployments and integration tests) - - git-ape-destroy.yml (tears down resources when requested) - - git-ape-verify.yml (runs post-deployment verification) - -Next steps: -1. Review .github/workflows/git-ape-*.yml for familiarity -2. Push changes to a feature branch and open a PR -3. Verify the plan workflow runs and shows what-if analysis in the PR comment -4. For first deployment, merge to main and monitor git-ape-deploy.yml execution -``` + - If the file does not exist (scaffold step was skipped or scaffolding + was not run), print the captured preferences in chat and ask the user + to add them manually. Do NOT create a new file from scratch — that is + the scaffold step's responsibility. + - If the file exists AND contains a `## Compliance & Azure Policy` + section, edit the `### Compliance Frameworks` and + `### Policy Enforcement Mode` subsections in place. + - If the file exists but does NOT contain that section (user has a + customized file), do NOT mutate it. Instead, print the captured + preferences and a suggested patch in chat so the user can apply it. + - In all cases, leave changes unstaged and let the user commit them. ## Safe-Execution Rules 1. Echo target repository and subscription(s) before execution. 2. Require explicit user confirmation before running onboarding. 3. Never print secret values in chat output. -4. **Require explicit acknowledgments before activating workflows** — User must confirm Git-Ape is experimental, will review plans, and won't deploy to production. -5. **Only activate workflows if ALL acknowledgments are confirmed** — Renaming happens only after explicit "Yes" to all three questions. -6. If user refuses any acknowledgment, complete onboarding but skip workflow activation. User can enable later manually. -7. Summarize what was created or updated (app registration, federated credentials, role assignments, GitHub environments, workflows activated). -8. If onboarding fails, surface the failing step and command context, then stop. +4. Summarize what was created or updated (app registration, federated credentials, role assignments, GitHub environments, scaffolded files). +5. If onboarding fails, surface the failing step and command context, then stop. +6. Never overwrite an existing `.github/workflows/*` file or + `.github/copilot-instructions.md`. The scaffold helper enforces + skip-with-notice; do not bypass it. +7. Never run `git add`, `git commit`, `git push`, or open a PR for the + scaffolded files — leave them unstaged so the user decides how to land + them. ## Suggested Agent Flow @@ -218,13 +222,11 @@ Next steps: 2. Confirm target repo URL, onboarding mode, and role model. 3. Validate current Azure/GitHub auth context (subscription, tenant, GitHub org). 4. Ask for final confirmation. -5. Execute the required Azure CLI and GitHub CLI commands directly from this playbook (Steps 1-8). -6. Ask compliance framework and enforcement mode preferences (Step 9 in playbook). -7. Update `copilot-instructions.md` with compliance preferences. -8. **Display experimental warning and collect acknowledgments** (three explicit "Yes" answers required). -9. If all acknowledgments confirmed, execute workflow activation (Step 11 in playbook). -10. If any acknowledgment refused, skip workflow activation (workflows remain `.exampleyml`). -11. Summarize outcome, activated workflows (if any), and suggest verification commands. +5. Execute the required Azure CLI and GitHub CLI commands directly from this playbook. +6. Scaffold workflow files and `copilot-instructions.md` via `./scripts/scaffold-repo.sh` on macOS/Linux/WSL, or `pwsh ./scripts/scaffold-repo.ps1` on Windows (Step 9 in playbook). Report which files were created vs skipped. +7. Ask compliance framework and enforcement mode preferences (Step 10 in playbook). +8. Update `copilot-instructions.md` with compliance preferences — or, if the file was skipped by the scaffold step, surface the preferences in chat for manual integration. +9. Summarize outcome (including scaffolded file counts) and suggest verification commands. ## Known Gotchas diff --git a/website/docs/workflows/daily-repo-status-lock.md b/website/docs/workflows/daily-repo-status-lock.md index c1a72b0..37442f4 100644 --- a/website/docs/workflows/daily-repo-status-lock.md +++ b/website/docs/workflows/daily-repo-status-lock.md @@ -38,7 +38,7 @@ _Inherited from repository defaults_ | **Display Name** | agent | | **Runs On** | `ubuntu-latest` | | **Depends On** | `activation` | -| **Steps** | 38 | +| **Steps** | 39 | ### `conclusion` @@ -75,8 +75,8 @@ _Inherited from repository defaults_ Click to view full workflow YAML ```yaml -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"894339bb13dd70330e962756b5d6c9f0b1e2a45fa72eb57fed9eda15479a0af4","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.72.1","version":"v0.72.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e28027ab984b676c4452279cea43d3b83bc209351eb48ea197ca509a66e3cc85","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -91,7 +91,7 @@ _Inherited from repository defaults_ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.72.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. # # To update this file, edit githubnext/agentics/workflows/daily-repo-status.md@fc4ab36dedc44e2a1cdc195cecce262f06c81230 and run: # gh aw compile @@ -118,26 +118,25 @@ _Inherited from repository defaults_ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@v0.75.4 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.41 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.41 -# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c -# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 -# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Daily Repo Status" -"on": +on: schedule: - - cron: "25 10 * * *" - # Friendly format: daily (scattered) + - cron: "0 0 * * *" workflow_dispatch: inputs: aw_context: default: "" - description: Agent caller context (used internally by Agentic Workflows). + description: "Agent caller context (used internally by Agentic Workflows)." required: false type: string @@ -161,37 +160,44 @@ jobs: lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Repo Status" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.40" - GH_AW_INFO_AGENT_VERSION: "1.0.40" - GH_AW_INFO_CLI_VERSION: "v0.72.1" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" GH_AW_INFO_WORKFLOW_NAME: "Daily Repo Status" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.41" + GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@fc4ab36dedc44e2a1cdc195cecce262f06c81230" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -212,6 +218,7 @@ jobs: sparse-checkout: | .github .agents + .antigravity .claude .codex .crush @@ -222,8 +229,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file @@ -241,7 +248,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.72.1" + GH_AW_COMPILED_VERSION: "v0.76.1" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -252,11 +259,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -264,54 +271,54 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_310a08ed815a7265_EOF' + cat << 'GH_AW_PROMPT_9dd56e6c580fbeb5_EOF' - GH_AW_PROMPT_310a08ed815a7265_EOF + GH_AW_PROMPT_9dd56e6c580fbeb5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_310a08ed815a7265_EOF' + cat << 'GH_AW_PROMPT_9dd56e6c580fbeb5_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_310a08ed815a7265_EOF + GH_AW_PROMPT_9dd56e6c580fbeb5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_310a08ed815a7265_EOF' + cat << 'GH_AW_PROMPT_9dd56e6c580fbeb5_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - GH_AW_PROMPT_310a08ed815a7265_EOF + GH_AW_PROMPT_9dd56e6c580fbeb5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_310a08ed815a7265_EOF' + cat << 'GH_AW_PROMPT_9dd56e6c580fbeb5_EOF' {{#runtime-import .github/workflows/daily-repo-status.md}} - GH_AW_PROMPT_310a08ed815a7265_EOF + GH_AW_PROMPT_9dd56e6c580fbeb5_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -328,11 +335,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -348,11 +355,11 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, @@ -383,6 +390,7 @@ jobs: /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/base /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -403,29 +411,36 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: dailyrepostatus outputs: - agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} - mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} - model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Repo Status" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | @@ -472,11 +487,11 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 - name: Parse integrity filter lists id: parse-guard-vars env: @@ -492,24 +507,28 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: GH_AW_SUB_AGENT_DIR: ".github/agents" GH_AW_SUB_AGENT_EXT: ".agent.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1318fba0649d9410_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_631dae4d655044a9_EOF' {"create_issue":{"close_older_issues":true,"labels":["report","daily-status"],"max":1,"title_prefix":"[repo-status] "},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_1318fba0649d9410_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_631dae4d655044a9_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -531,6 +550,9 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "fields": { + "type": "array" + }, "labels": { "type": "array", "itemType": "string", @@ -702,17 +724,22 @@ jobs: export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_748eb5049957e6dc_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_ab444830d49295a4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -751,7 +778,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_748eb5049957e6dc_EOF + GH_AW_MCP_CONFIG_ab444830d49295a4_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -779,25 +806,32 @@ jobs: timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.72.1 + GH_AW_VERSION: v0.76.1 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -812,11 +846,11 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] XDG_CONFIG_HOME: /home/runner - - name: Detect Copilot errors - id: detect-copilot-errors + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -990,6 +1024,7 @@ jobs: concurrency: group: "gh-aw-conclusion-daily-repo-status" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -998,15 +1033,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Repo Status" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1106,6 +1145,8 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} @@ -1118,6 +1159,7 @@ jobs: GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1142,15 +1184,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Repo Status" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1176,7 +1222,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 - name: Check if detection needed id: detection_guard if: always() @@ -1235,11 +1281,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1248,23 +1294,30 @@ jobs: timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.72.1 + GH_AW_VERSION: v0.76.1 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1292,6 +1345,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" with: script: | @@ -1302,10 +1356,11 @@ jobs: await main(); } catch (loadErr) { const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); core.error(msg); core.setOutput('reason', 'parse_error'); - if (continueOnError) { + if (continueOnError && !detectionExecutionFailed) { core.warning('\u26A0\uFE0F ' + msg); core.setOutput('conclusion', 'warning'); core.setOutput('success', 'false'); @@ -1334,7 +1389,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_ENGINE_VERSION: "1.0.52" GH_AW_WORKFLOW_ID: "daily-repo-status" GH_AW_WORKFLOW_NAME: "Daily Repo Status" GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@fc4ab36dedc44e2a1cdc195cecce262f06c81230" @@ -1351,15 +1406,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Repo Status" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1388,10 +1447,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"report\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"report\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/website/docs/workflows/git-ape-actionlint.md b/website/docs/workflows/git-ape-actionlint.md index 4e80183..3dbbec6 100644 --- a/website/docs/workflows/git-ape-actionlint.md +++ b/website/docs/workflows/git-ape-actionlint.md @@ -62,7 +62,18 @@ jobs: # Download the official actionlint binary into the workspace. bash <(curl --silent --show-error --fail \ https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) - ./actionlint -color + # Exclude gh-aw compiler output (*.lock.yml under .github/workflows). + # Those files are generated by `gh aw compile` and their contents are + # the responsibility of the gh-aw compiler, not this repo. + mapfile -t WORKFLOW_FILES < <( + find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) \ + ! -name '*.lock.yml' + ) + if [ "${#WORKFLOW_FILES[@]}" -eq 0 ]; then + echo "No workflow files to lint." + exit 0 + fi + ./actionlint -color "${WORKFLOW_FILES[@]}" ``` diff --git a/website/docs/workflows/git-ape-deploy.md b/website/docs/workflows/git-ape-deploy.md index 8a736e4..416a290 100644 --- a/website/docs/workflows/git-ape-deploy.md +++ b/website/docs/workflows/git-ape-deploy.md @@ -4,15 +4,15 @@ sidebar_label: "Deploy" description: "GitHub Actions workflow: Git-Ape: Deploy" --- - + # Git-Ape: Deploy -**Workflow file:** `.github/workflows/git-ape-deploy.exampleyml` +**Workflow file:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml` -:::info[Activation required] -This workflow ships as `git-ape-deploy.exampleyml` and is **inert** until renamed to `git-ape-deploy.yml`. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames every `.exampleyml` file in `.github/workflows/` to `.yml` after you complete the experimental-status acknowledgments. +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. ::: ## Triggers @@ -57,7 +57,7 @@ This workflow ships as `git-ape-deploy.exampleyml` and is **inert** until rename | **Runs On** | `ubuntu-latest` | | **Environment** | `azure-deploy` | | **Depends On** | `detect-deployments`, `check-comment-trigger` | -| **Steps** | 13 | +| **Steps** | 17 | @@ -114,7 +114,7 @@ jobs: steps: - name: Check comment and PR status id: check - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const comment = context.payload.comment.body.trim(); @@ -262,15 +262,75 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Validate before deploy + - name: Capture pre-deploy state (for rollback) + id: pre_state run: | - az deployment sub validate \ + STACK_NAME="${{ matrix.deployment_id }}" + echo "::group::Pre-deploy state capture" + echo "[$(date -u +%H:%M:%S)] Checking if stack '$STACK_NAME' already exists…" + + # Does the stack currently exist? If yes, this is an UPDATE (rollback possible). + # If no, this is a NEW deployment (rollback = delete partial stack). + if PRIOR_STACK=$(az stack sub show --name "$STACK_NAME" --output json 2>/dev/null); then + PRIOR_STATE=$(echo "$PRIOR_STACK" | jq -r '.provisioningState // "unknown"') + PRIOR_ID=$(echo "$PRIOR_STACK" | jq -r '.id // empty') + echo "stack_existed=true" >> "$GITHUB_OUTPUT" + echo "prior_stack_id=$PRIOR_ID" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] Prior stack found — provisioningState=$PRIOR_STATE" + echo "[$(date -u +%H:%M:%S)] Prior stackId: $PRIOR_ID" + else + echo "stack_existed=false" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] No prior stack — this is a NEW deployment." + fi + + # Also snapshot the previous template from git (parent commit of this merge + # or origin/main for /deploy comment). Used to redeploy last-known-good on failure. + DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" + mkdir -p /tmp/rollback + if git show HEAD~1:"$DEPLOY_DIR/template.json" > /tmp/rollback/template.json 2>/dev/null; then + cp "$DEPLOY_DIR/parameters.json" /tmp/rollback/parameters.json 2>/dev/null || true + # Prefer the previous parameters if they exist at HEAD~1 + git show HEAD~1:"$DEPLOY_DIR/parameters.json" > /tmp/rollback/parameters.json 2>/dev/null || true + echo "prior_template_available=true" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] Previous template captured from HEAD~1 → /tmp/rollback/" + echo " template bytes: $(wc -c < /tmp/rollback/template.json)" + else + echo "prior_template_available=false" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] No previous template in git history (first deployment)" + fi + echo "::endgroup::" + + - name: Validate before deploy (stack) + run: | + echo "::group::Template validation" + echo "[$(date -u +%H:%M:%S)] Validating stack '${{ matrix.deployment_id }}' in '${{ steps.params.outputs.location }}'" + az stack sub validate \ + --name "${{ matrix.deployment_id }}" \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ --output json + echo "[$(date -u +%H:%M:%S)] Validation passed āœ“" + echo "::endgroup::" + + - name: Stage template for security scan + id: scan_stage + run: | + # WORKAROUND: see git-ape-plan.yml for full explanation. Template Analyzer's + # directory walker skips .azure/ on Linux (.NET treats dot-prefixed paths as + # Hidden), so we stage the template into a non-dotted dir at the workspace root. + STAGE_DIR="templateanalyzer-scan/${{ matrix.deployment_id }}" + mkdir -p "$STAGE_DIR" + cp "${{ steps.params.outputs.deploy_dir }}/template.json" "$STAGE_DIR/template.json" + if [[ -f "${{ steps.params.outputs.deploy_dir }}/parameters.json" ]]; then + cp "${{ steps.params.outputs.deploy_dir }}/parameters.json" "$STAGE_DIR/template.parameters.json" + fi + echo "stage_dir=$STAGE_DIR" >> "$GITHUB_OUTPUT" + ls -la "$STAGE_DIR" - name: Run Microsoft Defender for DevOps template analyzer id: security_scan @@ -278,8 +338,10 @@ jobs: uses: microsoft/security-devops-action@v1 with: tools: templateanalyzer - env: - GDN_TEMPLATEANALYZER_INPUT: ${{ steps.params.outputs.deploy_dir }}/template.json + + - name: Cleanup staged template + if: always() + run: rm -rf templateanalyzer-scan - name: Upload SARIF results if: always() && steps.security_scan.outputs.sarifFile != '' @@ -303,24 +365,47 @@ jobs: echo "Security scan passed — no errors found" fi - - name: Deploy to Azure + - name: Deploy to Azure (Deployment Stack) id: deploy run: | - echo "šŸš€ Starting deployment: ${{ matrix.deployment_id }}" + STACK_NAME="${{ matrix.deployment_id }}" + echo "::group::Stack deployment" + echo "[$(date -u +%H:%M:%S)] šŸš€ Starting stack deployment: $STACK_NAME" + echo " location : ${{ steps.params.outputs.location }}" + echo " template : ${{ steps.params.outputs.deploy_dir }}/template.json" + echo " parameters : ${{ steps.params.outputs.deploy_dir }}/parameters.json" + echo " project : ${{ steps.params.outputs.project }}" + echo " environment : ${{ steps.params.outputs.environment }}" + echo " prior stack : ${{ steps.pre_state.outputs.stack_existed }}" START_TIME=$(date +%s) - DEPLOY_OUTPUT=$(az deployment sub create \ - --name "${{ matrix.deployment_id }}" \ + # Enable verbose Azure CLI logging for this step + export AZURE_CORE_OUTPUT=json + + # Create/update the subscription-scope Deployment Stack. + # --action-on-unmanage deleteAll binds the whole stack (RG + contents) + # to a single lifecycle so destroy is idempotent across all scopes. + set +e + DEPLOY_OUTPUT=$(az stack sub create \ + --name "$STACK_NAME" \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --description "Git-Ape deployment $STACK_NAME" \ + --tags "managedBy=git-ape" "deploymentId=$STACK_NAME" \ + --yes \ + --verbose \ --output json 2>&1) - EXIT_CODE=$? + set -e + END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) - echo "deploy_duration=${DURATION}s" >> "$GITHUB_OUTPUT" + echo "[$(date -u +%H:%M:%S)] az stack sub create exited with code $EXIT_CODE after ${DURATION}s" + echo "::endgroup::" if [[ $EXIT_CODE -ne 0 ]]; then echo "deploy_status=failed" >> "$GITHUB_OUTPUT" @@ -329,27 +414,55 @@ jobs: echo "EOF" >> "$GITHUB_OUTPUT" echo "" echo "==========================================" - echo "āŒ DEPLOYMENT FAILED" + echo "āŒ STACK DEPLOYMENT FAILED" echo "==========================================" echo "$DEPLOY_OUTPUT" echo "==========================================" - echo "::error::Deployment failed — see output above for details" + + # Surface the underlying deployment operation errors — the stack error + # is usually just a summary; the real root cause is in the operations list. + echo "::group::Underlying deployment operation errors" + echo "[$(date -u +%H:%M:%S)] Fetching failed operations from deployment '$STACK_NAME'…" + az deployment sub show --name "$STACK_NAME" --output json 2>/dev/null \ + | jq -r '.properties // {}' || echo "No subscription-scope deployment details available." + + # Enumerate per-operation failures with their error messages + az deployment operation sub list --name "$STACK_NAME" --output json 2>/dev/null \ + | jq -r '.[] | select(.properties.provisioningState == "Failed") | + "──────────\nResource : \(.properties.targetResource.resourceName // "n/a") (\(.properties.targetResource.resourceType // "n/a"))\nStatus : \(.properties.statusCode // "n/a")\nMessage : \(.properties.statusMessage.error.message // .properties.statusMessage // "n/a")"' \ + || echo "No operation details available (deployment may not have reached Azure)." + echo "::endgroup::" + + echo "::error::Stack deployment failed — see output above for details" exit 1 fi echo "deploy_status=succeeded" >> "$GITHUB_OUTPUT" - # Extract outputs - OUTPUTS=$(echo "$DEPLOY_OUTPUT" | jq -r '.properties.outputs // {}') + # Capture the stack resource id — this is the single source of truth + # for destroy. Stored in state.json as `stackId`. + STACK_ID=$(echo "$DEPLOY_OUTPUT" | jq -r '.id // empty') + echo "stack_id=$STACK_ID" >> "$GITHUB_OUTPUT" + + # Extract template outputs from the stack + OUTPUTS=$(echo "$DEPLOY_OUTPUT" | jq -r '.outputs // .properties.outputs // {}') echo "deploy_outputs<> "$GITHUB_OUTPUT" echo "$OUTPUTS" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" - # Extract resource group name + # Extract resource group name (for integration tests) RG_NAME=$(echo "$OUTPUTS" | jq -r '.resourceGroupName.value // empty') echo "resource_group=$RG_NAME" >> "$GITHUB_OUTPUT" - echo "āœ… Deployment succeeded in ${DURATION}s" + # Capture the list of managed resources from the stack — this is the + # authoritative manifest for everything the stack will delete on destroy. + MANAGED=$(echo "$DEPLOY_OUTPUT" | jq -c '[(.resources // .properties.resources // [])[] | {id: .id, status: .status}]') + echo "managed_resources<> "$GITHUB_OUTPUT" + echo "$MANAGED" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + echo "āœ… Stack deployed in ${DURATION}s — stackId: $STACK_ID" + echo " Managed resources: $(echo "$MANAGED" | jq 'length')" - name: Run integration tests id: tests @@ -412,31 +525,128 @@ jobs: echo -e "$TEST_RESULTS" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" + - name: Rollback on failure + id: rollback + if: failure() && steps.deploy.outcome == 'failure' + run: | + STACK_NAME="${{ matrix.deployment_id }}" + STACK_EXISTED="${{ steps.pre_state.outputs.stack_existed }}" + PRIOR_TEMPLATE_AVAILABLE="${{ steps.pre_state.outputs.prior_template_available }}" + + echo "::group::Rollback decision" + echo "[$(date -u +%H:%M:%S)] Evaluating rollback strategy…" + echo " stack existed before : $STACK_EXISTED" + echo " prior template available : $PRIOR_TEMPLATE_AVAILABLE" + + ROLLBACK_ACTION="none" + ROLLBACK_STATUS="not-attempted" + + if [[ "$STACK_EXISTED" == "true" && "$PRIOR_TEMPLATE_AVAILABLE" == "true" ]]; then + ROLLBACK_ACTION="redeploy-previous" + echo "[$(date -u +%H:%M:%S)] Strategy: redeploy previous template (last-known-good)" + echo "::endgroup::" + + echo "::group::Rollback — redeploying previous template" + set +e + az stack sub create \ + --name "$STACK_NAME" \ + --location "${{ steps.params.outputs.location }}" \ + --template-file /tmp/rollback/template.json \ + --parameters @/tmp/rollback/parameters.json \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ + --description "Git-Ape ROLLBACK of failed deployment $STACK_NAME" \ + --tags "managedBy=git-ape" "deploymentId=$STACK_NAME" "rollback=true" \ + --yes --verbose --output json + RB_EXIT=$? + set -e + if [[ $RB_EXIT -eq 0 ]]; then + ROLLBACK_STATUS="succeeded" + echo "[$(date -u +%H:%M:%S)] āœ… Rollback succeeded — stack restored to previous template" + else + ROLLBACK_STATUS="failed" + echo "::error::Rollback to previous template FAILED (exit $RB_EXIT) — manual intervention required" + fi + echo "::endgroup::" + elif [[ "$STACK_EXISTED" == "false" ]]; then + ROLLBACK_ACTION="delete-failed-stack" + echo "[$(date -u +%H:%M:%S)] Strategy: delete the failed new stack (clean slate)" + echo "::endgroup::" + + echo "::group::Rollback — tearing down failed new stack" + set +e + az stack sub delete \ + --name "$STACK_NAME" \ + --action-on-unmanage deleteAll \ + --yes --output json + RB_EXIT=$? + set -e + if [[ $RB_EXIT -eq 0 ]]; then + ROLLBACK_STATUS="succeeded" + echo "[$(date -u +%H:%M:%S)] āœ… Failed stack deleted — no orphan resources" + else + ROLLBACK_STATUS="failed" + echo "::error::Failed-stack cleanup FAILED (exit $RB_EXIT) — manual intervention required" + fi + echo "::endgroup::" + else + echo "[$(date -u +%H:%M:%S)] āš ļø No rollback possible: prior stack existed but previous template is not in git history" + echo "::endgroup::" + ROLLBACK_ACTION="manual-required" + fi + + echo "rollback_action=$ROLLBACK_ACTION" >> "$GITHUB_OUTPUT" + echo "rollback_status=$ROLLBACK_STATUS" >> "$GITHUB_OUTPUT" + - name: Save deployment state if: always() run: | DEPLOY_DIR="${{ steps.params.outputs.deploy_dir }}" STATUS="${{ steps.deploy.outputs.deploy_status || 'failed' }}" TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) - - # Create/update state.json - cat > "$DEPLOY_DIR/state.json" < "$DEPLOY_DIR/state.json" - name: Commit deployment state if: always() @@ -472,59 +682,147 @@ jobs: git push || echo "::warning::Could not push state update to main" - name: Post deployment result - if: always() && github.event_name == 'issue_comment' - uses: actions/github-script@v8 + if: always() + uses: actions/github-script@v9 + env: + DEPLOY_ERROR: ${{ steps.deploy.outputs.deploy_error }} + TEST_ENDPOINTS: ${{ steps.tests.outputs.test_endpoints }} + RESOURCES_JSON: ${{ steps.tests.outputs.resources }} with: script: | const deploymentId = '${{ matrix.deployment_id }}'; const status = '${{ steps.deploy.outputs.deploy_status }}' || 'failed'; const duration = '${{ steps.deploy.outputs.deploy_duration }}'; - const outputs = `${{ steps.deploy.outputs.deploy_outputs }}`; - const resources = `${{ steps.tests.outputs.resources }}`; - const testEndpoints = `${{ steps.tests.outputs.test_endpoints }}`; + const rollbackAction = '${{ steps.rollback.outputs.rollback_action }}' || 'none'; + const rollbackStatus = '${{ steps.rollback.outputs.rollback_status }}' || 'not-attempted'; const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`; + const resources = process.env.RESOURCES_JSON || ''; + const testEndpoints = process.env.TEST_ENDPOINTS || ''; + const deployError = process.env.DEPLOY_ERROR || ''; - let comment = `## Git-Ape Deploy: \`${deploymentId}\`\n\n`; + // Build the comment body + let body = `## Git-Ape Deploy: \`${deploymentId}\`\n\n`; if (status === 'succeeded') { - comment += `### āœ… Deployment Succeeded\n\n`; - comment += `- **Duration:** ${duration}\n`; - comment += `- **Workflow Run:** [View logs](${runUrl})\n\n`; + body += `### āœ… Deployment Succeeded\n\n`; + body += `- **Duration:** ${duration}\n`; + body += `- **Workflow Run:** [View logs](${runUrl})\n\n`; - if (testEndpoints) { - comment += `### Endpoints\n\n${testEndpoints}\n\n`; - } + if (testEndpoints) body += `### Endpoints\n\n${testEndpoints}\n\n`; if (resources) { try { const parsed = JSON.parse(resources); - comment += `### Resources (${parsed.length})\n\n`; - comment += `| Name | Type | Status |\n|------|------|--------|\n`; + body += `### Resources (${parsed.length})\n\n| Name | Type | Status |\n|------|------|--------|\n`; for (const r of parsed) { const icon = r.provisioningState === 'Succeeded' ? 'āœ…' : 'āš ļø'; - comment += `| ${r.name} | ${r.type} | ${icon} ${r.provisioningState} |\n`; + body += `| ${r.name} | ${r.type} | ${icon} ${r.provisioningState} |\n`; } - comment += '\n'; + body += '\n'; } catch {} } } else { - comment += `### āŒ Deployment Failed\n\n`; - comment += `- **Workflow Run:** [View logs](${runUrl})\n\n`; - const error = `${{ steps.deploy.outputs.deploy_error }}`; - if (error) { - comment += `\`\`\`\n${error.substring(0, 2000)}\n\`\`\`\n\n`; + body += `### āŒ Deployment Failed\n\n`; + body += `- **Workflow Run:** [View logs](${runUrl})\n`; + + // Rollback summary + const rbIcon = rollbackStatus === 'succeeded' ? 'āœ…' + : rollbackStatus === 'failed' ? 'āŒ' + : 'āš ļø'; + const rbText = { + 'redeploy-previous': 'Redeployed previous template (last-known-good)', + 'delete-failed-stack': 'Deleted partially-provisioned stack (clean slate)', + 'manual-required': 'Manual intervention required — no previous template in git history', + 'none': 'No rollback attempted', + }[rollbackAction] || rollbackAction; + body += `- **Rollback:** ${rbIcon} \`${rollbackAction}\` — ${rbText} (*${rollbackStatus}*)\n\n`; + + if (deployError) { + body += `
Error output\n\n\`\`\`\n${deployError.substring(0, 4000)}\n\`\`\`\n
\n\n`; + } + + body += `### Next steps\n\n`; + if (rollbackStatus === 'succeeded' && rollbackAction === 'redeploy-previous') { + body += `- Environment is restored to the previous known-good state.\n`; + body += `- Fix the template and push a new commit — CI will redeploy automatically.\n`; + } else if (rollbackStatus === 'succeeded' && rollbackAction === 'delete-failed-stack') { + body += `- No resources are provisioned. Safe to iterate on the template and redeploy.\n`; + } else { + body += `- āš ļø Manual cleanup may be required. Inspect the stack with:\n`; + body += ` \`\`\`bash\n az stack sub show --name ${deploymentId} -o table\n \`\`\`\n`; } } const marker = ``; - comment = marker + '\n' + comment; + body = marker + '\n' + body; + + // Find the target PR. On issue_comment we have it; on push we find it from the SHA. + let prNumber = null; + if (context.eventName === 'issue_comment') { + prNumber = context.issue.number; + } else if (context.eventName === 'push') { + const sha = context.sha; + const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: sha, + }); + if (prs.length > 0) prNumber = prs[0].number; + } + + if (!prNumber) { + core.info('No PR associated with this run — skipping PR comment.'); + return; + } + // Post the comment (new comment each run; merged PRs still accept comments) await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, - body: comment, + issue_number: prNumber, + body: body, }); + core.info(`Posted deployment result comment on PR #${prNumber}`); + + // On failure, try to reopen the PR so the team notices. + // Merged PRs cannot be reopened — file a tracking issue instead. + if (status !== 'succeeded') { + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + if (pr.state === 'closed' && !pr.merged) { + try { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'open', + }); + core.info(`Reopened PR #${prNumber} due to deployment failure`); + } catch (e) { + core.warning(`Could not reopen PR #${prNumber}: ${e.message}`); + } + } else if (pr.merged) { + // Cannot reopen a merged PR — open a tracking issue referencing the PR + const issueTitle = `Deployment failed: ${deploymentId} (from PR #${prNumber})`; + const issueBody = `The deployment for \`${deploymentId}\` failed after PR #${prNumber} was merged.\n\n` + + `- **Rollback:** \`${rollbackAction}\` (${rollbackStatus})\n` + + `- **Workflow run:** ${runUrl}\n` + + `- **Merged PR:** #${prNumber}\n\n` + + `See the comment on PR #${prNumber} for full details.`; + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: ['deployment-failed', 'git-ape'], + }); + core.info(`Created tracking issue #${issue.number} for merged-PR deployment failure`); + } + } - name: Notify via Slack if: always() diff --git a/website/docs/workflows/git-ape-destroy.md b/website/docs/workflows/git-ape-destroy.md index 7f8264b..e294830 100644 --- a/website/docs/workflows/git-ape-destroy.md +++ b/website/docs/workflows/git-ape-destroy.md @@ -4,15 +4,15 @@ sidebar_label: "Destroy" description: "GitHub Actions workflow: Git-Ape: Destroy" --- - + # Git-Ape: Destroy -**Workflow file:** `.github/workflows/git-ape-destroy.exampleyml` +**Workflow file:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml` -:::info[Activation required] -This workflow ships as `git-ape-destroy.exampleyml` and is **inert** until renamed to `git-ape-destroy.yml`. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames every `.exampleyml` file in `.github/workflows/` to `.yml` after you complete the experimental-status acknowledgments. +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. ::: ## Triggers @@ -46,7 +46,7 @@ This workflow ships as `git-ape-destroy.exampleyml` and is **inert** until renam | **Runs On** | `ubuntu-latest` | | **Environment** | `azure-destroy` | | **Depends On** | `detect-destroys` | -| **Steps** | 9 | +| **Steps** | 8 | @@ -189,17 +189,24 @@ jobs: exit 1 fi + # Stacks-only: stackId is the single source of truth. If it's missing + # this deployment wasn't created via Deployment Stacks and can't be + # destroyed by this workflow. + STACK_ID=$(jq -r '.stackId // empty' "$STATE_FILE") + STACK_NAME=$(jq -r '.deploymentId // empty' "$STATE_FILE") RG_NAME=$(jq -r '.resourceGroup // empty' "$STATE_FILE") - if [[ -z "$RG_NAME" ]]; then - echo "::error::No resource group found in state file" + if [[ -z "$STACK_ID" && -z "$STACK_NAME" ]]; then + echo "::error::state.json has no stackId or deploymentId — cannot destroy" echo "found=false" >> "$GITHUB_OUTPUT" exit 1 fi echo "found=true" >> "$GITHUB_OUTPUT" + echo "stack_id=$STACK_ID" >> "$GITHUB_OUTPUT" + echo "stack_name=$STACK_NAME" >> "$GITHUB_OUTPUT" echo "resource_group=$RG_NAME" >> "$GITHUB_OUTPUT" - echo "Will destroy resource group: $RG_NAME" + echo "Will destroy deployment stack: $STACK_NAME (${STACK_ID:-by name})" - name: Azure Login (OIDC) if: steps.state.outputs.found == 'true' @@ -207,137 +214,69 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Build destroy plan + - name: Inventory managed resources id: check if: steps.state.outputs.found == 'true' run: | - RG="${{ steps.state.outputs.resource_group }}" - DEPLOYMENT_ID="${{ matrix.deployment_id }}" - - # Check if resource group exists - EXISTS=$(az group exists --name "$RG") - echo "exists=$EXISTS" >> "$GITHUB_OUTPUT" + STACK_NAME="${{ steps.state.outputs.stack_name }}" - if [[ "$EXISTS" != "true" ]]; then - echo "Resource group $RG does not exist (already deleted?)" + # Read live managed-resource list from the stack itself. + # Stacks are idempotent: if the stack is already gone we record that and exit cleanly. + if ! STACK_JSON=$(az stack sub show --name "$STACK_NAME" --output json 2>/dev/null); then + echo "Stack $STACK_NAME not found (already destroyed?)" + echo "exists=false" >> "$GITHUB_OUTPUT" echo "resource_count=0" >> "$GITHUB_OUTPUT" - echo "sub_count=0" >> "$GITHUB_OUTPUT" exit 0 fi - # Inventory RG resources - RESOURCES=$(az resource list --resource-group "$RG" \ - --query "[].{name:name, type:type, id:id, provisioningState:provisioningState}" \ - --output json 2>/dev/null || echo "[]") - RESOURCE_COUNT=$(echo "$RESOURCES" | jq 'length') + echo "exists=true" >> "$GITHUB_OUTPUT" - echo "resource_count=$RESOURCE_COUNT" >> "$GITHUB_OUTPUT" + RESOURCES=$(echo "$STACK_JSON" | jq -c '[(.resources // [])[] | {id: .id, status: .status}]') + COUNT=$(echo "$RESOURCES" | jq 'length') + + echo "resource_count=$COUNT" >> "$GITHUB_OUTPUT" echo "resources<> "$GITHUB_OUTPUT" echo "$RESOURCES" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" - echo "Resource group $RG has $RESOURCE_COUNT resources" - echo "$RESOURCES" | jq -r '.[] | " - \(.type)/\(.name) (\(.provisioningState))"' - - # Query deployment operations to find subscription-scoped resources - # These are NOT deleted by az group delete (e.g. role assignments, policy assignments) - SUB_RESOURCES="[]" - - OPS=$(az deployment operation sub list \ - --name "$DEPLOYMENT_ID" \ - --query "[?properties.provisioningState=='Succeeded' && properties.targetResource.id != null].properties.targetResource" \ - -o json 2>/dev/null || echo "[]") - - if [[ "$OPS" != "[]" ]]; then - # Find subscription-scoped authorization/policy resources (role assignments, etc.) - # These live outside the RG and survive az group delete - SUB_RESOURCES=$(echo "$OPS" | jq -c '[ - .[] | select( - (.resourceType // "" | test("Microsoft.Authorization|Microsoft.Policy")) and - (.id // "" | test("/resourceGroups/") | not) - ) - ]') - - # Check nested deployments for RG-scoped role assignments too - NESTED_NAMES=$(echo "$OPS" | jq -r '[ - .[] | select(.resourceType == "Microsoft.Resources/deployments") - ] | .[].resourceName // empty') - - for NESTED_NAME in $NESTED_NAMES; do - NESTED_OPS=$(az deployment operation group list \ - --resource-group "$RG" --name "$NESTED_NAME" \ - --query "[?properties.provisioningState=='Succeeded' && properties.targetResource.id != null].properties.targetResource" \ - -o json 2>/dev/null || echo "[]") - - # Role assignments scoped to resources within the RG - NESTED_AUTH=$(echo "$NESTED_OPS" | jq -c '[ - .[] | select( - (.resourceType // "" | test("Microsoft.Authorization")) - ) - ]') - - SUB_RESOURCES=$(jq -n --argjson a "$SUB_RESOURCES" --argjson b "$NESTED_AUTH" '$a + $b') - done - fi - - SUB_COUNT=$(echo "$SUB_RESOURCES" | jq 'length') - - echo "sub_count=$SUB_COUNT" >> "$GITHUB_OUTPUT" - echo "sub_resources<> "$GITHUB_OUTPUT" - echo "$SUB_RESOURCES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - echo "" echo "=== Destroy Plan ===" - echo "Resource group: $RG ($RESOURCE_COUNT resources)" - echo "Subscription-scoped resources: $SUB_COUNT" - if [[ "$SUB_COUNT" -gt 0 ]]; then - echo "$SUB_RESOURCES" | jq -r '.[] | " - \(.resourceType): \(.resourceName) (\(.id))"' - fi + echo "Stack: $STACK_NAME" + echo "Managed resources: $COUNT" + echo "$RESOURCES" | jq -r '.[] | " - \(.id) [\(.status)]"' echo "===================" - - name: Delete subscription-scoped resources - id: destroy_sub - if: steps.check.outputs.exists == 'true' && steps.check.outputs.sub_count != '0' - run: | - echo "šŸ—‘ļø Deleting subscription-scoped resources first..." - FAILED=0 - - echo '${{ steps.check.outputs.sub_resources }}' | jq -r '.[].id' | while read -r RESOURCE_ID; do - echo " Deleting: $RESOURCE_ID" - if ! az resource delete --ids "$RESOURCE_ID" 2>&1; then - echo "::warning::Failed to delete $RESOURCE_ID" - FAILED=$((FAILED + 1)) - fi - done - - if [[ "$FAILED" -gt 0 ]]; then - echo "::warning::$FAILED subscription-scoped resource(s) failed to delete" - fi - - - name: Delete resource group + - name: Delete deployment stack id: destroy if: steps.check.outputs.exists == 'true' run: | - RG="${{ steps.state.outputs.resource_group }}" - echo "šŸ—‘ļø Deleting resource group: $RG" - echo "This will block until the resource group is fully deleted..." + STACK_NAME="${{ steps.state.outputs.stack_name }}" + echo "šŸ—‘ļø Deleting deployment stack: $STACK_NAME" + echo " --action-on-unmanage deleteAll — removes every resource (across RGs / sub scope) the stack manages" + echo " This will block until all managed resources are fully deleted..." START_TIME=$(date +%s) - az group delete --name "$RG" --yes 2>&1 || { + # --bypass-stack-out-of-sync-error: a destroyed run is one-shot; we + # don't need the safety check that protects against stale manifests + # during iterative updates. + if ! az stack sub delete \ + --name "$STACK_NAME" \ + --action-on-unmanage deleteAll \ + --bypass-stack-out-of-sync-error true \ + --yes 2>&1; then echo "destroy_status=failed" >> "$GITHUB_OUTPUT" - echo "::error::Failed to delete resource group $RG" + echo "::error::Failed to delete deployment stack $STACK_NAME" exit 1 - } + fi END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo "destroy_status=succeeded" >> "$GITHUB_OUTPUT" echo "destroy_duration=${DURATION}s" >> "$GITHUB_OUTPUT" - echo "āœ… Resource group deleted in ${DURATION}s: $RG" + echo "āœ… Stack deleted in ${DURATION}s: $STACK_NAME" - name: Update deployment state if: always() && steps.state.outputs.found == 'true' @@ -380,11 +319,11 @@ jobs: if: always() run: | DEPLOY_ID="${{ matrix.deployment_id }}" + STACK="${{ steps.state.outputs.stack_name }}" RG="${{ steps.state.outputs.resource_group }}" STATUS="${{ steps.destroy.outputs.destroy_status }}" DURATION="${{ steps.destroy.outputs.destroy_duration }}" RESOURCE_COUNT="${{ steps.check.outputs.resource_count }}" - SUB_COUNT="${{ steps.check.outputs.sub_count }}" EXISTS="${{ steps.check.outputs.exists }}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" @@ -392,11 +331,12 @@ jobs: echo "Git-Ape Destroy Summary" echo "============================================" echo "Deployment: $DEPLOY_ID" + echo "Stack: $STACK" echo "Resource Group: $RG" if [[ "$EXISTS" == "false" ]]; then - echo "Result: Already destroyed" + echo "Result: Already destroyed (stack not found)" elif [[ "$STATUS" == "succeeded" ]]; then - echo "Result: āœ… Destroyed ($RESOURCE_COUNT RG resources + $SUB_COUNT subscription-scoped)" + echo "Result: āœ… Destroyed ($RESOURCE_COUNT managed resources)" echo "Duration: $DURATION" else echo "Result: āŒ Failed" @@ -413,16 +353,16 @@ jobs: if [[ -z "$SLACK_WEBHOOK_URL" ]]; then exit 0; fi DEPLOY_ID="${{ matrix.deployment_id }}" - RG="${{ steps.state.outputs.resource_group }}" + STACK="${{ steps.state.outputs.stack_name }}" STATUS="${{ steps.destroy.outputs.destroy_status }}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" if [[ "$STATUS" == "succeeded" ]]; then EMOJI="šŸ—‘ļø" - MSG="Resource group *$RG* ($DEPLOY_ID) destroyed" + MSG="Deployment stack *$STACK* ($DEPLOY_ID) destroyed" else EMOJI="āŒ" - MSG="Destroy failed for *$RG* ($DEPLOY_ID)" + MSG="Destroy failed for stack *$STACK* ($DEPLOY_ID)" fi curl -sf -X POST "$SLACK_WEBHOOK_URL" \ diff --git a/website/docs/workflows/git-ape-drift-lock.md b/website/docs/workflows/git-ape-drift-lock.md new file mode 100644 index 0000000..a8be627 --- /dev/null +++ b/website/docs/workflows/git-ape-drift-lock.md @@ -0,0 +1,1600 @@ +--- +title: "Continuous Drift Remediation" +sidebar_label: "Continuous Drift Remediation" +description: "GitHub Actions workflow: Continuous Drift Remediation" +--- + + + + +# Continuous Drift Remediation + +**Workflow file:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml` + +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. +::: + +## Triggers + +- **`schedule`** +- **`workflow_dispatch`** + + +## Permissions + +_Inherited from repository defaults_ + +## Jobs + +### `activation` + +| Property | Value | +|----------|-------| +| **Display Name** | activation | +| **Runs On** | `ubuntu-slim` | +| **Steps** | 13 | + +### `agent` + +| Property | Value | +|----------|-------| +| **Display Name** | agent | +| **Runs On** | `ubuntu-latest` | +| **Depends On** | `activation` | +| **Steps** | 46 | + +### `conclusion` + +| Property | Value | +|----------|-------| +| **Display Name** | conclusion | +| **Runs On** | `ubuntu-slim` | +| **Depends On** | `activation`, `agent`, `detection`, `safe_outputs`, `update_cache_memory` | +| **Steps** | 8 | + +### `detection` + +| Property | Value | +|----------|-------| +| **Display Name** | detection | +| **Runs On** | `ubuntu-latest` | +| **Depends On** | `activation`, `agent` | +| **Steps** | 17 | + +### `safe_outputs` + +| Property | Value | +|----------|-------| +| **Display Name** | safe_outputs | +| **Runs On** | `ubuntu-slim` | +| **Depends On** | `activation`, `agent`, `detection` | +| **Steps** | 6 | + +### `update_cache_memory` + +| Property | Value | +|----------|-------| +| **Display Name** | update_cache_memory | +| **Runs On** | `ubuntu-slim` | +| **Depends On** | `activation`, `agent`, `detection` | +| **Steps** | 4 | + + + +## Source + +
+Click to view full workflow YAML + +```yaml +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e69fef083f92a9ea5e1d997c17da168d0c628110b25472f88c235251e6f56309","compiler_version":"v0.76.1","agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["AZURE_CLIENT_ID","AZURE_TENANT_ID","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"azure/login","sha":"1384c340ab2dda50fed2bee3041d1d87018aa5e8","version":"v2"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Continuous drift remediation workflow for Git-Ape deployments. Runs daily +# to detect configuration drift between Azure resources and stored deployment +# state, classifies changes by severity, and creates PRs for human review. +# +# Secrets used: +# - AZURE_CLIENT_ID +# - AZURE_TENANT_ID +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 # v2 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Continuous Drift Remediation" +on: + schedule: + - cron: "6 6 * * *" + # Friendly format: daily around 06:00 (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Continuous Drift Remediation" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "false" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "git-ape-drift.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_a9df7e8899c2c448_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_a9df7e8899c2c448_EOF' + + {{#runtime-import .github/workflows/git-ape-drift.md}} + GH_AW_PROMPT_a9df7e8899c2c448_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: ' Drift detection state — tracks last-seen drift per deployment to implement anti-flapping logic' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: gitapedrift + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Azure Login (OIDC) + uses: azure/login@1384c340ab2dda50fed2bee3041d1d87018aa5e8 # v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + - name: Snapshot current Azure state for all tracked deployments + run: "set -euo pipefail\nmkdir -p /tmp/drift-snapshots\nfor dir in .azure/deployments/*/; do\n [ -f \"$dir/state.json\" ] || continue\n [ -f \"$dir/metadata.json\" ] || continue\n id=$(basename \"$dir\")\n meta_status=$(jq -r '.status // \"\"' \"$dir/metadata.json\")\n # Skip only explicitly destroyed / destroy-requested — everything\n # else (succeeded, failed, partial) may have live resources we must\n # track for drift. Deploy failures frequently leave partial infra\n # behind, and users legitimately modify those resources manually.\n if [ \"$meta_status\" = \"destroyed\" ] || [ \"$meta_status\" = \"destroy-requested\" ]; then\n echo \"{\\\"status\\\":\\\"skipped\\\",\\\"reason\\\":\\\"metadata.status=$meta_status\\\"}\" > \"/tmp/drift-snapshots/${id}.status.json\"\n continue\n fi\n # Resolve resource group: prefer state.json, fall back to metadata.json\n # (state.json .resourceGroup is empty for failed deploys and some\n # succeeded-but-buggy deploys like helloworld-containerapp-dev).\n rg=$(jq -r '.resourceGroup // empty' \"$dir/state.json\")\n if [ -z \"$rg\" ]; then\n rg=$(jq -r '.resourceGroup // empty' \"$dir/metadata.json\")\n fi\n if [ -z \"$rg\" ]; then\n echo \"{\\\"status\\\":\\\"no-resource-group\\\",\\\"reason\\\":\\\"neither state.json nor metadata.json has a resourceGroup\\\"}\" > \"/tmp/drift-snapshots/${id}.status.json\"\n continue\n fi\n snapshot=\"/tmp/drift-snapshots/${id}.json\"\n details_file=\"/tmp/drift-snapshots/${id}.details.json\"\n dns_file=\"/tmp/drift-snapshots/${id}.dns.json\"\n status_file=\"/tmp/drift-snapshots/${id}.status.json\"\n err_file=\"/tmp/drift-snapshots/${id}.err\"\n echo \"Snapshotting $id (rg=$rg, meta_status=$meta_status)\"\n # Redirect stdout → snapshot, stderr → err_file, so we can\n # distinguish \"RG missing in Azure\" from \"RG exists but is empty\"\n # AND preserve the full az error message verbatim.\n if az resource list --resource-group \"$rg\" -o json > \"$snapshot\" 2> \"$err_file\"; then\n count=$(jq 'length' \"$snapshot\")\n echo \"{\\\"status\\\":\\\"ok\\\",\\\"rg\\\":\\\"$rg\\\",\\\"resourceCount\\\":$count,\\\"metaStatus\\\":\\\"$meta_status\\\"}\" > \"$status_file\"\n # Property-level snapshot: fetch full bodies for every resource so\n # the agent can detect drift inside properties (SKUs, tags,\n # firewall rules, TLS versions, etc.), not just existence.\n echo \"[]\" > \"$details_file\"\n if [ \"$count\" -gt 0 ]; then\n jq -r '.[] | .id' \"$snapshot\" | while read -r rid; do\n [ -z \"$rid\" ] && continue\n az resource show --ids \"$rid\" -o json 2>/dev/null || true\n done | jq -s '.' > \"$details_file.tmp\" 2>/dev/null && mv \"$details_file.tmp\" \"$details_file\" || echo \"[]\" > \"$details_file\"\n fi\n # Private DNS zones: record sets are sub-resources not returned by\n # `az resource list`, so query them explicitly per zone.\n echo \"{}\" > \"$dns_file\"\n jq -r '.[] | select(.type==\"Microsoft.Network/privateDnsZones\") | .name' \"$snapshot\" | while read -r zone; do\n [ -z \"$zone\" ] && continue\n records=$(az network private-dns record-set list -g \"$rg\" -z \"$zone\" -o json 2>/dev/null || echo \"[]\")\n jq --arg z \"$zone\" --argjson r \"$records\" '. + {($z): $r}' \"$dns_file\" > \"$dns_file.tmp\" && mv \"$dns_file.tmp\" \"$dns_file\"\n done\n else\n err=$(cat \"$err_file\")\n err_escaped=$(printf '%s' \"$err\" | head -c 1000 | jq -Rs .)\n echo \"[]\" > \"$snapshot\"\n echo \"[]\" > \"$details_file\"\n echo \"{}\" > \"$dns_file\"\n if printf '%s' \"$err\" | grep -q 'ResourceGroupNotFound'; then\n code=\"rg-not-found\"\n elif printf '%s' \"$err\" | grep -qi 'AuthenticationFailed\\|AuthorizationFailed'; then\n code=\"auth-failed\"\n else\n code=\"az-error\"\n fi\n echo \"{\\\"status\\\":\\\"$code\\\",\\\"rg\\\":\\\"$rg\\\",\\\"metaStatus\\\":\\\"$meta_status\\\",\\\"error\\\":$err_escaped}\" > \"$status_file\"\n fi\n rm -f \"$err_file\"\ndone\n# Move snapshots into workspace so the agent can read them\nmkdir -p .drift-snapshots\ncp /tmp/drift-snapshots/*.json .drift-snapshots/ 2>/dev/null || true\nls -la .drift-snapshots/ || true\n\n# Build an inventory markdown the agent is guaranteed to read first,\n# so it cannot claim the snapshots are missing.\n{\n echo \"# Drift Snapshot Inventory\"\n echo \"\"\n echo \"**Pre-step ran at:** $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n echo \"**Workspace:** $GITHUB_WORKSPACE\"\n echo \"**Snapshot directory:** .drift-snapshots/ (repo-root-relative)\"\n echo \"\"\n echo \"## Files produced\"\n echo \"\"\n (cd .drift-snapshots && ls -la) | sed 's/^/ /'\n echo \"\"\n echo \"## Per-deployment status\"\n echo \"\"\n for f in .drift-snapshots/*.status.json; do\n [ -f \"$f\" ] || continue\n did=$(basename \"$f\" .status.json)\n st=$(jq -r '.status' \"$f\")\n rg=$(jq -r '.rg // .reason // \"\"' \"$f\")\n rc=$(jq -r '.resourceCount // \"n/a\"' \"$f\")\n err=$(jq -r '.error // \"\"' \"$f\" | head -c 300)\n echo \"### \\`$did\\`\"\n echo \"\"\n echo \"- **Status:** \\`$st\\`\"\n echo \"- **Resource group / reason:** \\`$rg\\`\"\n echo \"- **Live resource count:** \\`$rc\\`\"\n if [ -n \"$err\" ] && [ \"$err\" != \"null\" ]; then\n echo \"- **Captured error:**\"\n echo \"\"\n echo \"\\`\\`\\`\"\n echo \"$err\"\n echo \"\\`\\`\\`\"\n fi\n # Companion file sizes (helps agent know details.json and dns.json exist)\n for ext in json details.json dns.json; do\n fp=\".drift-snapshots/${did}.${ext}\"\n if [ -f \"$fp\" ]; then\n sz=$(wc -c < \"$fp\" | tr -d ' ')\n echo \"- \\`$fp\\`: ${sz} bytes\"\n fi\n done\n echo \"\"\n done\n} > .drift-snapshots/INVENTORY.md\necho \"---- INVENTORY.md ----\"\ncat .drift-snapshots/INVENTORY.md\n" + shell: bash + + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f0c1474fe8a872a2_EOF' + {"create_issue":{"close_older_issues":true,"labels":["drift-status"],"max":1,"title_prefix":"[drift-status] "},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_f0c1474fe8a872a2_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[drift-status] \". Labels [\"drift-status\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "fields": { + "type": "array" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_cea132e2569a1a51_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_cea132e2569a1a51_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + env: + GH_AW_CLEAN_AZURE: "true" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + bash "${RUNNER_TEMP}/gh-aw/actions/clean_known_action_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(diff) + # --allow-tool shell(echo) + # --allow-tool shell(find) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(printf) + # --allow-tool shell(pwd) + # --allow-tool shell(safeoutputs:*) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(diff)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'AZURE_CLIENT_ID,AZURE_TENANT_ID,COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + SECRET_AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,portal.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_GITHUB_REFS: "" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + include-hidden-files: true + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-git-ape-drift" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "git-ape-drift" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + GH_AW_CACHE_MEMORY_ENABLED: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Continuous Drift Remediation" + WORKFLOW_DESCRIPTION: "Continuous drift remediation workflow for Git-Ape deployments. Runs daily\nto detect configuration drift between Azure resources and stored deployment\nstate, classifies changes by severity, and creates PRs for human review." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/git-ape-drift" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "git-ape-drift" + GH_AW_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/git-ape-drift.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,portal.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"drift-status\"],\"max\":1,\"title_prefix\":\"[drift-status] \"},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: always() && needs.detection.result == 'success' && needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: gitapedrift + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Continuous Drift Remediation" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/git-ape-drift.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + + +``` + +
diff --git a/website/docs/workflows/git-ape-onboarding-template-check.md b/website/docs/workflows/git-ape-onboarding-template-check.md new file mode 100644 index 0000000..7337723 --- /dev/null +++ b/website/docs/workflows/git-ape-onboarding-template-check.md @@ -0,0 +1,138 @@ +--- +title: "Git-Ape: Onboarding Template Check" +sidebar_label: "Onboarding Template Check" +description: "GitHub Actions workflow: Git-Ape: Onboarding Template Check" +--- + + + + +# Git-Ape: Onboarding Template Check + +**Workflow file:** `.github/workflows/git-ape-onboarding-template-check.yml` + +## Triggers + +- **`pull_request`** — paths: `.github/skills/git-ape-onboarding/templates/**, .github/skills/git-ape-onboarding/scripts/sync-templates.sh, .github/skills/git-ape-onboarding/scripts/sync-templates.ps1...` +- **`workflow_dispatch`** + + +## Permissions + +- `contents: read` + +## Jobs + +### `check-sync-bash` + +| Property | Value | +|----------|-------| +| **Display Name** | Verify onboarding templates ↔ mirrors are in sync (bash) | +| **Runs On** | `ubuntu-latest` | +| **Steps** | 2 | + +### `check-sync-pwsh` + +| Property | Value | +|----------|-------| +| **Display Name** | Verify onboarding templates ↔ mirrors are in sync (pwsh on Windows) | +| **Runs On** | `windows-latest` | +| **Steps** | 2 | + +### `scaffold-parity-smoke` + +| Property | Value | +|----------|-------| +| **Display Name** | Scaffold parity smoke (bash sandbox vs pwsh sandbox produces identical files) | +| **Runs On** | `ubuntu-latest` | +| **Steps** | 2 | + + + +## Source + +
+Click to view full workflow YAML + +```yaml +name: "Git-Ape: Onboarding Template Check" + +# Fails any PR that edits either the canonical onboarding templates or the +# .github/copilot-instructions.md mirror without keeping the two in sync. +# +# Note: The workflow templates under templates/workflows/ are NOT mirrored +# into this repository's .github/workflows/. They are scaffolded only into a +# USER's repository by scaffold-repo.{sh,ps1} during onboarding, so there is +# nothing to sync-check for them here. The scaffold-parity-smoke job still +# validates that the bash and pwsh scaffolders produce byte-identical output. +# +# Fix sync drift with: +# .github/skills/git-ape-onboarding/scripts/sync-templates.sh apply +# pwsh .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 apply +# and commit the updated mirror alongside the template change. +# +# Three jobs run on every matching PR: +# 1. check-sync-bash — Ubuntu, runs the .sh sync check. +# 2. check-sync-pwsh — Windows, runs the .ps1 sync check. +# 3. scaffold-parity-smoke — Ubuntu, scaffolds via both runtimes into +# separate sandboxes and fails on any byte-level divergence. + +on: + pull_request: + paths: + - '.github/skills/git-ape-onboarding/templates/**' + - '.github/skills/git-ape-onboarding/scripts/sync-templates.sh' + - '.github/skills/git-ape-onboarding/scripts/sync-templates.ps1' + - '.github/skills/git-ape-onboarding/scripts/scaffold-repo.sh' + - '.github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1' + - '.github/copilot-instructions.md' + - '.github/workflows/git-ape-onboarding-template-check.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-sync-bash: + name: Verify onboarding templates ↔ mirrors are in sync (bash) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Run sync check + run: | + chmod +x .github/skills/git-ape-onboarding/scripts/sync-templates.sh + .github/skills/git-ape-onboarding/scripts/sync-templates.sh check + + check-sync-pwsh: + name: Verify onboarding templates ↔ mirrors are in sync (pwsh on Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Run sync check + shell: pwsh + run: | + pwsh -NoProfile -File .github/skills/git-ape-onboarding/scripts/sync-templates.ps1 check + + scaffold-parity-smoke: + name: Scaffold parity smoke (bash sandbox vs pwsh sandbox produces identical files) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Scaffold via bash and pwsh into separate sandboxes, then diff + run: | + set -euo pipefail + SANDBOX_SH="$(mktemp -d)" + SANDBOX_PS="$(mktemp -d)" + chmod +x .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh + bash .github/skills/git-ape-onboarding/scripts/scaffold-repo.sh "$SANDBOX_SH" > /dev/null + pwsh -NoProfile -File .github/skills/git-ape-onboarding/scripts/scaffold-repo.ps1 "$SANDBOX_PS" > /dev/null + # Recursive diff: fails the job if the two scaffolds differ by a single byte. + diff -r "$SANDBOX_SH" "$SANDBOX_PS" + echo "scaffold-repo.sh and scaffold-repo.ps1 produce byte-identical output." + +``` + +
diff --git a/website/docs/workflows/git-ape-plan.md b/website/docs/workflows/git-ape-plan.md index 53d5728..bf61584 100644 --- a/website/docs/workflows/git-ape-plan.md +++ b/website/docs/workflows/git-ape-plan.md @@ -4,15 +4,15 @@ sidebar_label: "Plan" description: "GitHub Actions workflow: Git-Ape: Plan" --- - + # Git-Ape: Plan -**Workflow file:** `.github/workflows/git-ape-plan.exampleyml` +**Workflow file:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml` -:::info[Activation required] -This workflow ships as `git-ape-plan.exampleyml` and is **inert** until renamed to `git-ape-plan.yml`. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames every `.exampleyml` file in `.github/workflows/` to `.yml` after you complete the experimental-status acknowledgments. +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. ::: ## Triggers @@ -45,7 +45,7 @@ This workflow ships as `git-ape-plan.exampleyml` and is **inert** until renamed | **Display Name** | Plan Local: ${{ matrix.deployment_id }} | | **Runs On** | `ubuntu-latest` | | **Depends On** | `detect-deployments` | -| **Steps** | 10 | +| **Steps** | 12 | ### `plan-azure` @@ -54,7 +54,7 @@ This workflow ships as `git-ape-plan.exampleyml` and is **inert** until renamed | **Display Name** | Plan Azure: ${{ matrix.deployment_id }} | | **Runs On** | `ubuntu-latest` | | **Depends On** | `detect-deployments` | -| **Steps** | 7 | +| **Steps** | 8 | ### `plan-comment` @@ -274,14 +274,38 @@ jobs: echo "has_architecture=false" >> "$GITHUB_OUTPUT" fi + - name: Stage template for security scan + id: scan_stage + run: | + # WORKAROUND: Microsoft Defender for DevOps' templateanalyzer tool always + # runs `analyze-directory $GITHUB_WORKSPACE` and ignores GDN_TEMPLATEANALYZER_INPUT. + # Template Analyzer's file walker uses .NET EnumerationOptions which default to + # AttributesToSkip=Hidden|System. On Linux, .NET treats any path starting with + # "." as Hidden — so .azure/deployments//template.json is silently skipped + # and the scanner reports "Analyzed 0 files in the directory specified." + # See: https://github.com/Azure/template-analyzer/blob/main/src/Analyzer.Utilities/TemplateDiscovery.cs + # + # Workaround: copy the template + parameters to a non-dotted directory at the + # workspace root so the walker discovers them. + STAGE_DIR="templateanalyzer-scan/${{ matrix.deployment_id }}" + mkdir -p "$STAGE_DIR" + cp "${{ steps.params.outputs.deploy_dir }}/template.json" "$STAGE_DIR/template.json" + if [[ -f "${{ steps.params.outputs.deploy_dir }}/parameters.json" ]]; then + cp "${{ steps.params.outputs.deploy_dir }}/parameters.json" "$STAGE_DIR/template.parameters.json" + fi + echo "stage_dir=$STAGE_DIR" >> "$GITHUB_OUTPUT" + ls -la "$STAGE_DIR" + - name: Run Microsoft Defender for DevOps template analyzer id: security_scan continue-on-error: true uses: microsoft/security-devops-action@v1 with: tools: templateanalyzer - env: - GDN_TEMPLATEANALYZER_INPUT: ${{ steps.params.outputs.deploy_dir }}/template.json + + - name: Cleanup staged template + if: always() + run: rm -rf templateanalyzer-scan - name: Upload SARIF results (non-blocking) id: sarif_upload @@ -426,18 +450,23 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Validate template + - name: Validate template (stack) id: validate if: steps.azure_login.outcome == 'success' run: | - echo "### Validating ARM template..." + echo "### Validating deployment stack..." - RESULT=$(az deployment sub validate \ + # az stack sub validate mirrors az deployment sub validate but also + # verifies stack-specific settings (action-on-unmanage, deny settings). + RESULT=$(az stack sub validate \ + --name "${{ matrix.deployment_id }}" \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ + --action-on-unmanage deleteAll \ + --deny-settings-mode none \ --output json 2>&1) || true # Guard against non-JSON output (e.g. auth/CLI errors) — jq exits non-zero @@ -461,14 +490,31 @@ jobs: - name: Run what-if analysis id: whatif - if: steps.validate.outputs.validation_status == 'passed' + if: steps.azure_login.outcome == 'success' run: | + # NOTE: Deployment Stacks don't yet support what-if + # (see https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-stacks#known-issues). + # We fall back to `az deployment sub what-if` against the underlying + # ARM template — this accurately previews resource changes even though + # it doesn't model the stack wrapper itself. + # + # Run unconditionally on login success: validation and what-if catch + # different classes of issues (schema vs. preflight/runtime), so even + # if validation failed, what-if may surface additional context. + set +e WHATIF_OUTPUT=$(az deployment sub what-if \ --location "${{ steps.params.outputs.location }}" \ --template-file "${{ steps.params.outputs.deploy_dir }}/template.json" \ --parameters @"${{ steps.params.outputs.deploy_dir }}/parameters.json" \ - --no-prompt 2>&1) || true + --no-prompt 2>&1) + WHATIF_EXIT=$? + set -e + if [[ $WHATIF_EXIT -eq 0 ]]; then + echo "whatif_status=passed" >> "$GITHUB_OUTPUT" + else + echo "whatif_status=failed" >> "$GITHUB_OUTPUT" + fi echo "whatif_result<> "$GITHUB_OUTPUT" echo "$WHATIF_OUTPUT" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" @@ -480,6 +526,7 @@ jobs: AZURE_LOGIN_OUTCOME: ${{ steps.azure_login.outcome }} VALIDATION_STATUS: ${{ steps.validate.outputs.validation_status }} VALIDATION_ERROR: ${{ steps.validate.outputs.validation_error }} + WHATIF_STATUS: ${{ steps.whatif.outputs.whatif_status }} WHATIF_RESULT: ${{ steps.whatif.outputs.whatif_result }} run: | mkdir -p .git-ape-plan @@ -492,17 +539,28 @@ jobs: fi fi + FINAL_WHATIF_STATUS="$WHATIF_STATUS" + if [[ -z "$FINAL_WHATIF_STATUS" ]]; then + if [[ "$AZURE_LOGIN_OUTCOME" == "failure" ]]; then + FINAL_WHATIF_STATUS="login_failed" + else + FINAL_WHATIF_STATUS="skipped" + fi + fi + jq -n \ --arg deploymentId "$DEPLOYMENT_ID" \ --arg azureLoginOutcome "$AZURE_LOGIN_OUTCOME" \ --arg validationStatus "$FINAL_VALIDATION_STATUS" \ --arg validationError "$VALIDATION_ERROR" \ + --arg whatifStatus "$FINAL_WHATIF_STATUS" \ --arg whatifResult "$WHATIF_RESULT" \ '{ deploymentId: $deploymentId, azureLoginOutcome: $azureLoginOutcome, validationStatus: $validationStatus, validationError: $validationError, + whatifStatus: $whatifStatus, whatifResult: $whatifResult }' > ".git-ape-plan/plan-azure-${DEPLOYMENT_ID}.json" @@ -515,6 +573,17 @@ jobs: if-no-files-found: error retention-days: 1 + - name: What-if gate + # Runs AFTER the artifact upload so the Plan Comment job still posts the + # full plan (validation + scan + what-if details) to the PR. This step + # only fails the Plan Azure job to block merge/deploy when what-if + # cannot produce a valid deployment preview — a failed what-if means the + # template wouldn't deploy successfully even if everything else is green. + if: steps.whatif.outputs.whatif_status == 'failed' + run: | + echo "::error::What-if analysis failed — deployment would not succeed. See the PR comment for the full output." + exit 1 + plan-comment: name: "Plan Comment: ${{ matrix.deployment_id }}" needs: [detect-deployments, plan-local, plan-azure] @@ -541,7 +610,7 @@ jobs: path: .git-ape-plan/azure - name: Post plan as PR comment - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); @@ -555,11 +624,55 @@ jobs: return JSON.parse(fs.readFileSync(path, 'utf8')); } + // Fetch templateanalyzer Code Scanning alerts for THIS PR so we can render + // each finding as a clickable link to its alert page (Security tab) and to + // the rule documentation. Falls back gracefully if the API call fails or + // returns no alerts (e.g. SARIF upload was skipped or still processing). + async function fetchTemplateAnalyzerAlerts() { + try { + const ref = `refs/pull/${context.issue.number}/merge`; + const { data: alerts } = await github.rest.codeScanning.listAlertsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + ref, + tool_name: 'templateanalyzer', + per_page: 100, + }); + return alerts; + } catch (err) { + core.warning(`Could not fetch templateanalyzer alerts: ${err.message}`); + return []; + } + } + + function renderAlertsTable(alerts) { + if (!alerts || alerts.length === 0) return ''; + const sevIcon = (s) => ({ error: 'šŸ”“', warning: '🟔', note: 'šŸ”µ', none: '⚪' }[s] || '⚪'); + let table = '| Sev | Rule | Line | Description |\n'; + table += '|---|---|---|---|\n'; + for (const a of alerts) { + const sev = a.rule?.severity || 'none'; + const ruleId = a.rule?.id || '?'; + const ruleName = a.rule?.name || ''; + const helpUri = a.rule?.help_uri || ''; + const ruleLabel = helpUri + ? `[\`${ruleId}\`](${helpUri}) ${ruleName}` + : `\`${ruleId}\` ${ruleName}`; + const line = a.most_recent_instance?.location?.start_line || '?'; + const desc = (a.rule?.description || '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); + const alertLink = `[#${a.number}](${a.html_url})`; + table += `| ${sevIcon(sev)} | ${alertLink} Ā· ${ruleLabel} | ${line} | ${desc} |\n`; + } + return table; + } + + const alerts = await fetchTemplateAnalyzerAlerts(); const local = loadSummary('local') || {}; const azure = loadSummary('azure') || {}; const validationStatus = azure.validationStatus || 'skipped'; const validationError = azure.validationError || ''; + const whatifStatus = azure.whatifStatus || 'skipped'; const whatifResult = azure.whatifResult || ''; const azureLoginOutcome = azure.azureLoginOutcome || ''; const scanStatus = local.scanStatus || 'skipped'; @@ -599,27 +712,40 @@ jobs: comment += `${tagDetails}\n\n`; } - if (validationStatus === 'passed') { - if (securityScanOutcome === 'failure' && scanStatus === 'skipped') { - comment += `### āš ļø Security Scan: Tool Execution Failed\n\n`; - } else if (scanStatus === 'passed') { - comment += `### āœ… Security Scan: Passed`; - if (parseInt(scanWarnings) > 0 || parseInt(scanNotes) > 0) { - comment += ` (${scanWarnings} warning(s), ${scanNotes} note(s))`; - } - comment += `\n\n`; - } else if (scanStatus === 'failed') { - comment += `### āŒ Security Scan: Failed (${scanErrors} error(s), ${scanWarnings} warning(s))\n\n`; - } else { - comment += `### āš ļø Security Scan: Skipped\n\n`; + // Security scan runs locally on the template file and is independent of + // Azure validation. Always render the section so reviewers see the result + // even when validation fails. + if (securityScanOutcome === 'failure' && scanStatus === 'skipped') { + comment += `### āš ļø Security Scan: Tool Execution Failed\n\n`; + } else if (scanStatus === 'passed') { + comment += `### āœ… Security Scan: Passed`; + if (parseInt(scanWarnings) > 0 || parseInt(scanNotes) > 0) { + comment += ` (${scanWarnings} warning(s), ${scanNotes} note(s))`; } + comment += `\n\n`; + } else if (scanStatus === 'failed') { + comment += `### āŒ Security Scan: Failed (${scanErrors} error(s), ${scanWarnings} warning(s))\n\n`; + } else { + comment += `### āš ļø Security Scan: Skipped\n\n`; + } - if (scanFindings) { - comment += `
\nSecurity findings\n\n${scanFindings}\n\n
\n\n`; - } - if (sarifUploadOutcome === 'failure') { - comment += `> SARIF upload to GitHub code scanning failed, but this does not block plan generation.\n\n`; - } + // Prefer the live Code Scanning alerts table (rich links into the Security + // tab + rule docs). Fall back to the inline SARIF text findings when the + // alerts API hasn't surfaced them yet (e.g. SARIF still processing). + const alertsTable = renderAlertsTable(alerts); + const codeScanningFilterUrl = + `https://github.com/${context.repo.owner}/${context.repo.repo}` + + `/security/code-scanning?query=pr%3A${context.issue.number}+tool%3Atemplateanalyzer`; + + if (alertsTable) { + comment += `
\n${alerts.length} finding(s) — Microsoft Defender for DevOps Ā· Template Analyzer\n\n${alertsTable}\n\n
\n\n`; + comment += `> šŸ”— **[View all ${alerts.length} alerts in the Security tab →](${codeScanningFilterUrl})**\n\n`; + } else if (scanFindings) { + comment += `
\nSecurity findings (Microsoft Defender for DevOps Ā· Template Analyzer)\n\n${scanFindings}\n\n
\n\n`; + comment += `> šŸ”— **[View in Security tab →](${codeScanningFilterUrl})** (alerts may take a moment to appear after upload)\n\n`; + } + if (sarifUploadOutcome === 'failure') { + comment += `> SARIF upload to GitHub code scanning failed, but this does not block plan generation.\n\n`; } if (costTotal && validationStatus === 'passed') { @@ -638,6 +764,18 @@ jobs: if (validationStatus === 'passed' && whatifResult) { comment += `### What-If Analysis\n\n`; comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'passed' && whatifResult) { + comment += `### What-If Analysis\n\n`; + comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'failed') { + comment += `### āŒ What-If Analysis: Failed\n\n`; + comment += `\`\`\`\n${whatifResult}\n\`\`\`\n\n`; + } else if (whatifStatus === 'login_failed') { + comment += `### āš ļø What-If Analysis: Skipped\n\n`; + comment += `> Skipped because Azure OIDC login failed.\n\n`; + } else { + comment += `### āš ļø What-If Analysis: Skipped\n\n`; + comment += `> What-if did not run. See validation/login status above.\n\n`; } if (validationStatus === 'passed') { diff --git a/website/docs/workflows/git-ape-release.md b/website/docs/workflows/git-ape-release.md index d16a417..4556c21 100644 --- a/website/docs/workflows/git-ape-release.md +++ b/website/docs/workflows/git-ape-release.md @@ -350,28 +350,19 @@ jobs: fi # VS Code Marketplace rejects semver pre-release suffixes - # (e.g. 0.1.0-rc.1). The official channel model uses odd minors - # for the Pre-Release channel and even minors for Release. - # See: https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions + # (e.g. 0.1.0-rc.1). We publish every release as a stable + # marketplace release regardless of minor parity — the odd/even + # minor convention is opt-in and would require packaging the VSIX + # with --pre-release to match, which we don't do here. if [[ "$VERSION" == *-* ]]; then echo "Version $VERSION carries a semver pre-release suffix, which the" echo "VS Code Marketplace does not accept. Skipping marketplace publish." exit 0 fi - MINOR=$(echo "$VERSION" | cut -d. -f2) - if (( MINOR % 2 == 1 )); then - FLAG="--pre-release" - CHANNEL="Pre-Release" - else - FLAG="" - CHANNEL="Release" - fi - VSIX_FILE=$(ls ./*.vsix) - echo "Publishing $VSIX_FILE to VS Code Marketplace ($CHANNEL channel)" - # shellcheck disable=SC2086 - vsce publish --packagePath "$VSIX_FILE" --no-dependencies $FLAG + 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' diff --git a/website/docs/workflows/git-ape-verify.md b/website/docs/workflows/git-ape-verify.md index 53ac97c..01adfce 100644 --- a/website/docs/workflows/git-ape-verify.md +++ b/website/docs/workflows/git-ape-verify.md @@ -4,15 +4,15 @@ sidebar_label: "Verify Setup" description: "GitHub Actions workflow: Git-Ape: Verify Setup" --- - + # Git-Ape: Verify Setup -**Workflow file:** `.github/workflows/git-ape-verify.exampleyml` +**Workflow file:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml` -:::info[Activation required] -This workflow ships as `git-ape-verify.exampleyml` and is **inert** until renamed to `git-ape-verify.yml`. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames every `.exampleyml` file in `.github/workflows/` to `.yml` after you complete the experimental-status acknowledgments. +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. ::: ## Triggers @@ -81,7 +81,7 @@ jobs: echo "āœ… AZURE_TENANT_ID is set" fi - if [[ -z "${{ secrets.AZURE_SUBSCRIPTION_ID }}" ]]; then + if [[ -z "${{ vars.AZURE_SUBSCRIPTION_ID }}" ]]; then echo "::error::Missing secret: AZURE_SUBSCRIPTION_ID" MISSING=$((MISSING + 1)) else @@ -97,7 +97,7 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - name: Verify Azure access if: steps.secrets.outputs.missing == '0' @@ -162,6 +162,8 @@ jobs: "git-ape-plan.yml:Git-Ape: Plan" "git-ape-deploy.yml:Git-Ape: Deploy" "git-ape-destroy.yml:Git-Ape: Destroy" + "git-ape-drift.yml:Git-Ape: Drift Detection" + "git-ape-ttl-reaper.yml:Git-Ape: TTL Reaper" ) for WF in "${WORKFLOWS[@]}"; do diff --git a/website/docs/workflows/issue-triage-agent-lock.md b/website/docs/workflows/issue-triage-agent-lock.md index b9182a8..7a07734 100644 --- a/website/docs/workflows/issue-triage-agent-lock.md +++ b/website/docs/workflows/issue-triage-agent-lock.md @@ -38,7 +38,7 @@ _Inherited from repository defaults_ | **Display Name** | agent | | **Runs On** | `ubuntu-latest` | | **Depends On** | `activation` | -| **Steps** | 36 | +| **Steps** | 37 | ### `conclusion` @@ -75,8 +75,8 @@ _Inherited from repository defaults_ Click to view full workflow YAML ```yaml -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b87d30f0e6dfd42fbdc898c7bee5db51b0c988a7124831508d49c98c3e999c90","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc56a0cad2f450c562810785ef38649c04db812a","version":"v0.72.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b87d30f0e6dfd42fbdc898c7bee5db51b0c988a7124831508d49c98c3e999c90","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -91,7 +91,7 @@ _Inherited from repository defaults_ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.72.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. # # To update this file, edit github/gh-aw/.github/workflows/issue-triage-agent.md@852cb06ad52958b402ed982b69957ffc57ca0619 and run: # gh aw compile @@ -116,29 +116,29 @@ _Inherited from repository defaults_ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.41 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.41 -# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c -# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 -# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Issue Triage Agent" -"on": +on: schedule: - cron: "0 14 * * 1-5" workflow_dispatch: inputs: aw_context: default: "" - description: Agent caller context (used internally by Agentic Workflows). + description: "Agent caller context (used internally by Agentic Workflows)." required: false type: string @@ -162,37 +162,44 @@ jobs: lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.40" - GH_AW_INFO_AGENT_VERSION: "1.0.40" - GH_AW_INFO_CLI_VERSION: "v0.72.1" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" GH_AW_INFO_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.41" + GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@852cb06ad52958b402ed982b69957ffc57ca0619" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -213,6 +220,7 @@ jobs: sparse-checkout: | .github .agents + .antigravity .claude .codex .crush @@ -223,8 +231,8 @@ jobs: fetch-depth: 1 - name: Save agent config folders for base branch restoration env: - GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file @@ -242,7 +250,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.72.1" + GH_AW_COMPILED_VERSION: "v0.76.1" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -253,11 +261,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -281,28 +289,28 @@ jobs: cat << 'GH_AW_PROMPT_a1c0370ef2dd6d34_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} @@ -332,11 +340,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -352,11 +360,11 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, @@ -387,6 +395,7 @@ jobs: /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/base /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -405,28 +414,35 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: issuetriageagent outputs: - agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} - mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} - model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | @@ -459,14 +475,14 @@ jobs: git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -484,8 +500,12 @@ jobs: GH_AW_SUB_AGENT_DIR: ".github/agents" GH_AW_SUB_AGENT_EXT: ".agent.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -697,8 +717,13 @@ jobs: export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) @@ -707,7 +732,7 @@ jobs: "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -771,25 +796,32 @@ jobs: timeout-minutes: 5 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.72.1 + GH_AW_VERSION: v0.76.1 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -804,11 +836,11 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] XDG_CONFIG_HOME: /home/runner - - name: Detect Copilot errors - id: detect-copilot-errors + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -981,6 +1013,7 @@ jobs: concurrency: group: "gh-aw-conclusion-issue-triage-agent" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -989,15 +1022,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1096,6 +1133,8 @@ jobs: GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} @@ -1108,6 +1147,7 @@ jobs: GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "5" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1132,15 +1172,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1166,7 +1210,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 - name: Check if detection needed id: detection_guard if: always() @@ -1225,11 +1269,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1238,23 +1282,30 @@ jobs: timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.72.1 + GH_AW_VERSION: v0.76.1 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1282,6 +1333,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" with: script: | @@ -1292,10 +1344,11 @@ jobs: await main(); } catch (loadErr) { const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); core.error(msg); core.setOutput('reason', 'parse_error'); - if (continueOnError) { + if (continueOnError && !detectionExecutionFailed) { core.warning('\u26A0\uFE0F ' + msg); core.setOutput('conclusion', 'warning'); core.setOutput('success', 'false'); @@ -1326,7 +1379,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_ENGINE_VERSION: "1.0.52" GH_AW_WORKFLOW_ID: "issue-triage-agent" GH_AW_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@852cb06ad52958b402ed982b69957ffc57ca0619" @@ -1343,15 +1396,19 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@9f050961da586148d135e113d8bb025185cdf2b8 # v0.75.4 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1380,6 +1437,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} diff --git a/website/docs/workflows/overview.md b/website/docs/workflows/overview.md index 393bda1..a224999 100644 --- a/website/docs/workflows/overview.md +++ b/website/docs/workflows/overview.md @@ -12,29 +12,36 @@ description: "Overview of Git-Ape GitHub Actions workflows" Git-Ape provides GitHub Actions workflows for automated deployment lifecycle management. -:::info[Activation required] -Workflows ship as **`*.exampleyml`** files in `.github/workflows/` so they are inert when the plugin is first installed. The [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow renames each `.exampleyml` to `.yml` after you complete the experimental-status acknowledgments. Files still ending in `.exampleyml` in the inventory below are not yet active. +:::info[Workflows ship via the onboarding skill] +The user-facing workflows below are **shipped as templates** under `.github/skills/git-ape-onboarding/templates/workflows/` and **scaffolded into your repository** by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. The scaffold step uses **skip-with-notice on collision** — it never overwrites an existing file. The workflows ship as ready-to-run `.yml` files (no manual rename needed) and do not run inside the git-ape repo itself. ::: -## Workflow Inventory +## User-facing workflows (scaffolded into your repo) + +| Workflow | Template file | Triggers | Jobs | +|----------|---------------|----------|------| +| [Git-Ape: Deploy](./git-ape-deploy) | `.github/skills/git-ape-onboarding/templates/workflows/git-ape-deploy.yml` | push, issue_comment | check-comment-trigger, detect-deployments, deploy | +| [Git-Ape: Destroy](./git-ape-destroy) | `.github/skills/git-ape-onboarding/templates/workflows/git-ape-destroy.yml` | push, workflow_dispatch | detect-destroys, destroy | +| [Continuous Drift Remediation](./git-ape-drift-lock) | `.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml` | schedule, workflow_dispatch | activation, agent, conclusion, detection, safe_outputs, update_cache_memory | +| [Git-Ape: Plan](./git-ape-plan) | `.github/skills/git-ape-onboarding/templates/workflows/git-ape-plan.yml` | pull_request | detect-deployments, plan-local, plan-azure, plan-comment | +| [Git-Ape: Verify Setup](./git-ape-verify) | `.github/skills/git-ape-onboarding/templates/workflows/git-ape-verify.yml` | workflow_dispatch | verify | + +## Repo CI workflows (run inside the git-ape repo) | Workflow | File | Triggers | Jobs | |----------|------|----------|------| -| [Daily Repo Status](./daily-repo-status-lock) | `daily-repo-status.lock.yml` | schedule, workflow_dispatch | activation, agent, conclusion, detection, safe_outputs | -| [Git-Ape: Workflow Lint](./git-ape-actionlint) | `git-ape-actionlint.yml` | pull_request | actionlint | -| [Git-Ape: Extension Build](./git-ape-build) | `git-ape-build.yml` | pull_request | build | -| [Git-Ape: Deploy](./git-ape-deploy) | `git-ape-deploy.exampleyml` | push, issue_comment | check-comment-trigger, detect-deployments, deploy | -| [Git-Ape: Destroy](./git-ape-destroy) | `git-ape-destroy.exampleyml` | push, workflow_dispatch | detect-destroys, destroy | -| [Git-Ape: Docs Check](./git-ape-docs-check) | `git-ape-docs-check.yml` | pull_request | check-docs | -| [Git-Ape: Docs Deploy](./git-ape-docs) | `git-ape-docs.yml` | push | build, deploy | -| [Git-Ape: Plan](./git-ape-plan) | `git-ape-plan.exampleyml` | pull_request | detect-deployments, plan-local, plan-azure, plan-comment | -| [Git-Ape: Plugin Version Check](./git-ape-plugin-version-check) | `git-ape-plugin-version-check.yml` | pull_request | check-version-drift | -| [Git-Ape: Plugin Release](./git-ape-release) | `git-ape-release.yml` | push, workflow_dispatch | release | -| [Git-Ape: Verify Setup](./git-ape-verify) | `git-ape-verify.exampleyml` | workflow_dispatch | verify | -| [Issue Triage Agent](./issue-triage-agent-lock) | `issue-triage-agent.lock.yml` | schedule, workflow_dispatch | activation, agent, conclusion, detection, safe_outputs | -| [PR Validation](./pr-validation) | `pr-validation.yml` | pull_request | structure-check, markdownlint | -| [Waza agent evals](./waza-agent-evals) | `waza-agent-evals.yml` | pull_request, workflow_dispatch | preflight, prepare, tokens, eval, comment | -| [Waza skill evals](./waza-evals) | `waza-evals.yml` | pull_request, workflow_dispatch | preflight, prepare, tokens, eval, comment | +| [Daily Repo Status](./daily-repo-status-lock) | `.github/workflows/daily-repo-status.lock.yml` | schedule, workflow_dispatch | activation, agent, conclusion, detection, safe_outputs | +| [Git-Ape: Workflow Lint](./git-ape-actionlint) | `.github/workflows/git-ape-actionlint.yml` | pull_request | actionlint | +| [Git-Ape: Extension Build](./git-ape-build) | `.github/workflows/git-ape-build.yml` | pull_request | build | +| [Git-Ape: Docs Check](./git-ape-docs-check) | `.github/workflows/git-ape-docs-check.yml` | pull_request | check-docs | +| [Git-Ape: Docs Deploy](./git-ape-docs) | `.github/workflows/git-ape-docs.yml` | push | build, deploy | +| [Git-Ape: Onboarding Template Check](./git-ape-onboarding-template-check) | `.github/workflows/git-ape-onboarding-template-check.yml` | pull_request, workflow_dispatch | check-sync-bash, check-sync-pwsh, scaffold-parity-smoke | +| [Git-Ape: Plugin Version Check](./git-ape-plugin-version-check) | `.github/workflows/git-ape-plugin-version-check.yml` | pull_request | check-version-drift | +| [Git-Ape: Plugin Release](./git-ape-release) | `.github/workflows/git-ape-release.yml` | push, workflow_dispatch | release | +| [Issue Triage Agent](./issue-triage-agent-lock) | `.github/workflows/issue-triage-agent.lock.yml` | schedule, workflow_dispatch | activation, agent, conclusion, detection, safe_outputs | +| [PR Validation](./pr-validation) | `.github/workflows/pr-validation.yml` | pull_request | structure-check, markdownlint | +| [Waza agent evals](./waza-agent-evals) | `.github/workflows/waza-agent-evals.yml` | pull_request, workflow_dispatch | preflight, prepare, tokens, eval, comment | +| [Waza skill evals](./waza-evals) | `.github/workflows/waza-evals.yml` | pull_request, workflow_dispatch | preflight, prepare, tokens, eval, comment | ## Pipeline Architecture From e66e2ecc0fd48820d260d1f1f17008e0b9e718dc Mon Sep 17 00:00:00 2001 From: Arnaud Lheureux Date: Fri, 29 May 2026 16:37:30 +0800 Subject: [PATCH 6/6] docs(website): add companion page for git-ape-drift agentic workflow source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-generated 'Continuous Drift Remediation' page documents the compiled '.lock.yml' shape. This adds the missing hand-curated page documenting the agentic '.md' source — schedule, severity model, anti-flapping rules, safe-outputs configuration, and how to recompile after editing. Ported from the private repo with two small adaptations: - Workflow-file path updated to the template location under .github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md (matches the autogen lock-page convention). - Added the ':::info[Scaffolded by /git-ape-onboarding]' admonition for consistency with the autogen lock page; clarifies the file is shipped as a template, not run in the git-ape repo itself. - Added a Related section linking to the lock-page, the azure-drift-detector skill, the deployment guide, and the use-case overview so readers can navigate the full drift story. Marked HAND-CURATED at the top so generate-docs.js maintainers know not to add a generator branch for '.md' workflow sources. 🌊 - Generated by Copilot --- website/docs/workflows/git-ape-drift.md | 135 ++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 website/docs/workflows/git-ape-drift.md diff --git a/website/docs/workflows/git-ape-drift.md b/website/docs/workflows/git-ape-drift.md new file mode 100644 index 0000000..418c49d --- /dev/null +++ b/website/docs/workflows/git-ape-drift.md @@ -0,0 +1,135 @@ +--- +title: "Git-Ape: Drift (source)" +sidebar_label: "Drift (source)" +description: "Agentic workflow source: Continuous drift detection and remediation" +--- + + + +# Git-Ape: Drift + +**Workflow source:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.md` +**Compiled lock:** `.github/skills/git-ape-onboarding/templates/workflows/git-ape-drift.lock.yml` ([generated page](./git-ape-drift-lock)) + +:::info[Scaffolded by `/git-ape-onboarding`] +This workflow is **shipped as a template** under `.github/skills/git-ape-onboarding/templates/workflows/` and copied into your repository's `.github/workflows/` by the [`/git-ape-onboarding`](/docs/skills/git-ape-onboarding) flow. It does **not** run in the git-ape repo itself. +::: + +:::info Agentic Workflow +This is a [GitHub Agentic Workflow](https://github.github.com/gh-aw/) — an AI-driven workflow authored in Markdown rather than traditional YAML. The AI agent reasons about drift, classifies changes, and creates PRs for human review. +::: + +## Triggers + +- **`schedule`** — Daily around 06:00 UTC +- **`workflow_dispatch`** — Manual trigger + +## Permissions + +- `contents: read` +- `issues: read` +- `pull-requests: read` + +## What It Does + +The drift workflow implements continuous drift remediation as described in the [Platform Engineering for the Agentic AI Era](https://devblogs.microsoft.com/all-things-azure/platform-engineering-for-the-agentic-ai-era/) manifesto. + +### 1. Discovery + +Scans `.azure/deployments/` for all active deployments (`state.json` with `"status": "succeeded"`, excluding destroyed deployments). + +### 2. Detection + +For each active deployment, the agent: +- Reads the stored ARM template (`template.json`) as the desired state +- Queries Azure for current resource configuration via `az resource show` +- Compares properties and identifies differences + +### 3. Classification + +Each drifted property is classified by severity: + +| Severity | Examples | Action | +|----------|----------|--------| +| šŸ”“ **Critical** | HTTPS disabled, firewall removed, auth changes, TLS downgrade | Issue + two PRs | +| 🟔 **Warning** | SKU changes, tag modifications, runtime version changes | Two PRs | +| šŸ”µ **Info** | Description changes, Azure Policy-added tags | Logged only | + +### 4. Anti-Flapping + +To prevent alert fatigue and churn: + +- **Debounce** — No duplicate alerts for the same drift within 24 hours +- **Cooldown** — Skip resources with recently merged remediation PRs +- **Persistence threshold** — Only alert on drift persisting for 2+ consecutive checks + +### 5. Remediation PRs + +For each drifted deployment, the agent creates **two draft PRs**: + +| PR | Purpose | Changes | +|----|---------|---------| +| **Revert** | Restore Azure to match IaC | Contains `az` commands to revert Azure state | +| **Adopt** | Update IaC to match Azure | Updates `template.json` to reflect current Azure config | + +The human reviewer chooses which PR to merge (or closes both if neither is appropriate). + +For Critical drift, a GitHub issue is also created with `priority:critical` and `security` labels. + +## Safe Outputs + +| Output | Configuration | +|--------|--------------| +| `create-issue` | Prefix: `[drift]`, labels: `drift, security`, max: 5, auto-close older | +| `create-pull-request` | Prefix: `[drift-remediation]`, labels: `drift, automated-remediation`, draft: true, max: 10 | + +## Tools + +| Tool | Purpose | +|------|---------| +| `bash` | Azure CLI queries, JSON processing with jq | +| `edit` | Read/modify deployment files | +| `cache-memory` | Anti-flapping state and drift history | + +## Configuration + +### Enabling the Workflow + +The workflow is scaffolded into your repository by `/git-ape-onboarding`. To recompile after editing the agentic source: + +1. Install the `gh-aw` CLI extension: + ```bash + gh extension install github/gh-aw + ``` + +2. Compile the workflow: + ```bash + gh aw compile + ``` + +3. Commit and push both `.github/workflows/git-ape-drift.md` and the generated `.github/workflows/git-ape-drift.lock.yml` + +4. Configure required secrets for your chosen AI engine (see [Authentication](https://github.github.com/gh-aw/reference/auth/)) + +### Customizing the Schedule + +Edit the `on.schedule` field in the frontmatter: +```yaml +on: + schedule: daily around 06:00 # Default + # schedule: "0 */6 * * *" # Every 6 hours + # schedule: weekly on monday # Weekly +``` + +### Azure Authentication + +The workflow needs Azure CLI access to query resource state. Configure OIDC credentials as described in the [onboarding guide](../getting-started/onboarding). + +## Related + +- [Continuous Drift Remediation (compiled workflow)](./git-ape-drift-lock) — auto-generated reference for the `.lock.yml` +- [Azure Drift Detector skill](../skills/azure-drift-detector) — the skill that powers the agent's reasoning +- [Drift Detection use case](../use-cases/drift-detection) — high-level overview +- [Drift Detection deployment guide](../deployment/drift-detection) — operator playbook