From d7efbdcc558138529ffe0a62df7a44407a818d15 Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 12:07:11 +0200 Subject: [PATCH 1/7] Let the build-failure analyst push fixes it cannot suggest inline GitHub only accepts `suggestion` blocks on lines that are part of a PR's diff. Dependency-flow PRs break exactly that assumption: their diff is nothing but version files, so when a flowed package changes an API and previously-untouched call sites stop compiling, the analysis can only describe the fix and ask a human to commit it (see https://github.com/dotnet/arcade/pull/17348#issuecomment-5312250249). Enable the `push-to-pull-request-branch` safe output on the automatic build-failure-analysis workflow so the agent can append the fix commit instead, with narrow guardrails: * `allowed-files` is an exclusive allowlist, so build infrastructure is out of reach; `protected-files` keeps its default blocked policy. * gh-aw refuses pushes to fork branches, which bounds `roles: all` to same-repo branches (dependency flow + write-access humans). * `max: 1` plus a `[build-failure-analysis]` commit-marker check in the agent playbook (Step 6b) stops a fail -> push -> ADO rebuild -> fail loop from converging on nothing. * Step 6b also requires the fix to be mechanical and provable from the compiler error; anything else stays a comment. Authoring the commit requires the PR's tree, so the agent job now checks out the PR head branch by name (`pr-checkout-ref`, resolved by the fetch job; forks fall back to `refs/pull//head`). A branch name is required because gh-aw derives the push target from `git rev-parse --abbrev-ref HEAD`. gh-aw's own base-branch config restore is gated on its built-in PR-checkout step, which never fires for `check_run`, so a second sparse checkout plus a `pre-agent-steps` step restores `.github`, `.agents` and the root instruction files from the base branch before the agent starts. No PR code is built or executed: the bash allowlist gains only scoped `git status/diff/log/rev-parse/add/commit`, and the push itself is performed by the safe-outputs job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 42 ++++- .../build-failure-analysis-command.lock.yml | 24 ++- .../workflows/build-failure-analysis.lock.yml | 175 ++++++++++++++++-- .github/workflows/build-failure-analysis.md | 146 ++++++++++++++- .../shared/build-failure-analysis-fetch.md | 22 +++ .../shared/build-failure-analysis-shared.md | 14 +- 6 files changed, 393 insertions(+), 30 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index 164148ecaa..9d5ab67782 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -1,6 +1,6 @@ --- name: build-failure-analyst -description: "Expert build-failure analyst for .NET / MSBuild repositories. Invoke when a build produced a binary log (`*.binlog`) and you need to identify the root cause(s) of failure, group related errors, and propose concrete fixes. Queries the binlog live through the `binlog-mcp` MCP server (containerised — see the calling workflow's `mcp-servers.binlog-mcp` config) and posts an analysis comment plus inline `suggestion` blocks on the originating PR." +description: "Expert build-failure analyst for .NET / MSBuild repositories. Invoke when a build produced a binary log (`*.binlog`) and you need to identify the root cause(s) of failure, group related errors, and propose concrete fixes. Queries the binlog live through the `binlog-mcp` MCP server (containerised — see the calling workflow's `mcp-servers.binlog-mcp` config) and posts an analysis comment plus inline `suggestion` blocks on the originating PR — and, when the caller enables `push-to-pull-request-branch` and the fix cannot be expressed as a suggestion, appends a mechanical fix commit to the PR branch." --- # Expert Build Failure Analyst @@ -11,8 +11,9 @@ You are a senior .NET build engineer reviewing the binary log of a failed `dotne 2. Group all surface symptoms under each root cause. 3. Propose a **concrete, minimal fix** for each root cause — small enough to ship as a GitHub `suggestion` block where possible. 4. Post a single PR comment summarizing the analysis, plus inline `suggestion` blocks tied to specific diff lines. +5. When — and only when — the fix cannot be expressed as a suggestion because it lives outside the PR diff, append it to the PR branch as a commit (Step 6b). -You are read-only with respect to the repository. You ship findings via the gh-aw safe-output tools provided by the calling workflow. +You do not write to the repository directly. Every change you make is staged as a local commit and shipped through the gh-aw safe-output tools provided by the calling workflow, which apply their own allowlists and refuse anything outside them. --- @@ -188,7 +189,40 @@ Hard caps and rules: - The `suggestion` block must contain the **exact replacement line(s)** including original indentation. Do not include the line number, file name, or any prefix/suffix — just the raw code. - For multi-line suggestions, include all replacement lines inside the same `suggestion` block (each on its own line). The suggestion replaces the single line targeted by the comment. -If the offending line is **not** in the diff but the root cause clearly is (e.g., a declaration change in a PR-touched file caused errors at unchanged call sites), pick a declaration line in a PR-changed file and post the suggestion there with a note explaining the cascade. +If the offending line is **not** in the diff but the root cause clearly is (e.g., a declaration change in a PR-touched file caused errors at unchanged call sites), pick a declaration line in a PR-changed file and post the suggestion there with a note explaining the cascade. When there is no such line at all — the fix belongs entirely to a file the PR never touched — a suggestion cannot carry it; go to Step 6b. + +### Step 6b — Push the fix when a suggestion structurally cannot carry it + +GitHub only accepts a `suggestion` block on lines that are **part of the PR diff**. When the root-cause fix lives in a file the PR never touched, no inline comment can deliver it, and the analysis degrades into "here is a patch, please apply it by hand". The classic case is a dependency-flow PR (`darc-*`), whose diff is nothing but version bumps, where a flowed package changed an API and previously-unchanged call sites stopped compiling. For exactly that case the automatic `build-failure-analysis` workflow exposes the `push_to_pull_request_branch` safe-output tool, which appends a fix commit to the PR branch. + +**Use it only when every one of the following holds.** If any fails, describe the fix in the summary comment (Step 5) and stop — that is the expected outcome, not a failure: + +1. `push_to_pull_request_branch` is actually available to you as a tool. Not every caller enables it (the `/analyze-build-failure` command workflow does not); never assume it exists. +2. The fix target is **outside** the PR diff. If the line is in the diff, Step 6's inline suggestion wins — a suggestion a human clicks to apply is always preferable to a commit. +3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, so attempting one only wastes the run. +4. Every file you touch is under `src/` or `test/`. The workflow's `allowed-files` allowlist refuses anything else, and build infrastructure (`eng/`, `global.json`, `NuGet.config`, `.github/`) must never be "fixed" this way. +5. The fix is **mechanical and provable from the compiler error itself** — a renamed or moved API, an argument that must now be passed by name, a moved namespace. Anything that requires a design decision, changes behavior, suppresses an analyzer, or that you cannot fully verify against source you have actually read is a comment, not a commit. +6. **Loop guard.** List the PR's commits first. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. + +How to push: + +1. Edit the file(s) in the checked-out working tree. The workspace is checked out at the PR's head branch, so **verify `git rev-parse HEAD` equals `GH_AW_PR_HEAD_SHA` before editing** and abandon the push if it does not — the branch moved while you were analyzing and your fix would be based on a revision you never inspected. Note that `.github/`, `.agents/` and the root instruction files in the workspace are deliberately restored from the base branch and will therefore show as modified; ignore them and never stage them. +2. Stage only the files you changed (`git add `), then verify with `git status` that nothing else is staged. +3. Commit with a first line naming the fix and the marker on its own line, e.g.: + + ```text + Fix CS1503 after Microsoft.Testing.Platform bump + + RunAsync gained a filter parameter before cancellationToken, so pass the + token by name at both call sites. + + [build-failure-analysis] + ``` + +4. Call `push_to_pull_request_branch` targeting pull request `GH_AW_PR_NUMBER`. +5. Post the Step 5 summary comment **as well**, stating near the top that a fix commit was pushed to the branch and still requires human review. + +Never run `git push`, `git checkout`, `git reset`, `git rebase` or `git merge`: the safe-outputs job performs the push, and rewriting history on a branch you do not own is never acceptable. Push at most one commit per run. ### Step 7 — Stop @@ -200,7 +234,7 @@ Do not call `submit_pull_request_review` — this workflow uses `add-comment` (g - If a `binlog-mcp` call fails (server crashed, timeout, malformed response), fall back to whatever you have. Posting a partial analysis is better than posting nothing — but be clear about the gap in the summary comment. - If the binlog reports **no errors** but the build exit code says it failed, look for `Targets that failed`, `OnError` handlers, or non-MSBuild process failures (`Process is terminating due to ...`, native crashes). Include any clue in the summary. -- Do not propose fixes to files outside the PR diff in scan mode unless you are extremely confident — those changes are usually load-bearing across other projects. Prefer to explain the root cause in the comment and let a human apply the fix. +- Do not propose fixes to files outside the PR diff in scan mode unless you are extremely confident — those changes are usually load-bearing across other projects. Prefer to explain the root cause in the comment and let a human apply the fix. The single exception is Step 6b, whose conditions (mechanical fix, provable from the compiler error, `src/` or `test/` only, same-repo PR, no previous automated attempt) exist precisely to keep that confidence bar high. - Never propose a fix that disables an analyzer (`#pragma warning disable`, `` addition) without explicit reasoning — analyzers exist for a reason. - If you detect that the build failure looks like a **flake** (intermittent NuGet feed timeout, sporadic SDK download error, machine state), say so in the summary and recommend a re-run rather than a code change. diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index 69b7eb291e..a4748c95b5 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c26dae4a1ca4abef4b42214b148a46db479bbdf60f46c3a14e372b705fb234e4","body_hash":"12138283dffe89b249c24adc8cc1f216cc9a3f094796cd026187500eb1627c9e","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"90d1f93ae1016e00f0d30ccab1dd20b28f7ff18e33a830b86ec724165d8af4c3","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1654,6 +1654,7 @@ jobs: ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} binlog-found: ${{ steps.fetch.outputs.binlog-found }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} + pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-number: ${{ steps.fetch.outputs.pr-number }} @@ -1819,6 +1820,26 @@ jobs: *) echo "::warning::PR #${PR_NUMBER} base '${BASE_REF}' is out of scope (main, rel/*); skipping."; emit_none ;; esac + # --- 2b. Resolve the ref the agent job should check out --- + # The agent edits the PR's tree in place when it can push a fix, so it + # needs the PR revision — not `check_run`'s ref, which is the default + # branch. Two cases: + # * same-repo PR (dependency flow, maintainer branches): check out + # the head BRANCH BY NAME. gh-aw derives the push target from + # `git rev-parse --abbrev-ref HEAD`, so a detached checkout would + # report `HEAD` and break bundle generation. + # * fork PR: that branch does not exist here, so use the read-only + # `refs/pull//head`. Detached is fine — gh-aw refuses pushes to + # fork branches anyway, so those runs stay comment-only. + HEAD_REPO=$(printf '%s' "${PR_JSON}" | jq -r '.head.repo.full_name // empty') + HEAD_REF=$(printf '%s' "${PR_JSON}" | jq -r '.head.ref // empty') + if [ -n "${HEAD_REF}" ] && [ "${HEAD_REPO}" = "${GH_AW_REPO}" ]; then + CHECKOUT_REF="${HEAD_REF}" + else + CHECKOUT_REF="refs/pull/${PR_NUMBER}/head" + fi + echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -2146,6 +2167,7 @@ jobs: echo "pr-number=${PR_NUMBER}" echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" + echo "pr-checkout-ref=${CHECKOUT_REF}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index 2a71385867..32b1142960 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7d48f1e46b455a6fe0dcdbafdb6554b03ec37f9fb298054f30714d9d985355da","body_hash":"806439f4425a9fce72c3201fc4677084ce778e327529fdfafb68cdd558ca822d","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"464408b57c426ce6c37233e5e7bba320725f203ec5557655c305ae66e3a49530","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -32,6 +32,7 @@ # # Secrets used: # - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -268,20 +269,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_f5fce482246cb41a_EOF' + cat << 'GH_AW_PROMPT_a2e4d638f8e521cb_EOF' - GH_AW_PROMPT_f5fce482246cb41a_EOF + GH_AW_PROMPT_a2e4d638f8e521cb_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_f5fce482246cb41a_EOF' + cat << 'GH_AW_PROMPT_a2e4d638f8e521cb_EOF' - Tools: add_comment(max:5), create_pull_request_review_comment(max:25), missing_tool, missing_data, noop(max:5) + Tools: add_comment(max:5), create_pull_request_review_comment(max:25), push_to_pull_request_branch, missing_tool, missing_data, noop(max:5) + GH_AW_PROMPT_a2e4d638f8e521cb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_a2e4d638f8e521cb_EOF' - GH_AW_PROMPT_f5fce482246cb41a_EOF + GH_AW_PROMPT_a2e4d638f8e521cb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_f5fce482246cb41a_EOF' + cat << 'GH_AW_PROMPT_a2e4d638f8e521cb_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -308,16 +312,30 @@ jobs: {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [shallow clone, fetch-depth=1 (default)] + - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE/.gh-aw-base-config` [shallow clone, fetch-depth=1] [sparse checkout enabled] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + - **Warning: No git credentials are available to the agent.** Credentials are + intentionally removed after the checkout step for security. This means any git + operation that needs to authenticate to the remote will fail. In private repositories, that includes: + - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) + - Checking out or switching to a remote branch that is not already fetched + - Deepening a shallow clone (`git fetch --unshallow`) + - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) + Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — + authentication will not succeed. If you encounter credential prompts or authentication errors, + stop immediately and report the limitation rather than spending turns trying to work around it. - GH_AW_PROMPT_f5fce482246cb41a_EOF + GH_AW_PROMPT_a2e4d638f8e521cb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_f5fce482246cb41a_EOF' + cat << 'GH_AW_PROMPT_a2e4d638f8e521cb_EOF' {{#runtime-import .github/workflows/shared/build-failure-analysis-shared.md}} {{#runtime-import .github/workflows/shared/build-failure-analysis-fetch.md}} {{#runtime-import .github/workflows/build-failure-analysis.md}} - GH_AW_PROMPT_f5fce482246cb41a_EOF + GH_AW_PROMPT_a2e4d638f8e521cb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -467,6 +485,23 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }} + - name: Checkout .gh-aw-base-config + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.repository.default_branch }} + path: .gh-aw-base-config + fetch-depth: 1 + sparse-checkout: | + .github + .agents + filter: 'blob:limit=1073741824' + - name: Clear partial clone markers after sparse checkout + continue-on-error: true + run: | + git -C "${{ github.workspace }}/.gh-aw-base-config" config --local --unset-all remote.origin.promisor || true + git -C "${{ github.workspace }}/.gh-aw-base-config" config --local --unset-all remote.origin.partialclonefilter || true - 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 @@ -547,6 +582,12 @@ jobs: env: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + name: Restore agent config from the base branch + run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. Root instruction files are handled the\n# same way because the engine auto-loads them.\nfor FOLDER in .github .agents; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nfor FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" + shell: bash + - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c - name: Generate Safe Outputs Config @@ -554,16 +595,17 @@ jobs: 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_4761fa3a17077275_EOF' - {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_4761fa3a17077275_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_87aeebce45d8d849_EOF' + {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/**","test/**"],"check_branch_protection":false,"fallback_as_pull_request":false,"if_no_changes":"ignore","ignore_missing_branch_failure":true,"max":1,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_87aeebce45d8d849_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "create_pull_request_review_comment": " CONSTRAINTS: Maximum 25 review comment(s) can be created. Comments will be on the RIGHT side of the diff." + "create_pull_request_review_comment": " CONSTRAINTS: Maximum 25 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 1 push(es) can be made." }, "repo_params": {}, "dynamic_tools": [] @@ -686,6 +728,25 @@ jobs: } } }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + }, "report_incomplete": { "defaultMax": 5, "fields": { @@ -860,6 +921,18 @@ jobs: # --allow-tool shell(date) # --allow-tool shell(echo) # --allow-tool shell(find) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git diff:*) + # --allow-tool shell(git log:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rev-parse:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git status:*) + # --allow-tool shell(git switch:*) # --allow-tool shell(github:*) # --allow-tool shell(grep) # --allow-tool shell(head) @@ -907,7 +980,7 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 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_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 binlog-mcp --allow-tool '\''binlog-mcp(*)'\'' --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(binlog-mcp:*)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --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 --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 + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 binlog-mcp --allow-tool '\''binlog-mcp(*)'\'' --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(binlog-mcp:*)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git diff:*)'\'' --allow-tool '\''shell(git log:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rev-parse:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git status:*)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --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 --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 @@ -1094,6 +1167,7 @@ jobs: needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + contents: write issues: write pull-requests: write concurrency: @@ -1303,6 +1377,8 @@ jobs: GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} @@ -1591,6 +1667,7 @@ jobs: ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} binlog-found: ${{ steps.fetch.outputs.binlog-found }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} + pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-number: ${{ steps.fetch.outputs.pr-number }} @@ -1756,6 +1833,26 @@ jobs: *) echo "::warning::PR #${PR_NUMBER} base '${BASE_REF}' is out of scope (main, rel/*); skipping."; emit_none ;; esac + # --- 2b. Resolve the ref the agent job should check out --- + # The agent edits the PR's tree in place when it can push a fix, so it + # needs the PR revision — not `check_run`'s ref, which is the default + # branch. Two cases: + # * same-repo PR (dependency flow, maintainer branches): check out + # the head BRANCH BY NAME. gh-aw derives the push target from + # `git rev-parse --abbrev-ref HEAD`, so a detached checkout would + # report `HEAD` and break bundle generation. + # * fork PR: that branch does not exist here, so use the read-only + # `refs/pull//head`. Detached is fine — gh-aw refuses pushes to + # fork branches anyway, so those runs stay comment-only. + HEAD_REPO=$(printf '%s' "${PR_JSON}" | jq -r '.head.repo.full_name // empty') + HEAD_REF=$(printf '%s' "${PR_JSON}" | jq -r '.head.ref // empty') + if [ -n "${HEAD_REF}" ] && [ "${HEAD_REPO}" = "${GH_AW_REPO}" ]; then + CHECKOUT_REF="${HEAD_REF}" + else + CHECKOUT_REF="refs/pull/${PR_NUMBER}/head" + fi + echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -2083,6 +2180,7 @@ jobs: echo "pr-number=${PR_NUMBER}" echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" + echo "pr-checkout-ref=${CHECKOUT_REF}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" @@ -2140,6 +2238,7 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: + contents: write issues: write pull-requests: write timeout-minutes: 45 @@ -2168,6 +2267,8 @@ jobs: create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} 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 }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: - name: Setup Scripts id: setup @@ -2197,6 +2298,45 @@ jobs: 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: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true + ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + - name: Checkout .gh-aw-base-config + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true + ref: ${{ github.event.repository.default_branch }} + path: .gh-aw-base-config + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + fetch-depth: 1 + sparse-checkout: | + .github + .agents + filter: 'blob:limit=1073741824' + - name: Clear partial clone markers after sparse checkout + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + continue-on-error: true + run: | + git -C "${{ github.workspace }}/.gh-aw-base-config" config --local --unset-all remote.origin.promisor || true + git -C "${{ github.workspace }}/.gh-aw-base-config" config --local --unset-all remote.origin.partialclonefilter || true + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -2215,7 +2355,8 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.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,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"test/**\"],\"check_branch_protection\":false,\"fallback_as_pull_request\":false,\"if_no_changes\":\"ignore\",\"ignore_missing_branch_failure\":true,\"max\":1,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index 9f416ca73c..35d88bc414 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -19,8 +19,9 @@ description: >- # only downloads build artifacts (data) and reads them — it does **not** build # or execute PR code. (gh-aw's generated agent job **does** check out the # repository — via `actions/checkout` — to load the workflow's own agent -# configuration; that checkout is for tooling only and uses the event's ref, -# **not** the PR head, so no PR code is built or executed.) +# configuration and, since the `checkout:` block below, the analysed PR head so +# the agent can author a fix commit. The PR tree is only read and edited as +# text; nothing in it is built or executed.) on: # `check_run` fires for every check on a commit, so the `fetch-binlog` job @@ -32,7 +33,10 @@ on: # gh-aw's default author-association gate (which would otherwise skip # non-write-access actors, and on `check_run` the actor is the pipeline app # anyway). This is safe here: the workflow only reads a public binlog and - # posts advisory comments — it never builds or executes PR code. + # posts advisory comments — it never builds or executes PR code. The one + # write path that touches code (`push-to-pull-request-branch`) is refused + # outright by gh-aw's handler for fork PRs, so `roles: all` cannot turn an + # external contribution into a push. roles: all # Manual entry point for reruns / testing: analyse a specific Azure DevOps # build id and post to a specific PR. @@ -59,9 +63,10 @@ if: needs.fetch-binlog.outputs.binlog-found == 'true' # Least-privilege for the workflow/agent jobs. The agent runs read-only; it # does NOT post directly. All PR writes (summary comment + inline review -# suggestions) go through gh-aw **safe-outputs**, which the compiler emits as -# a separate `safe_outputs` job granted `pull-requests: write` + `issues: -# write` in the generated lock. Keep `pull-requests: read` here so the AI +# suggestions + the fix commit) go through gh-aw **safe-outputs**, which the +# compiler emits as a separate `safe_outputs` job granted `pull-requests: +# write` + `issues: write` (and, for `push-to-pull-request-branch`, `contents: +# write`) in the generated lock. Keep `pull-requests: read` here so the AI # agent job stays least-privilege — do NOT raise it to `write`, that would # hand PR-write scope to the agent job unnecessarily. permissions: @@ -81,6 +86,85 @@ concurrency: timeout-minutes: 30 +# The agent job's default checkout uses the event ref, and for `check_run` that +# is the repository's DEFAULT BRANCH — not the pull request. Without this block +# the workspace holds `main`, so an agent asked to fix a PR file would patch the +# wrong revision: `push-to-pull-request-branch` pushes the *file contents* of +# the agent's tree onto the PR branch, so a fix authored against `main` would +# silently revert anything else that changed in that file. `pr-checkout-ref` is +# the PR's head branch for same-repo PRs (attached, so gh-aw can derive the push +# target from `git rev-parse --abbrev-ref HEAD`) and `refs/pull//head` for +# forks, which gh-aw refuses to push to anyway. +# +# Checking out the PR head does NOT execute PR code: this workflow never builds, +# and the agent's bash allowlist contains no interpreters, package managers or +# build tools — the tree is read and edited as text only. +# +# It does, however, put PR-controlled `.github/`, `.agents/` and `AGENTS.md` +# content in the workspace, and the agent reads its playbook from there. gh-aw's +# own base-branch restore (`restore_base_github_folders.sh`) is gated on its +# built-in PR-checkout step, which never fires for `check_run` (that event +# carries no `pull_request` payload), so the second checkout below fetches the +# same agent config from the base branch and a `pre-agent-steps` step copies it +# over the PR's copy before the agent starts. Without that, a fork PR could +# rewrite the analyst's own instructions — `roles: all` lets every fork reach +# this workflow. +checkout: + - ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }} + - ref: ${{ github.event.repository.default_branch }} + path: .gh-aw-base-config + fetch-depth: 1 + sparse-checkout: | + .github + .agents + +pre-agent-steps: + - name: Restore agent config from the base branch + shell: bash + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + BASE=".gh-aw-base-config" + # Mirror gh-aw's restore_base_github_folders.sh: for each agent-config + # path, prefer the base-branch copy, and delete anything the PR added that + # the base branch does not have. Root instruction files are handled the + # same way because the engine auto-loads them. + for FOLDER in .github .agents; do + rm -rf "${FOLDER}" + if [ -d "${BASE}/${FOLDER}" ]; then + cp -r "${BASE}/${FOLDER}" "${FOLDER}" + echo "Restored ${FOLDER} from ${BASE_BRANCH}" + else + echo "Base branch has no ${FOLDER}; removed the PR's copy" + fi + done + for FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do + rm -f "${FILE}" + if [ -f "${BASE}/${FILE}" ]; then + cp "${BASE}/${FILE}" "${FILE}" + echo "Restored ${FILE} from ${BASE_BRANCH}" + fi + done + rm -rf "${BASE}" + # gh-aw restores inline sub-agents/skills from the activation artifact in + # the steps just above; the wipe above would drop them, so replay those + # restores. They no-op when the workflow defines none (this one does not), + # and are skipped entirely if a compiler upgrade renames the scripts. + for SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do + if [ -f "${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}" ]; then + GH_AW_SUB_AGENT_DIR=".github/agents" \ + GH_AW_SUB_AGENT_EXT=".agent.md" \ + GH_AW_SKILL_DIR=".github/skills" \ + bash "${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}" + fi + done + # The restored files differ from the PR head, so leave them staged-free and + # let git see them as modifications: the agent only ever commits the single + # source file it fixes, and gh-aw builds its patch from commits, never from + # the dirty worktree. Fail loudly if that assumption ever breaks. + git -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true + network: allowed: - defaults @@ -160,6 +244,13 @@ steps: tools: github: toolsets: [pull_requests, repos] + # `edit` + the `git add`/`git commit` pair below exist only for the + # `push-to-pull-request-branch` escape hatch (see safe-outputs and the + # analyst agent's "Step 6b"). Everything else stays read-only; note there is + # deliberately no `git push`, `git checkout`, `git reset`, `git rebase` or + # `git merge` — gh-aw builds the patch from the agent's local commits and + # performs the push itself in the `safe_outputs` job. + edit: bash: - "cat" - "head" @@ -170,6 +261,12 @@ tools: - "uniq" - "ls" - "find" + - "git status:*" + - "git diff:*" + - "git log:*" + - "git rev-parse:*" + - "git add:*" + - "git commit:*" safe-outputs: messages: @@ -184,6 +281,43 @@ safe-outputs: create-pull-request-review-comment: max: 25 target: "*" + # Escape hatch for the case inline suggestions structurally cannot cover: a + # `suggestion` block is only accepted by GitHub on lines that are part of the + # PR diff, so when the root-cause fix lives in a file the PR never touched + # (the classic dependency-flow break — a flowed package changes an API and the + # unchanged call sites stop compiling) the analysis could previously only + # describe the fix and ask a maintainer to commit it by hand. This lets the + # agent append the fix commit to the PR branch instead. + # + # Guardrails, in order of how much they actually protect: + # * gh-aw's handler refuses fork PRs outright (the workflow token has no + # write access to a fork), so this only ever reaches same-repo branches — + # i.e. dependency-flow (`darc-*`) branches and branches from people who + # already have write access. It is append-only; force-push is impossible. + # * `allowed-files` is an exclusive allowlist: anything outside `src/` and + # `test/` is refused by the handler regardless of what the agent produced. + # Build infrastructure (`eng/`, `global.json`, `.github/`, `NuGet.config`) + # is therefore out of reach, and `protected-files` stays at its default + # `blocked` policy on top of that. + # * `max: 1` plus the agent's own marker check (see the analyst agent's + # "Step 6b") bounds the fail → push → rebuild → fail loop. The push is + # made with GITHUB_TOKEN, which does not re-trigger GitHub Actions, but + # Azure DevOps' GitHub app *does* rebuild — so the loop guard is the agent + # refusing to push twice, not GitHub declining to re-run us. + # `fallback-as-pull-request: false` keeps a diverged branch from silently + # turning into a surprise PR (and drops the extra `pull-requests: write` + # requirement); `check-branch-protection: false` avoids needing + # `administration: read` just for a pre-flight the platform enforces anyway. + push-to-pull-request-branch: + max: 1 + target: "*" + allowed-files: + - "src/**" + - "test/**" + if-no-changes: "ignore" + ignore-missing-branch-failure: true + fallback-as-pull-request: false + check-branch-protection: false noop: max: 5 report-as-issue: false diff --git a/.github/workflows/shared/build-failure-analysis-fetch.md b/.github/workflows/shared/build-failure-analysis-fetch.md index 0c0832db10..954df20f19 100644 --- a/.github/workflows/shared/build-failure-analysis-fetch.md +++ b/.github/workflows/shared/build-failure-analysis-fetch.md @@ -46,6 +46,7 @@ jobs: pr-number: ${{ steps.fetch.outputs.pr-number }} pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} + pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} ado-build-id: ${{ steps.fetch.outputs.ado-build-id }} ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} @@ -274,6 +275,26 @@ jobs: *) echo "::warning::PR #${PR_NUMBER} base '${BASE_REF}' is out of scope (main, rel/*); skipping."; emit_none ;; esac + # --- 2b. Resolve the ref the agent job should check out --- + # The agent edits the PR's tree in place when it can push a fix, so it + # needs the PR revision — not `check_run`'s ref, which is the default + # branch. Two cases: + # * same-repo PR (dependency flow, maintainer branches): check out + # the head BRANCH BY NAME. gh-aw derives the push target from + # `git rev-parse --abbrev-ref HEAD`, so a detached checkout would + # report `HEAD` and break bundle generation. + # * fork PR: that branch does not exist here, so use the read-only + # `refs/pull//head`. Detached is fine — gh-aw refuses pushes to + # fork branches anyway, so those runs stay comment-only. + HEAD_REPO=$(printf '%s' "${PR_JSON}" | jq -r '.head.repo.full_name // empty') + HEAD_REF=$(printf '%s' "${PR_JSON}" | jq -r '.head.ref // empty') + if [ -n "${HEAD_REF}" ] && [ "${HEAD_REPO}" = "${GH_AW_REPO}" ]; then + CHECKOUT_REF="${HEAD_REF}" + else + CHECKOUT_REF="refs/pull/${PR_NUMBER}/head" + fi + echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -601,6 +622,7 @@ jobs: echo "pr-number=${PR_NUMBER}" echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" + echo "pr-checkout-ref=${CHECKOUT_REF}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/shared/build-failure-analysis-shared.md b/.github/workflows/shared/build-failure-analysis-shared.md index 95b061e570..68113454cb 100644 --- a/.github/workflows/shared/build-failure-analysis-shared.md +++ b/.github/workflows/shared/build-failure-analysis-shared.md @@ -20,8 +20,10 @@ Do **not** try to spawn a sub-agent: the `task` tool is intentionally not available here. Work directly with the tools you do have: `binlog-mcp` to read the logs, the `github` tools to read PR/repo context (the GitHub MCP server is **read-only** here), the `safeoutputs` tools (`add_comment`, -`create_pull_request_review_comment`, `noop`) to post results, and a small set -of read-only `shell` commands (including `cat`). +`create_pull_request_review_comment`, `noop`, and — only in the automatic +`build-failure-analysis` workflow — `push_to_pull_request_branch`) to post +results, and a small set of `shell` commands (including `cat` and, where the +caller allows it, `git add`/`git commit` for authoring a fix commit). ## Instructions @@ -65,6 +67,14 @@ of read-only `shell` commands (including `cat`). the pull request `GH_AW_PR_NUMBER` explicitly** (these workflows use `target: "*"`, so there is no implicit "triggering PR" — pass the number on every safe-output call). + - When the fix belongs to a file the PR never touched, an inline + `suggestion` cannot deliver it (GitHub only accepts suggestions on diff + lines). If — and only if — `push_to_pull_request_branch` is available to + you, follow **Step 6b** of the playbook to append a mechanical fix commit + to the PR branch. Every one of its conditions must hold (same-repo PR, + `src/` or `test/` only, provable from the compiler error, no earlier + `[build-failure-analysis]` commit on the branch); otherwise just describe + the fix in the summary comment. - `submit_pull_request_review` is **not** a safe output for this workflow; inline comments stand alone. From 899100faf1505e99ec444f9a2ee8c4b49c950d66 Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 13:44:45 +0200 Subject: [PATCH 2/7] Address review feedback on the push-to-branch escape hatch - Restore of the root agent-config files now consults the base-branch tree (git ls-tree) instead of trusting the sparse checkout to materialize them, so a sparse-checkout change can never turn "restore" into "delete". - Step 6b's loop guard now explicitly reads the PR's commit list through the GitHub tools; the PR-head checkout is depth-1, so git log cannot see it. - Correct the ools: comment: gh-aw itself widens the shell allowlist with git branch/checkout/merge/rm/switch when push-to-pull-request-branch is enabled. git push is still absent, and the playbook forbids the injected verbs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 4 +-- .../workflows/build-failure-analysis.lock.yml | 4 +-- .github/workflows/build-failure-analysis.md | 34 ++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index 9d5ab67782..5774dd695d 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -202,7 +202,7 @@ GitHub only accepts a `suggestion` block on lines that are **part of the PR diff 3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, so attempting one only wastes the run. 4. Every file you touch is under `src/` or `test/`. The workflow's `allowed-files` allowlist refuses anything else, and build infrastructure (`eng/`, `global.json`, `NuGet.config`, `.github/`) must never be "fixed" this way. 5. The fix is **mechanical and provable from the compiler error itself** — a renamed or moved API, an argument that must now be passed by name, a moved namespace. Anything that requires a design decision, changes behavior, suppresses an analyzer, or that you cannot fully verify against source you have actually read is a comment, not a commit. -6. **Loop guard.** List the PR's commits first. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. +6. **Loop guard.** List the PR's commits **with the GitHub tools** (the `pull_requests` toolset) — not `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. How to push: @@ -222,7 +222,7 @@ How to push: 4. Call `push_to_pull_request_branch` targeting pull request `GH_AW_PR_NUMBER`. 5. Post the Step 5 summary comment **as well**, stating near the top that a fix commit was pushed to the branch and still requires human review. -Never run `git push`, `git checkout`, `git reset`, `git rebase` or `git merge`: the safe-outputs job performs the push, and rewriting history on a branch you do not own is never acceptable. Push at most one commit per run. +Never run `git push`, `git checkout`, `git switch`, `git branch`, `git rm`, `git reset`, `git rebase` or `git merge`. Enabling the push safe output makes gh-aw widen the shell allowlist with several of these on its own — an allowlist entry is not permission. The safe-outputs job performs the push, and switching branches or rewriting history on a branch you do not own is never acceptable. Push at most one commit per run. ### Step 7 — Stop diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index 32b1142960..b9468623f4 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"464408b57c426ce6c37233e5e7bba320725f203ec5557655c305ae66e3a49530","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"eba8557235e54f62cbe9b1a82a11f110f880948334bb6b47585fe3ec08ec420c","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -585,7 +585,7 @@ jobs: - env: BASE_BRANCH: ${{ github.event.repository.default_branch }} name: Restore agent config from the base branch - run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. Root instruction files are handled the\n# same way because the engine auto-loads them.\nfor FOLDER in .github .agents; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nfor FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" + run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. Root instruction files are handled the\n# same way because the engine auto-loads them.\nfor FOLDER in .github .agents; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" shell: bash - name: Download container images diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index 35d88bc414..e29bb57c58 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -100,6 +100,12 @@ timeout-minutes: 30 # and the agent's bash allowlist contains no interpreters, package managers or # build tools — the tree is read and edited as text only. # +# The PR-head checkout is intentionally shallow (`actions/checkout`'s default +# depth of 1): gh-aw bundles only the commits the agent creates on top of it, so +# no history is needed. Step 6b's loop guard therefore reads the PR's commit +# list through the GitHub tools rather than `git log`, which cannot see the +# branch's history here. +# # It does, however, put PR-controlled `.github/`, `.agents/` and `AGENTS.md` # content in the workspace, and the agent reads its playbook from there. gh-aw's # own base-branch restore (`restore_base_github_folders.sh`) is gated on its @@ -114,6 +120,11 @@ checkout: - ref: ${{ github.event.repository.default_branch }} path: .gh-aw-base-config fetch-depth: 1 + # Cone mode (the `actions/checkout` default) materializes every top-level + # file in addition to the listed directories, which is how the base + # branch's `AGENTS.md`/`CLAUDE.md`/`GEMINI.md`/`.mcp.json` arrive. The + # restore step below does not rely on that: it consults the base tree + # directly, so a sparse-checkout change cannot turn "restore" into "delete". sparse-checkout: | .github .agents @@ -139,11 +150,20 @@ pre-agent-steps: echo "Base branch has no ${FOLDER}; removed the PR's copy" fi done + BASE_ROOT_FILES=$(git -C "${BASE}" ls-tree --name-only HEAD) for FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do rm -f "${FILE}" if [ -f "${BASE}/${FILE}" ]; then cp "${BASE}/${FILE}" "${FILE}" echo "Restored ${FILE} from ${BASE_BRANCH}" + elif printf '%s\n' "${BASE_ROOT_FILES}" | grep -qx -- "${FILE}"; then + # On the base branch but not materialized by the sparse checkout. + git -C "${BASE}" show "HEAD:${FILE}" > "${FILE}" + echo "Restored ${FILE} from ${BASE_BRANCH} (via git show)" + else + # Genuinely absent on the base branch, so the PR added it: removing + # it is the intended outcome. + echo "Base branch has no ${FILE}; removed the PR's copy" fi done rm -rf "${BASE}" @@ -246,10 +266,16 @@ tools: toolsets: [pull_requests, repos] # `edit` + the `git add`/`git commit` pair below exist only for the # `push-to-pull-request-branch` escape hatch (see safe-outputs and the - # analyst agent's "Step 6b"). Everything else stays read-only; note there is - # deliberately no `git push`, `git checkout`, `git reset`, `git rebase` or - # `git merge` — gh-aw builds the patch from the agent's local commits and - # performs the push itself in the `safe_outputs` job. + # analyst agent's "Step 6b"). Everything else stays read-only. + # + # NOTE: enabling `push-to-pull-request-branch` makes the **compiler** widen the + # generated shell allowlist on its own with `git branch/checkout/merge/rm/ + # switch` — see the `--allow-tool` list in the compiled lock. Those come from + # gh-aw, not from the list below, and cannot be removed from here. What matters + # is that `git push` is not among them: the agent can never write to the + # remote. The push happens in the `safe_outputs` job, from a bundle of the + # agent's local commits, filtered by `allowed-files`. The analyst playbook + # (Step 6b) forbids the agent from using the injected branch/history commands. edit: bash: - "cat" From 84c5cc665db856c574515b41ccfd77c23cd5f40d Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 14:06:50 +0200 Subject: [PATCH 3/7] Harden the build-failure-analysis push escape hatch Address the second round of review feedback: - Enforce the one-attempt loop guard deterministically instead of relying on the agent obeying a prompt. The fetch job now scans the PR's commits for the `[build-failure-analysis]` marker and publishes a `push-blocked` output; when it is set, `pre-agent-steps` installs a `pre-commit` hook via `core.hooksPath` that refuses every commit. `git config` is not in the agent's tool allowlist, so the agent cannot undo it, and gh-aw builds its patch from agent commits - with no commit there is nothing to push. - Restore the complete set of agent-config paths from the base branch, not just a subset. The sparse-checkout and the restore loops now cover gh-aw's full folder list (.agents .antigravity .claude .codex .crush .gemini .github .opencode .pi) and root files (.crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc), closing the gap where a PR could ship an unrestored instruction file. - Document that `GH_AW_CI_TRIGGER_TOKEN` is an optional gh-aw magic secret that is deliberately left unset: it only exists to re-trigger GitHub Actions CI on the pushed commit, and our CI runs in Azure DevOps which rebuilds on its own. Unset, the token is empty and the extra empty-commit step is skipped, so no configuration is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 4 +- .../build-failure-analysis-command.lock.yml | 20 ++++- .../workflows/build-failure-analysis.lock.yml | 41 +++++++++- .github/workflows/build-failure-analysis.md | 81 +++++++++++++++---- .../shared/build-failure-analysis-fetch.md | 18 +++++ 5 files changed, 141 insertions(+), 23 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index 5774dd695d..934c3554f1 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -202,11 +202,11 @@ GitHub only accepts a `suggestion` block on lines that are **part of the PR diff 3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, so attempting one only wastes the run. 4. Every file you touch is under `src/` or `test/`. The workflow's `allowed-files` allowlist refuses anything else, and build infrastructure (`eng/`, `global.json`, `NuGet.config`, `.github/`) must never be "fixed" this way. 5. The fix is **mechanical and provable from the compiler error itself** — a renamed or moved API, an argument that must now be passed by name, a moved namespace. Anything that requires a design decision, changes behavior, suppresses an analyzer, or that you cannot fully verify against source you have actually read is a comment, not a commit. -6. **Loop guard.** List the PR's commits **with the GitHub tools** (the `pull_requests` toolset) — not `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. +6. **Loop guard.** List the PR's commits **with the GitHub tools** (the `pull_requests` toolset) — not `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. This one is also enforced outside your control — when the workflow sees the marker on the branch it installs a git hook that refuses every commit, so ignoring this rule produces a failed `git commit`, never a push. How to push: -1. Edit the file(s) in the checked-out working tree. The workspace is checked out at the PR's head branch, so **verify `git rev-parse HEAD` equals `GH_AW_PR_HEAD_SHA` before editing** and abandon the push if it does not — the branch moved while you were analyzing and your fix would be based on a revision you never inspected. Note that `.github/`, `.agents/` and the root instruction files in the workspace are deliberately restored from the base branch and will therefore show as modified; ignore them and never stage them. +1. Edit the file(s) in the checked-out working tree. The workspace is checked out at the PR's head branch, so **verify `git rev-parse HEAD` equals `GH_AW_PR_HEAD_SHA` before editing** and abandon the push if it does not — the branch moved while you were analyzing and your fix would be based on a revision you never inspected. Note that the agent-config paths in the workspace (`.github/`, `.agents/`, the other engine-recognized config folders and the root instruction files) are deliberately restored from the base branch and will therefore show as modified; ignore them and never stage them. 2. Stage only the files you changed (`git add `), then verify with `git status` that nothing else is staged. 3. Commit with a first line naming the fix and the marker on its own line, e.g.: diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index a4748c95b5..9f58c2105d 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"90d1f93ae1016e00f0d30ccab1dd20b28f7ff18e33a830b86ec724165d8af4c3","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d0c924ce8ed0365de14b6c5afab1d4888fe675523d6f50cf8fcd39dd23fd2095","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1658,6 +1658,7 @@ jobs: pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-number: ${{ steps.fetch.outputs.pr-number }} + push-blocked: ${{ steps.fetch.outputs.push-blocked }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config @@ -1840,6 +1841,22 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 2c. Deterministic loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`; + # the analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement. When a fix commit is already on the + # branch and the build still fails, the automated fix is not + # converging and a human has to take over, so the agent job installs a + # git hook that refuses to create any commit at all (see + # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and + # the push cannot happen, whatever the model decides to do. + PUSH_BLOCKED=false + if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ + --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then + PUSH_BLOCKED=true + echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + fi + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -2168,6 +2185,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index b9468623f4..33fa868ca2 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"eba8557235e54f62cbe9b1a82a11f110f880948334bb6b47585fe3ec08ec420c","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"15632073185581d3a53c98f004252e1ebad189f0d6f03852185b38cb2c01a21b","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -494,8 +494,15 @@ jobs: path: .gh-aw-base-config fetch-depth: 1 sparse-checkout: | - .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .github + .opencode + .pi filter: 'blob:limit=1073741824' - name: Clear partial clone markers after sparse checkout continue-on-error: true @@ -584,8 +591,9 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - env: BASE_BRANCH: ${{ github.event.repository.default_branch }} + PUSH_BLOCKED: ${{ needs.fetch-binlog.outputs.push-blocked }} name: Restore agent config from the base branch - run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. Root instruction files are handled the\n# same way because the engine auto-loads them.\nfor FOLDER in .github .agents; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" + run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. The two lists below are gh-aw's own\n# `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with\n# `.mcp.json` added because this engine also auto-loads it — keeping them\n# in sync means the mitigation covers every path the engine recognizes,\n# not only the ones this repo uses. Unknown paths simply do not exist and\n# cost nothing.\nfor FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n\n# Deterministic loop guard. The fetch job (step 2c) already established\n# whether this branch carries a `[build-failure-analysis]` commit, i.e.\n# whether a previous run's automated fix failed to make the build pass.\n# In that case a human has to take over, so refuse commits outright rather\n# than trusting the agent to honour Step 6b: gh-aw assembles the patch\n# from the agent's commits, so a repository that cannot produce a commit\n# cannot produce a push. `git config` is not in the agent's tool\n# allowlist, so it cannot undo this.\nif [ \"${PUSH_BLOCKED}\" = \"true\" ]; then\n HOOKS_DIR=\"${RUNNER_TEMP}/gh-aw-refuse-commits\"\n mkdir -p \"${HOOKS_DIR}\"\n {\n echo '#!/usr/bin/env bash'\n echo 'echo \"This pull request already carries a [build-failure-analysis] fix commit and the build still failed, so the automated fix is not converging. Commits are refused for this run; report the analysis in a comment and leave the fix to a human.\" >&2'\n echo 'exit 1'\n } > \"${HOOKS_DIR}/pre-commit\"\n chmod +x \"${HOOKS_DIR}/pre-commit\"\n git config --local core.hooksPath \"${HOOKS_DIR}\"\n echo \"::warning::A [build-failure-analysis] commit is already on this branch; the push escape hatch is disabled for this run.\"\nfi\n" shell: bash - name: Download container images @@ -1671,6 +1679,7 @@ jobs: pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-number: ${{ steps.fetch.outputs.pr-number }} + push-blocked: ${{ steps.fetch.outputs.push-blocked }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config @@ -1853,6 +1862,22 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 2c. Deterministic loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`; + # the analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement. When a fix commit is already on the + # branch and the build still fails, the automated fix is not + # converging and a human has to take over, so the agent job installs a + # git hook that refuses to create any commit at all (see + # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and + # the push cannot happen, whatever the model decides to do. + PUSH_BLOCKED=false + if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ + --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then + PUSH_BLOCKED=true + echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + fi + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -2181,6 +2206,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" @@ -2321,8 +2347,15 @@ jobs: token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} fetch-depth: 1 sparse-checkout: | - .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .github + .opencode + .pi filter: 'blob:limit=1073741824' - name: Clear partial clone markers after sparse checkout if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index e29bb57c58..ae6caf2c83 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -106,8 +106,9 @@ timeout-minutes: 30 # list through the GitHub tools rather than `git log`, which cannot see the # branch's history here. # -# It does, however, put PR-controlled `.github/`, `.agents/` and `AGENTS.md` -# content in the workspace, and the agent reads its playbook from there. gh-aw's +# It does, however, put PR-controlled `.github/`, `.agents/`, `AGENTS.md` and +# every other agent-config path in the workspace, and the agent reads its +# playbook from there. gh-aw's # own base-branch restore (`restore_base_github_folders.sh`) is gated on its # built-in PR-checkout step, which never fires for `check_run` (that event # carries no `pull_request` payload), so the second checkout below fetches the @@ -121,27 +122,42 @@ checkout: path: .gh-aw-base-config fetch-depth: 1 # Cone mode (the `actions/checkout` default) materializes every top-level - # file in addition to the listed directories, which is how the base - # branch's `AGENTS.md`/`CLAUDE.md`/`GEMINI.md`/`.mcp.json` arrive. The - # restore step below does not rely on that: it consults the base tree - # directly, so a sparse-checkout change cannot turn "restore" into "delete". + # file in addition to the listed directories. The restore step below does + # not rely on that: it consults the base tree directly, so a sparse-checkout + # change cannot turn "restore" into "delete". The directory list mirrors + # gh-aw's own `GH_AW_AGENT_FOLDERS` (see the generated lock) — every path + # the engine treats as agent configuration, not just the ones this repo + # happens to use today, so a PR cannot introduce e.g. `.claude/` and have it + # survive into the agent's context. sparse-checkout: | - .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .github + .opencode + .pi pre-agent-steps: - name: Restore agent config from the base branch shell: bash env: BASE_BRANCH: ${{ github.event.repository.default_branch }} + PUSH_BLOCKED: ${{ needs.fetch-binlog.outputs.push-blocked }} run: | set -euo pipefail BASE=".gh-aw-base-config" # Mirror gh-aw's restore_base_github_folders.sh: for each agent-config # path, prefer the base-branch copy, and delete anything the PR added that - # the base branch does not have. Root instruction files are handled the - # same way because the engine auto-loads them. - for FOLDER in .github .agents; do + # the base branch does not have. The two lists below are gh-aw's own + # `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with + # `.mcp.json` added because this engine also auto-loads it — keeping them + # in sync means the mitigation covers every path the engine recognizes, + # not only the ones this repo uses. Unknown paths simply do not exist and + # cost nothing. + for FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do rm -rf "${FOLDER}" if [ -d "${BASE}/${FOLDER}" ]; then cp -r "${BASE}/${FOLDER}" "${FOLDER}" @@ -151,7 +167,7 @@ pre-agent-steps: fi done BASE_ROOT_FILES=$(git -C "${BASE}" ls-tree --name-only HEAD) - for FILE in AGENTS.md CLAUDE.md GEMINI.md .mcp.json; do + for FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do rm -f "${FILE}" if [ -f "${BASE}/${FILE}" ]; then cp "${BASE}/${FILE}" "${FILE}" @@ -185,6 +201,27 @@ pre-agent-steps: # the dirty worktree. Fail loudly if that assumption ever breaks. git -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true + # Deterministic loop guard. The fetch job (step 2c) already established + # whether this branch carries a `[build-failure-analysis]` commit, i.e. + # whether a previous run's automated fix failed to make the build pass. + # In that case a human has to take over, so refuse commits outright rather + # than trusting the agent to honour Step 6b: gh-aw assembles the patch + # from the agent's commits, so a repository that cannot produce a commit + # cannot produce a push. `git config` is not in the agent's tool + # allowlist, so it cannot undo this. + if [ "${PUSH_BLOCKED}" = "true" ]; then + HOOKS_DIR="${RUNNER_TEMP}/gh-aw-refuse-commits" + mkdir -p "${HOOKS_DIR}" + { + echo '#!/usr/bin/env bash' + echo 'echo "This pull request already carries a [build-failure-analysis] fix commit and the build still failed, so the automated fix is not converging. Commits are refused for this run; report the analysis in a comment and leave the fix to a human." >&2' + echo 'exit 1' + } > "${HOOKS_DIR}/pre-commit" + chmod +x "${HOOKS_DIR}/pre-commit" + git config --local core.hooksPath "${HOOKS_DIR}" + echo "::warning::A [build-failure-analysis] commit is already on this branch; the push escape hatch is disabled for this run." + fi + network: allowed: - defaults @@ -325,11 +362,23 @@ safe-outputs: # Build infrastructure (`eng/`, `global.json`, `.github/`, `NuGet.config`) # is therefore out of reach, and `protected-files` stays at its default # `blocked` policy on top of that. - # * `max: 1` plus the agent's own marker check (see the analyst agent's - # "Step 6b") bounds the fail → push → rebuild → fail loop. The push is - # made with GITHUB_TOKEN, which does not re-trigger GitHub Actions, but - # Azure DevOps' GitHub app *does* rebuild — so the loop guard is the agent - # refusing to push twice, not GitHub declining to re-run us. + # * `max: 1` bounds a single run; the fail → push → rebuild → fail loop is + # bounded deterministically instead of by model compliance. The fetch job + # (step 2c) checks whether the branch already carries a + # `[build-failure-analysis]` commit, and if it does, the `pre-agent-steps` + # step above points `core.hooksPath` at a `pre-commit` hook that refuses + # every commit. gh-aw builds the patch from the agent's commits, so with + # no commit there is nothing to push no matter what the model decides. + # The agent's own marker check ("Step 6b") is the polite layer on top. + # This matters because our push is made with GITHUB_TOKEN — which does not + # re-trigger GitHub Actions — but Azure DevOps' GitHub app *does* rebuild, + # so a new run can follow every push. + # Note the optional `GH_AW_CI_TRIGGER_TOKEN` magic secret (gh-aw wires it + # into the generated lock unconditionally) is deliberately NOT configured: + # it exists only to push an extra empty commit so *Actions* CI re-triggers. + # Unset, the expression resolves to an empty string and that step is + # skipped, so this workflow has no new secret prerequisite — and our CI is + # Azure DevOps, which rebuilds on its own. # `fallback-as-pull-request: false` keeps a diverged branch from silently # turning into a surprise PR (and drops the extra `pull-requests: write` # requirement); `check-branch-protection: false` avoids needing diff --git a/.github/workflows/shared/build-failure-analysis-fetch.md b/.github/workflows/shared/build-failure-analysis-fetch.md index 954df20f19..8fc323faa5 100644 --- a/.github/workflows/shared/build-failure-analysis-fetch.md +++ b/.github/workflows/shared/build-failure-analysis-fetch.md @@ -47,6 +47,7 @@ jobs: pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} + push-blocked: ${{ steps.fetch.outputs.push-blocked }} ado-build-id: ${{ steps.fetch.outputs.ado-build-id }} ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} @@ -295,6 +296,22 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" + # --- 2c. Deterministic loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`; + # the analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement. When a fix commit is already on the + # branch and the build still fails, the automated fix is not + # converging and a human has to take over, so the agent job installs a + # git hook that refuses to create any commit at all (see + # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and + # the push cannot happen, whatever the model decides to do. + PUSH_BLOCKED=false + if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ + --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then + PUSH_BLOCKED=true + echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + fi + # --- 3. Validate the build, whichever way it was resolved --- # It must be the microsoft.testfx definition (209), have failed, and # belong to this PR (sourceBranch == refs/pull//merge). No entry @@ -623,6 +640,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" } >> "$GITHUB_OUTPUT" From 399a6c58c1340b53792aedcc3d9795d8722a4b5e Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 14:58:53 +0200 Subject: [PATCH 4/7] Enforce the build-failure push loop guard in trusted code The previous loop guard did not actually work. It pointed `core.hooksPath` at `${RUNNER_TEMP}/gh-aw-refuse-commits`, but the agent firewall only mounts `${RUNNER_TEMP}/gh-aw` and the workspace, so git saw a nonexistent hooks directory and committed anyway - and even with the directory mounted, the allowed `git commit:*` permits `--no-verify`. Anything installed inside the agent's sandbox is a suggestion, not a guarantee. Move the decision entirely into trusted workflow code: - The fetch job publishes `push-blocked`, and the automatic workflow's job-level `if:` consumes it. When it is true the activation and agent jobs never run, and gh-aw's `safe_outputs` job (conditioned on the agent not being skipped) is skipped with them, so no push code path remains. The `/analyze-build-failure` command workflow is comment-only and ignores the output. - Stamp the `[build-failure-analysis]` marker with `commit-title-suffix` so gh-aw's push handler appends it while applying the patch. The guard no longer depends on the model remembering, or correctly spelling, a marker of its own; the playbook now tells the agent not to write one. - Make the check fail closed and read the branch tip directly. It used `gh api ... 2>/dev/null | grep -q`, so a transient API error silently produced "not blocked"; it now defaults to blocked and only clears after the tip commit was read successfully. The tip is resolved from the pull request's `head.sha` rather than the ambient `HEAD_SHA`, which can hold the check run's merge commit. Fork pull requests are exempt - gh-aw refuses to push to them, so the guard must not suppress their comment-only analysis. - Scope the guard to the branch tip instead of the whole history, so a pull request is not abandoned forever after one automated attempt: any later commit by anyone restores full analysis. Also fix a genuine defect introduced in the previous commit: two `echo` statements had been joined onto a single line in the fetch job's outputs block, which would have written a malformed `$GITHUB_OUTPUT` entry and lost `ado-build-id`. Finally, stop the playbook from asking for a second summary comment. Step 5 already posts exactly one; it is now posted after the push is requested and describes the commit as requested and pending rather than completed, since the push happens in a later job and can still fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 12 +-- .../build-failure-analysis-command.lock.yml | 57 ++++++++++---- .../workflows/build-failure-analysis.lock.yml | 76 +++++++++++++------ .github/workflows/build-failure-analysis.md | 54 ++++++------- .../shared/build-failure-analysis-fetch.md | 55 ++++++++++---- 5 files changed, 166 insertions(+), 88 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index 934c3554f1..f59f8e61be 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -113,7 +113,7 @@ If the source line at the reported `file:line` does not look like a plausible ca This step applies **only when you have confirmed a genuine build failure** (at least one leg has build errors or failed-target/process evidence). If every leg compiled cleanly, do not reach this step — `noop` silently per Step 2 instead. -When there is a build failure, first re-verify the target revision: read PR `GH_AW_PR_NUMBER` with the GitHub `pull_requests` read tool exposed by the github MCP server (the pull-request "get"/read operation) and take `head.sha` and `merge_commit_sha`. If `head.sha` cannot be read or no longer equals `GH_AW_PR_HEAD_SHA` — or `GH_AW_PR_MERGE_SHA` is non-empty and `merge_commit_sha` is non-empty but differs from it (the base branch advanced) — the PR moved while you were downloading/analyzing, so `noop` with a short reason and stop: your inline suggestions carry no `commit_id` and would land on the wrong lines of the new diff/merge. Otherwise post **exactly one** summary comment via `add_comment` (targeting the pull request `GH_AW_PR_NUMBER`). Mark it with the HTML marker `` so future runs (and humans) can identify and supersede it. The gh-aw `add-comment` config in `build-failure-analysis.md` has `hide-older-comments: true`, which collapses prior runs on update. +When there is a build failure, first re-verify the target revision: read PR `GH_AW_PR_NUMBER` with the GitHub `pull_requests` read tool exposed by the github MCP server (the pull-request "get"/read operation) and take `head.sha` and `merge_commit_sha`. If `head.sha` cannot be read or no longer equals `GH_AW_PR_HEAD_SHA` — or `GH_AW_PR_MERGE_SHA` is non-empty and `merge_commit_sha` is non-empty but differs from it (the base branch advanced) — the PR moved while you were downloading/analyzing, so `noop` with a short reason and stop: your inline suggestions carry no `commit_id` and would land on the wrong lines of the new diff/merge. Otherwise post **exactly one** summary comment via `add_comment` (targeting the pull request `GH_AW_PR_NUMBER`) — compose it here, but post it once as the last action of the run, after Steps 6 and 6b, so that it can report a requested push. Never post it twice. Mark it with the HTML marker `` so future runs (and humans) can identify and supersede it. The gh-aw `add-comment` config in `build-failure-analysis.md` has `hide-older-comments: true`, which collapses prior runs on update. Template: @@ -202,25 +202,25 @@ GitHub only accepts a `suggestion` block on lines that are **part of the PR diff 3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, so attempting one only wastes the run. 4. Every file you touch is under `src/` or `test/`. The workflow's `allowed-files` allowlist refuses anything else, and build infrastructure (`eng/`, `global.json`, `NuGet.config`, `.github/`) must never be "fixed" this way. 5. The fix is **mechanical and provable from the compiler error itself** — a renamed or moved API, an argument that must now be passed by name, a moved namespace. Anything that requires a design decision, changes behavior, suppresses an analyzer, or that you cannot fully verify against source you have actually read is a comment, not a commit. -6. **Loop guard.** List the PR's commits **with the GitHub tools** (the `pull_requests` toolset) — not `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. If any commit on the branch already carries the marker `[build-failure-analysis]` in its message, do **not** push again: a previous run already attempted a fix and the build still failed, which means the automated fix is not converging and a human must take over. Say exactly that in the summary comment instead. This one is also enforced outside your control — when the workflow sees the marker on the branch it installs a git hook that refuses every commit, so ignoring this rule produces a failed `git commit`, never a push. +6. **Loop guard.** You do not have to check this one, and you cannot influence it: before the workflow starts, a trusted job reads the branch tip and skips the entire run — agent included — when the tip commit is itself an automated `[build-failure-analysis]` fix. So if you are running at all, the previous automated attempt is not the head of this branch. Never try to re-establish the guard yourself from `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. How to push: 1. Edit the file(s) in the checked-out working tree. The workspace is checked out at the PR's head branch, so **verify `git rev-parse HEAD` equals `GH_AW_PR_HEAD_SHA` before editing** and abandon the push if it does not — the branch moved while you were analyzing and your fix would be based on a revision you never inspected. Note that the agent-config paths in the workspace (`.github/`, `.agents/`, the other engine-recognized config folders and the root instruction files) are deliberately restored from the base branch and will therefore show as modified; ignore them and never stage them. 2. Stage only the files you changed (`git add `), then verify with `git status` that nothing else is staged. -3. Commit with a first line naming the fix and the marker on its own line, e.g.: +3. Commit with a first line naming the fix and a body explaining it, e.g.: ```text Fix CS1503 after Microsoft.Testing.Platform bump RunAsync gained a filter parameter before cancellationToken, so pass the token by name at both call sites. - - [build-failure-analysis] ``` + Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. + 4. Call `push_to_pull_request_branch` targeting pull request `GH_AW_PR_NUMBER`. -5. Post the Step 5 summary comment **as well**, stating near the top that a fix commit was pushed to the branch and still requires human review. +5. Do **not** post a second comment. The run posts exactly one summary comment (Step 5); post it after this step and state near the top that a fix commit has been **requested** on the branch — the push is carried out by a later job and can still be rejected — and that it requires human review either way. Name the files you changed so a reviewer can act even if the push does not land. Never run `git push`, `git checkout`, `git switch`, `git branch`, `git rm`, `git reset`, `git rebase` or `git merge`. Enabling the push safe output makes gh-aw widen the shell allowlist with several of these on its own — an allowlist entry is not permission. The safe-outputs job performs the push, and switching branches or rewriting history on a branch you do not own is never acceptable. Push at most one commit per run. diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index 9f58c2105d..795feaa10f 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d0c924ce8ed0365de14b6c5afab1d4888fe675523d6f50cf8fcd39dd23fd2095","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d7106ed7d26055563de81e6265d413e39bf84109685ab819b58963364a92dccf","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1841,20 +1841,47 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" - # --- 2c. Deterministic loop guard for the push escape hatch --- - # Only the automatic workflow enables `push-to-pull-request-branch`; - # the analyst is told not to push a second fix (Step 6b), but an - # instruction is not enforcement. When a fix commit is already on the - # branch and the build still fails, the automated fix is not - # converging and a human has to take over, so the agent job installs a - # git hook that refuses to create any commit at all (see - # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and - # the push cannot happen, whatever the model decides to do. - PUSH_BLOCKED=false - if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ - --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then - PUSH_BLOCKED=true - echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + # --- 2c. Trusted loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`. + # The analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement, and neither is anything installed + # inside the agent's sandbox. So the decision is made here, in trusted + # workflow code, and the automatic workflow applies it in its job-level + # `if:`: when this output is `true` the activation and agent jobs never + # run, and `safe_outputs` is skipped with them, so no push is even + # reachable. The command workflow ignores this output — it is + # comment-only and has nothing to guard. + # + # The condition is "the branch tip is itself an automated fix": the + # previous attempt is the newest thing on the branch and the build + # still fails, so it did not converge and a human has to take over. + # Scoping it to the tip rather than to the whole history means the + # workflow resumes the moment anyone pushes anything else, instead of + # abandoning the pull request forever after one attempt. + # + # The `[build-failure-analysis]` marker is not written by the model: + # the workflow sets `commit-title-suffix`, so gh-aw's push handler + # appends it to the commit title while applying the patch. A guard + # that depended on the agent remembering to write its own marker + # would not be a guard. + # + # Fails closed: an unreadable commit blocks the escape hatch. + PUSH_BLOCKED=true + PR_TIP_SHA=$(printf '%s' "${PR_JSON}" | jq -r '.head.sha // empty') + if [ "${HEAD_REPO}" != "${GH_AW_REPO}" ]; then + # gh-aw refuses pushes to fork branches, so the loop guard is moot + # here and must not suppress the (comment-only) analysis. + PUSH_BLOCKED=false + elif [ -z "${PR_TIP_SHA}" ]; then + echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." + elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then + if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." + else + PUSH_BLOCKED=false + fi + else + echo "::warning::Could not read commit ${PR_TIP_SHA} of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." fi # --- 3. Validate the build, whichever way it was resolved --- diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index 33fa868ca2..1603b2ae04 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"15632073185581d3a53c98f004252e1ebad189f0d6f03852185b38cb2c01a21b","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ae32442356b88bdf749ee727db4884fdf35a08aed46dcc03cbf029f3cd9dcc27","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -92,7 +92,9 @@ jobs: needs: - fetch-binlog - pre_activation - if: needs.pre_activation.outputs.activated == 'true' && (needs.fetch-binlog.outputs.binlog-found == 'true') + if: > + needs.pre_activation.outputs.activated == 'true' && (needs.fetch-binlog.outputs.binlog-found == 'true' && + needs.fetch-binlog.outputs.push-blocked != 'true') runs-on: ubuntu-slim permissions: actions: read @@ -418,7 +420,9 @@ jobs: needs: - activation - fetch-binlog - if: (needs.fetch-binlog.outputs.binlog-found == 'true') && (needs.activation.outputs.daily_ai_credits_exceeded != 'true') + if: > + (needs.fetch-binlog.outputs.binlog-found == 'true' && needs.fetch-binlog.outputs.push-blocked != 'true') && + (needs.activation.outputs.daily_ai_credits_exceeded != 'true') runs-on: ubuntu-latest permissions: contents: read @@ -591,9 +595,8 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - env: BASE_BRANCH: ${{ github.event.repository.default_branch }} - PUSH_BLOCKED: ${{ needs.fetch-binlog.outputs.push-blocked }} name: Restore agent config from the base branch - run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. The two lists below are gh-aw's own\n# `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with\n# `.mcp.json` added because this engine also auto-loads it — keeping them\n# in sync means the mitigation covers every path the engine recognizes,\n# not only the ones this repo uses. Unknown paths simply do not exist and\n# cost nothing.\nfor FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n\n# Deterministic loop guard. The fetch job (step 2c) already established\n# whether this branch carries a `[build-failure-analysis]` commit, i.e.\n# whether a previous run's automated fix failed to make the build pass.\n# In that case a human has to take over, so refuse commits outright rather\n# than trusting the agent to honour Step 6b: gh-aw assembles the patch\n# from the agent's commits, so a repository that cannot produce a commit\n# cannot produce a push. `git config` is not in the agent's tool\n# allowlist, so it cannot undo this.\nif [ \"${PUSH_BLOCKED}\" = \"true\" ]; then\n HOOKS_DIR=\"${RUNNER_TEMP}/gh-aw-refuse-commits\"\n mkdir -p \"${HOOKS_DIR}\"\n {\n echo '#!/usr/bin/env bash'\n echo 'echo \"This pull request already carries a [build-failure-analysis] fix commit and the build still failed, so the automated fix is not converging. Commits are refused for this run; report the analysis in a comment and leave the fix to a human.\" >&2'\n echo 'exit 1'\n } > \"${HOOKS_DIR}/pre-commit\"\n chmod +x \"${HOOKS_DIR}/pre-commit\"\n git config --local core.hooksPath \"${HOOKS_DIR}\"\n echo \"::warning::A [build-failure-analysis] commit is already on this branch; the push escape hatch is disabled for this run.\"\nfi\n" + run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. The two lists below are gh-aw's own\n# `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with\n# `.mcp.json` added because this engine also auto-loads it — keeping them\n# in sync means the mitigation covers every path the engine recognizes,\n# not only the ones this repo uses. Unknown paths simply do not exist and\n# cost nothing.\nfor FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" shell: bash - name: Download container images @@ -603,9 +606,9 @@ jobs: 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_87aeebce45d8d849_EOF' - {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/**","test/**"],"check_branch_protection":false,"fallback_as_pull_request":false,"if_no_changes":"ignore","ignore_missing_branch_failure":true,"max":1,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_87aeebce45d8d849_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3cca9b46839d00f3_EOF' + {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/**","test/**"],"check_branch_protection":false,"commit_title_suffix":" [build-failure-analysis]","fallback_as_pull_request":false,"if_no_changes":"ignore","ignore_missing_branch_failure":true,"max":1,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_3cca9b46839d00f3_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -1862,20 +1865,47 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" - # --- 2c. Deterministic loop guard for the push escape hatch --- - # Only the automatic workflow enables `push-to-pull-request-branch`; - # the analyst is told not to push a second fix (Step 6b), but an - # instruction is not enforcement. When a fix commit is already on the - # branch and the build still fails, the automated fix is not - # converging and a human has to take over, so the agent job installs a - # git hook that refuses to create any commit at all (see - # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and - # the push cannot happen, whatever the model decides to do. - PUSH_BLOCKED=false - if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ - --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then - PUSH_BLOCKED=true - echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + # --- 2c. Trusted loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`. + # The analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement, and neither is anything installed + # inside the agent's sandbox. So the decision is made here, in trusted + # workflow code, and the automatic workflow applies it in its job-level + # `if:`: when this output is `true` the activation and agent jobs never + # run, and `safe_outputs` is skipped with them, so no push is even + # reachable. The command workflow ignores this output — it is + # comment-only and has nothing to guard. + # + # The condition is "the branch tip is itself an automated fix": the + # previous attempt is the newest thing on the branch and the build + # still fails, so it did not converge and a human has to take over. + # Scoping it to the tip rather than to the whole history means the + # workflow resumes the moment anyone pushes anything else, instead of + # abandoning the pull request forever after one attempt. + # + # The `[build-failure-analysis]` marker is not written by the model: + # the workflow sets `commit-title-suffix`, so gh-aw's push handler + # appends it to the commit title while applying the patch. A guard + # that depended on the agent remembering to write its own marker + # would not be a guard. + # + # Fails closed: an unreadable commit blocks the escape hatch. + PUSH_BLOCKED=true + PR_TIP_SHA=$(printf '%s' "${PR_JSON}" | jq -r '.head.sha // empty') + if [ "${HEAD_REPO}" != "${GH_AW_REPO}" ]; then + # gh-aw refuses pushes to fork branches, so the loop guard is moot + # here and must not suppress the (comment-only) analysis. + PUSH_BLOCKED=false + elif [ -z "${PR_TIP_SHA}" ]; then + echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." + elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then + if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." + else + PUSH_BLOCKED=false + fi + else + echo "::warning::Could not read commit ${PR_TIP_SHA} of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." fi # --- 3. Validate the build, whichever way it was resolved --- @@ -2388,7 +2418,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.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,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"test/**\"],\"check_branch_protection\":false,\"fallback_as_pull_request\":false,\"if_no_changes\":\"ignore\",\"ignore_missing_branch_failure\":true,\"max\":1,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"test/**\"],\"check_branch_protection\":false,\"commit_title_suffix\":\" [build-failure-analysis]\",\"fallback_as_pull_request\":false,\"if_no_changes\":\"ignore\",\"ignore_missing_branch_failure\":true,\"max\":1,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index ae6caf2c83..cc275b7a20 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -59,7 +59,17 @@ on: # fetch-binlog job is skipped, its output is empty, and this cascades into a # skipped agent — no AI calls on anything but a real `microsoft.testfx` failure whose # PR targets an in-scope base branch. -if: needs.fetch-binlog.outputs.binlog-found == 'true' +# +# `push-blocked` is the loop guard for the push escape hatch (shared fetch job, +# step 2c): when the branch tip is already an automated `[build-failure-analysis]` +# fix and the build still fails, the previous attempt did not converge and the +# pull request belongs to a human. Enforcing it here rather than inside the +# agent is deliberate — this condition skips the activation and agent jobs, and +# gh-aw's `safe_outputs` job is itself conditioned on the agent not being +# skipped, so there is no code path left that could push. Nothing the model +# does (or that a prompt injection makes it do) can re-enable it. The +# `/analyze-build-failure` command workflow is comment-only and ignores it. +if: needs.fetch-binlog.outputs.binlog-found == 'true' && needs.fetch-binlog.outputs.push-blocked != 'true' # Least-privilege for the workflow/agent jobs. The agent runs read-only; it # does NOT post directly. All PR writes (summary comment + inline review @@ -145,7 +155,6 @@ pre-agent-steps: shell: bash env: BASE_BRANCH: ${{ github.event.repository.default_branch }} - PUSH_BLOCKED: ${{ needs.fetch-binlog.outputs.push-blocked }} run: | set -euo pipefail BASE=".gh-aw-base-config" @@ -201,27 +210,6 @@ pre-agent-steps: # the dirty worktree. Fail loudly if that assumption ever breaks. git -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true - # Deterministic loop guard. The fetch job (step 2c) already established - # whether this branch carries a `[build-failure-analysis]` commit, i.e. - # whether a previous run's automated fix failed to make the build pass. - # In that case a human has to take over, so refuse commits outright rather - # than trusting the agent to honour Step 6b: gh-aw assembles the patch - # from the agent's commits, so a repository that cannot produce a commit - # cannot produce a push. `git config` is not in the agent's tool - # allowlist, so it cannot undo this. - if [ "${PUSH_BLOCKED}" = "true" ]; then - HOOKS_DIR="${RUNNER_TEMP}/gh-aw-refuse-commits" - mkdir -p "${HOOKS_DIR}" - { - echo '#!/usr/bin/env bash' - echo 'echo "This pull request already carries a [build-failure-analysis] fix commit and the build still failed, so the automated fix is not converging. Commits are refused for this run; report the analysis in a comment and leave the fix to a human." >&2' - echo 'exit 1' - } > "${HOOKS_DIR}/pre-commit" - chmod +x "${HOOKS_DIR}/pre-commit" - git config --local core.hooksPath "${HOOKS_DIR}" - echo "::warning::A [build-failure-analysis] commit is already on this branch; the push escape hatch is disabled for this run." - fi - network: allowed: - defaults @@ -363,16 +351,21 @@ safe-outputs: # is therefore out of reach, and `protected-files` stays at its default # `blocked` policy on top of that. # * `max: 1` bounds a single run; the fail → push → rebuild → fail loop is - # bounded deterministically instead of by model compliance. The fetch job - # (step 2c) checks whether the branch already carries a - # `[build-failure-analysis]` commit, and if it does, the `pre-agent-steps` - # step above points `core.hooksPath` at a `pre-commit` hook that refuses - # every commit. gh-aw builds the patch from the agent's commits, so with - # no commit there is nothing to push no matter what the model decides. - # The agent's own marker check ("Step 6b") is the polite layer on top. + # bounded by trusted code rather than by model compliance. `commit-title- + # suffix` makes gh-aw's push handler stamp `[build-failure-analysis]` onto + # the commit title as it applies the patch — the marker is written by the + # handler, never by the model — and the shared fetch job (step 2c) refuses + # to activate this workflow at all when the branch tip already carries it. + # Because the activation and agent jobs are skipped, gh-aw's own + # `safe_outputs` job (conditioned on the agent not being skipped) is + # skipped too, so no push code path remains. The agent playbook explains + # the rule, but nothing depends on the agent honouring it. # This matters because our push is made with GITHUB_TOKEN — which does not # re-trigger GitHub Actions — but Azure DevOps' GitHub app *does* rebuild, # so a new run can follow every push. + # The guard is scoped to the branch *tip*, not to the whole history, so a + # pull request is not abandoned forever after one automated attempt: any + # later commit by anyone restores full analysis. # Note the optional `GH_AW_CI_TRIGGER_TOKEN` magic secret (gh-aw wires it # into the generated lock unconditionally) is deliberately NOT configured: # it exists only to push an extra empty commit so *Actions* CI re-triggers. @@ -389,6 +382,7 @@ safe-outputs: allowed-files: - "src/**" - "test/**" + commit-title-suffix: " [build-failure-analysis]" if-no-changes: "ignore" ignore-missing-branch-failure: true fallback-as-pull-request: false diff --git a/.github/workflows/shared/build-failure-analysis-fetch.md b/.github/workflows/shared/build-failure-analysis-fetch.md index 8fc323faa5..e18e276305 100644 --- a/.github/workflows/shared/build-failure-analysis-fetch.md +++ b/.github/workflows/shared/build-failure-analysis-fetch.md @@ -296,20 +296,47 @@ jobs: fi echo "Agent checkout ref: '${CHECKOUT_REF}' (head repo '${HEAD_REPO}')" - # --- 2c. Deterministic loop guard for the push escape hatch --- - # Only the automatic workflow enables `push-to-pull-request-branch`; - # the analyst is told not to push a second fix (Step 6b), but an - # instruction is not enforcement. When a fix commit is already on the - # branch and the build still fails, the automated fix is not - # converging and a human has to take over, so the agent job installs a - # git hook that refuses to create any commit at all (see - # `pre-agent-steps`) — no commit means gh-aw has nothing to bundle and - # the push cannot happen, whatever the model decides to do. - PUSH_BLOCKED=false - if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \ - --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then - PUSH_BLOCKED=true - echo "PR #${PR_NUMBER} already carries a [build-failure-analysis] commit: the previous automated fix did not make the build pass, so the push escape hatch is disabled for this run and a human needs to take over." + # --- 2c. Trusted loop guard for the push escape hatch --- + # Only the automatic workflow enables `push-to-pull-request-branch`. + # The analyst is told not to push a second fix (Step 6b), but an + # instruction is not enforcement, and neither is anything installed + # inside the agent's sandbox. So the decision is made here, in trusted + # workflow code, and the automatic workflow applies it in its job-level + # `if:`: when this output is `true` the activation and agent jobs never + # run, and `safe_outputs` is skipped with them, so no push is even + # reachable. The command workflow ignores this output — it is + # comment-only and has nothing to guard. + # + # The condition is "the branch tip is itself an automated fix": the + # previous attempt is the newest thing on the branch and the build + # still fails, so it did not converge and a human has to take over. + # Scoping it to the tip rather than to the whole history means the + # workflow resumes the moment anyone pushes anything else, instead of + # abandoning the pull request forever after one attempt. + # + # The `[build-failure-analysis]` marker is not written by the model: + # the workflow sets `commit-title-suffix`, so gh-aw's push handler + # appends it to the commit title while applying the patch. A guard + # that depended on the agent remembering to write its own marker + # would not be a guard. + # + # Fails closed: an unreadable commit blocks the escape hatch. + PUSH_BLOCKED=true + PR_TIP_SHA=$(printf '%s' "${PR_JSON}" | jq -r '.head.sha // empty') + if [ "${HEAD_REPO}" != "${GH_AW_REPO}" ]; then + # gh-aw refuses pushes to fork branches, so the loop guard is moot + # here and must not suppress the (comment-only) analysis. + PUSH_BLOCKED=false + elif [ -z "${PR_TIP_SHA}" ]; then + echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." + elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then + if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." + else + PUSH_BLOCKED=false + fi + else + echo "::warning::Could not read commit ${PR_TIP_SHA} of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." fi # --- 3. Validate the build, whichever way it was resolved --- From ad4c324701aab6139260ee364b89a754624cd5ec Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 15:29:58 +0200 Subject: [PATCH 5/7] Bind the push escape hatch to the trusted PR and fix the marker transport Review round 4: * `target: "*"` left the pull request number inside the agent's own tool call: gh-aw's handler resolves the *supplied* number and only then checks whether that pull request is a fork, so an injected agent could aim the push at an unrelated same-repo pull request and escape both the fork boundary and the loop guard. The target is now bound to `check_run.pull_requests[0].number`, a field GitHub fills in itself and that is never routed through the model. Because GitHub leaves it empty for fork-originated check runs, this also removes the push target entirely on fork pull requests while leaving the comment-only analysis untouched (verified against live check runs in all three repositories: same-repo pull requests report exactly one entry, fork ones report none). * `patch-format` defaults to `bundle`, and gh-aw's handler only rewrites commit titles on the `git am` path, so `commit-title-suffix` was silently a no-op: the `[build-failure-analysis]` marker would never have been stamped and the loop guard that keys off it would never have fired. Pinned to `patch-format: am`. * The agent-config restore checked out `github.event.repository.default_branch` rather than the pull request's own base branch, so a release-branch pull request was analysed with `main`'s playbook and agent instructions. It now uses the base ref the fetch job already resolves from the GitHub API, falling back to the default branch when that lookup returns nothing. * The fetch job now refuses the run outright when the check payload and the Azure Pipelines build name different pull requests, so the push target and the loop guard can never end up scoped to different branches. * Reworded a comment that claimed a deliberately non-fatal diagnostic listing would "fail loudly". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 4 +- .../build-failure-analysis-command.lock.yml | 13 ++++- .../workflows/build-failure-analysis.lock.yml | 29 +++++++--- .github/workflows/build-failure-analysis.md | 55 +++++++++++++++---- .../shared/build-failure-analysis-fetch.md | 11 ++++ 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index f59f8e61be..b14efef871 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -199,7 +199,7 @@ GitHub only accepts a `suggestion` block on lines that are **part of the PR diff 1. `push_to_pull_request_branch` is actually available to you as a tool. Not every caller enables it (the `/analyze-build-failure` command workflow does not); never assume it exists. 2. The fix target is **outside** the PR diff. If the line is in the diff, Step 6's inline suggestion wins — a suggestion a human clicks to apply is always preferable to a commit. -3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, so attempting one only wastes the run. +3. The PR head repository equals the base repository (read the PR and compare `head.repo.full_name` with `base.repo.full_name`). gh-aw refuses pushes to fork branches, and the workflow additionally binds the push target to the pull request named in the `check_run` webhook payload — a field GitHub leaves empty for fork-originated check runs — so on a fork PR there is no push target at all and the call fails outright. Attempting one only wastes the run and turns it red. 4. Every file you touch is under `src/` or `test/`. The workflow's `allowed-files` allowlist refuses anything else, and build infrastructure (`eng/`, `global.json`, `NuGet.config`, `.github/`) must never be "fixed" this way. 5. The fix is **mechanical and provable from the compiler error itself** — a renamed or moved API, an argument that must now be passed by name, a moved namespace. Anything that requires a design decision, changes behavior, suppresses an analyzer, or that you cannot fully verify against source you have actually read is a comment, not a commit. 6. **Loop guard.** You do not have to check this one, and you cannot influence it: before the workflow starts, a trusted job reads the branch tip and skips the entire run — agent included — when the tip commit is itself an automated `[build-failure-analysis]` fix. So if you are running at all, the previous automated attempt is not the head of this branch. Never try to re-establish the guard yourself from `git log`: the workspace is a shallow, depth-1 checkout and does not contain the branch's history. @@ -219,7 +219,7 @@ How to push: Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. -4. Call `push_to_pull_request_branch` targeting pull request `GH_AW_PR_NUMBER`. +4. Call `push_to_pull_request_branch`. You do not choose — and cannot override — which pull request it lands on: the workflow pins the target to the pull request in the `check_run` payload, so the tool always pushes to the branch you are analyzing. 5. Do **not** post a second comment. The run posts exactly one summary comment (Step 5); post it after this step and state near the top that a fix commit has been **requested** on the branch — the push is carried out by a later job and can still be rejected — and that it requires human review either way. Name the files you changed so a reviewer can act even if the push does not land. Never run `git push`, `git checkout`, `git switch`, `git branch`, `git rm`, `git reset`, `git rebase` or `git merge`. Enabling the push safe output makes gh-aw widen the shell allowlist with several of these on its own — an allowlist entry is not permission. The safe-outputs job performs the push, and switching branches or rewriting history on a branch you do not own is never acceptable. Push at most one commit per run. diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index 795feaa10f..cb742ad844 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d7106ed7d26055563de81e6265d413e39bf84109685ab819b58963364a92dccf","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5792c2d4c5c2c7129ea2cc5d3995330abad93a4855a5aca529ae4838e4513e68","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1652,6 +1652,7 @@ jobs: outputs: ado-build-id: ${{ steps.fetch.outputs.ado-build-id }} ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} + base-ref: ${{ steps.fetch.outputs.base-ref }} binlog-found: ${{ steps.fetch.outputs.binlog-found }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} @@ -1872,6 +1873,15 @@ jobs: # gh-aw refuses pushes to fork branches, so the loop guard is moot # here and must not suppress the (comment-only) analysis. PUSH_BLOCKED=false + elif [ -n "${CHECK_PR_NUMBER}" ] && [ "${CHECK_PR_NUMBER}" != "${PR_NUMBER}" ]; then + # The push target is bound to `check_run.pull_requests[0].number` + # (see the `safe-outputs` block in the automatic workflow), while + # everything else keys off PR_NUMBER, which prefers the Azure + # Pipelines build's own source branch. Those agree in practice, but + # if they ever disagree the guard below would be checking one pull + # request while a push landed on another, so the loop would no + # longer be bounded. Refuse the run instead. + echo "::warning::The check payload names PR #${CHECK_PR_NUMBER} but the Azure Pipelines build belongs to PR #${PR_NUMBER}; skipping this run because the push target and the loop guard would disagree." elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then @@ -2212,6 +2222,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "base-ref=${BASE_REF}" echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index 1603b2ae04..e9c1fe9e66 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ae32442356b88bdf749ee727db4884fdf35a08aed46dcc03cbf029f3cd9dcc27","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ba1175ecb31670233f429282497c4bc4716186af3679063a347cef22933c95ff","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -494,7 +494,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.repository.default_branch }} + ref: ${{ needs.fetch-binlog.outputs.base-ref || github.event.repository.default_branch }} path: .gh-aw-base-config fetch-depth: 1 sparse-checkout: | @@ -594,9 +594,9 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - env: - BASE_BRANCH: ${{ github.event.repository.default_branch }} + BASE_BRANCH: ${{ needs.fetch-binlog.outputs.base-ref || github.event.repository.default_branch }} name: Restore agent config from the base branch - run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. The two lists below are gh-aw's own\n# `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with\n# `.mcp.json` added because this engine also auto-loads it — keeping them\n# in sync means the mitigation covers every path the engine recognizes,\n# not only the ones this repo uses. Unknown paths simply do not exist and\n# cost nothing.\nfor FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. Fail loudly if that assumption ever breaks.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" + run: "set -euo pipefail\nBASE=\".gh-aw-base-config\"\n# Mirror gh-aw's restore_base_github_folders.sh: for each agent-config\n# path, prefer the base-branch copy, and delete anything the PR added that\n# the base branch does not have. The two lists below are gh-aw's own\n# `GH_AW_AGENT_FOLDERS`/`GH_AW_AGENT_FILES` (see the generated lock), with\n# `.mcp.json` added because this engine also auto-loads it — keeping them\n# in sync means the mitigation covers every path the engine recognizes,\n# not only the ones this repo uses. Unknown paths simply do not exist and\n# cost nothing.\nfor FOLDER in .agents .antigravity .claude .codex .crush .gemini .github .opencode .pi; do\n rm -rf \"${FOLDER}\"\n if [ -d \"${BASE}/${FOLDER}\" ]; then\n cp -r \"${BASE}/${FOLDER}\" \"${FOLDER}\"\n echo \"Restored ${FOLDER} from ${BASE_BRANCH}\"\n else\n echo \"Base branch has no ${FOLDER}; removed the PR's copy\"\n fi\ndone\nBASE_ROOT_FILES=$(git -C \"${BASE}\" ls-tree --name-only HEAD)\nfor FILE in .crush.json .mcp.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc; do\n rm -f \"${FILE}\"\n if [ -f \"${BASE}/${FILE}\" ]; then\n cp \"${BASE}/${FILE}\" \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH}\"\n elif printf '%s\\n' \"${BASE_ROOT_FILES}\" | grep -qx -- \"${FILE}\"; then\n # On the base branch but not materialized by the sparse checkout.\n git -C \"${BASE}\" show \"HEAD:${FILE}\" > \"${FILE}\"\n echo \"Restored ${FILE} from ${BASE_BRANCH} (via git show)\"\n else\n # Genuinely absent on the base branch, so the PR added it: removing\n # it is the intended outcome.\n echo \"Base branch has no ${FILE}; removed the PR's copy\"\n fi\ndone\nrm -rf \"${BASE}\"\n# gh-aw restores inline sub-agents/skills from the activation artifact in\n# the steps just above; the wipe above would drop them, so replay those\n# restores. They no-op when the workflow defines none (this one does not),\n# and are skipped entirely if a compiler upgrade renames the scripts.\nfor SCRIPT in restore_inline_sub_agents.sh restore_inline_skills.sh; do\n if [ -f \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\" ]; then\n GH_AW_SUB_AGENT_DIR=\".github/agents\" \\\n GH_AW_SUB_AGENT_EXT=\".agent.md\" \\\n GH_AW_SKILL_DIR=\".github/skills\" \\\n bash \"${RUNNER_TEMP}/gh-aw/actions/${SCRIPT}\"\n fi\ndone\n# The restored files differ from the PR head, so leave them staged-free and\n# let git see them as modifications: the agent only ever commits the single\n# source file it fixes, and gh-aw builds its patch from commits, never from\n# the dirty worktree. The listing below is diagnostic only — it makes the\n# restored set visible in the job log when a push has to be explained\n# after the fact, and deliberately never fails the run.\ngit -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true\n" shell: bash - name: Download container images @@ -606,9 +606,9 @@ jobs: 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_3cca9b46839d00f3_EOF' - {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/**","test/**"],"check_branch_protection":false,"commit_title_suffix":" [build-failure-analysis]","fallback_as_pull_request":false,"if_no_changes":"ignore","ignore_missing_branch_failure":true,"max":1,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_3cca9b46839d00f3_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_cef41172d61399b3_EOF' + {"add_comment":{"hide_older_comments":true,"max":5,"target":"*"},"create_pull_request_review_comment":{"max":25,"side":"RIGHT","target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":5,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["src/**","test/**"],"check_branch_protection":false,"commit_title_suffix":" [build-failure-analysis]","fallback_as_pull_request":false,"if_no_changes":"ignore","ignore_missing_branch_failure":true,"max":1,"max_patch_size":4096,"patch_format":"am","protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"${{ github.event.check_run.pull_requests[0].number || github.event.inputs['pr-number'] }}"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_cef41172d61399b3_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -1676,6 +1676,7 @@ jobs: outputs: ado-build-id: ${{ steps.fetch.outputs.ado-build-id }} ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} + base-ref: ${{ steps.fetch.outputs.base-ref }} binlog-found: ${{ steps.fetch.outputs.binlog-found }} missing-legs: ${{ steps.fetch.outputs.missing-legs }} pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} @@ -1896,6 +1897,15 @@ jobs: # gh-aw refuses pushes to fork branches, so the loop guard is moot # here and must not suppress the (comment-only) analysis. PUSH_BLOCKED=false + elif [ -n "${CHECK_PR_NUMBER}" ] && [ "${CHECK_PR_NUMBER}" != "${PR_NUMBER}" ]; then + # The push target is bound to `check_run.pull_requests[0].number` + # (see the `safe-outputs` block in the automatic workflow), while + # everything else keys off PR_NUMBER, which prefers the Azure + # Pipelines build's own source branch. Those agree in practice, but + # if they ever disagree the guard below would be checking one pull + # request while a push landed on another, so the loop would no + # longer be bounded. Refuse the run instead. + echo "::warning::The check payload names PR #${CHECK_PR_NUMBER} but the Azure Pipelines build belongs to PR #${PR_NUMBER}; skipping this run because the push target and the loop guard would disagree." elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then @@ -2236,6 +2246,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "base-ref=${BASE_REF}" echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" @@ -2372,7 +2383,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true - ref: ${{ github.event.repository.default_branch }} + ref: ${{ needs.fetch-binlog.outputs.base-ref || github.event.repository.default_branch }} path: .gh-aw-base-config token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} fetch-depth: 1 @@ -2418,7 +2429,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.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,www.microsoft.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"test/**\"],\"check_branch_protection\":false,\"commit_title_suffix\":\" [build-failure-analysis]\",\"fallback_as_pull_request\":false,\"if_no_changes\":\"ignore\",\"ignore_missing_branch_failure\":true,\"max\":1,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":5,\"target\":\"*\"},\"create_pull_request_review_comment\":{\"max\":25,\"side\":\"RIGHT\",\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":5,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"test/**\"],\"check_branch_protection\":false,\"commit_title_suffix\":\" [build-failure-analysis]\",\"fallback_as_pull_request\":false,\"if_no_changes\":\"ignore\",\"ignore_missing_branch_failure\":true,\"max\":1,\"max_patch_size\":4096,\"patch_format\":\"am\",\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"${{ github.event.check_run.pull_requests[0].number || github.event.inputs['pr-number'] }}\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index cc275b7a20..6b9bba7d53 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -128,7 +128,13 @@ timeout-minutes: 30 # this workflow. checkout: - ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }} - - ref: ${{ github.event.repository.default_branch }} + # The pull request's own base branch (resolved from the GitHub API by the + # shared fetch job), not the repository default branch: a `rel/*` pull + # request must be analyzed with the playbook and agent config that branch + # actually carries, otherwise the restore below would silently swap in + # `main`'s instructions. Falls back to the default branch if the API lookup + # returned nothing. + - ref: ${{ needs.fetch-binlog.outputs.base-ref || github.event.repository.default_branch }} path: .gh-aw-base-config fetch-depth: 1 # Cone mode (the `actions/checkout` default) materializes every top-level @@ -154,7 +160,7 @@ pre-agent-steps: - name: Restore agent config from the base branch shell: bash env: - BASE_BRANCH: ${{ github.event.repository.default_branch }} + BASE_BRANCH: ${{ needs.fetch-binlog.outputs.base-ref || github.event.repository.default_branch }} run: | set -euo pipefail BASE=".gh-aw-base-config" @@ -207,7 +213,9 @@ pre-agent-steps: # The restored files differ from the PR head, so leave them staged-free and # let git see them as modifications: the agent only ever commits the single # source file it fixes, and gh-aw builds its patch from commits, never from - # the dirty worktree. Fail loudly if that assumption ever breaks. + # the dirty worktree. The listing below is diagnostic only — it makes the + # restored set visible in the job log when a push has to be explained + # after the fact, and deliberately never fails the run. git -c core.fileMode=false status --porcelain -- .github .agents AGENTS.md | head -n 20 || true network: @@ -341,9 +349,14 @@ safe-outputs: # agent append the fix commit to the PR branch instead. # # Guardrails, in order of how much they actually protect: - # * gh-aw's handler refuses fork PRs outright (the workflow token has no - # write access to a fork), so this only ever reaches same-repo branches — - # i.e. dependency-flow (`darc-*`) branches and branches from people who + # * The push target is bound to the pull request in the `check_run` + # webhook payload rather than to a number the agent supplies, so the + # agent cannot redirect the push at another pull request. Because GitHub + # leaves that field empty for fork-originated check runs, fork pull + # requests have no push target at all — on top of which gh-aw's handler + # refuses fork branches outright (the workflow token has no write access + # to a fork). So this only ever reaches same-repo branches — i.e. + # dependency-flow (`darc-*`) branches and branches from people who # already have write access. It is append-only; force-push is impossible. # * `allowed-files` is an exclusive allowlist: anything outside `src/` and # `test/` is refused by the handler regardless of what the agent produced. @@ -352,9 +365,11 @@ safe-outputs: # `blocked` policy on top of that. # * `max: 1` bounds a single run; the fail → push → rebuild → fail loop is # bounded by trusted code rather than by model compliance. `commit-title- - # suffix` makes gh-aw's push handler stamp `[build-failure-analysis]` onto - # the commit title as it applies the patch — the marker is written by the - # handler, never by the model — and the shared fetch job (step 2c) refuses + # suffix` (with `patch-format: am`, the only transport on which the + # handler rewrites commit titles) makes gh-aw's push handler stamp + # `[build-failure-analysis]` onto the commit title as it applies the + # patch — the marker is written by the handler, never by the model — + # and the shared fetch job (step 2c) refuses # to activate this workflow at all when the branch tip already carries it. # Because the activation and agent jobs are skipped, gh-aw's own # `safe_outputs` job (conditioned on the agent not being skipped) is @@ -378,11 +393,31 @@ safe-outputs: # `administration: read` just for a pre-flight the platform enforces anyway. push-to-pull-request-branch: max: 1 - target: "*" + # Deliberately NOT `target: "*"`. With `*`, gh-aw's handler takes the pull + # request number from the agent's own tool call, and only then checks + # whether *that* pull request is a fork — so the number is model-controlled + # and a prompt injection (build log, source comment, PR description) could + # aim the push at an unrelated same-repo pull request. Binding it to the + # check payload removes the choice: the number comes from GitHub's own + # webhook, is never routed through the model, and the handler rejects + # anything else. + # This also disables the escape hatch on fork pull requests at no extra + # cost: GitHub omits `pull_requests` for check runs on fork-originated + # commits, so the expression resolves to an empty string and no push target + # exists at all (verified against live `microsoft.testfx` check runs — + # same-repo pull requests report exactly one entry, the fork ones report + # none). The comment-only analysis is unaffected, which is the whole point + # of keeping this gate here instead of in the job-level `if:`. + target: "${{ github.event.check_run.pull_requests[0].number || github.event.inputs['pr-number'] }}" allowed-files: - "src/**" - "test/**" commit-title-suffix: " [build-failure-analysis]" + # Required for `commit-title-suffix` to do anything: `patch-format` + # defaults to `bundle`, and the handler only rewrites commit titles on the + # `git am` path. On the default transport the marker would never be + # applied, and the loop guard that keys off it would never fire. + patch-format: am if-no-changes: "ignore" ignore-missing-branch-failure: true fallback-as-pull-request: false diff --git a/.github/workflows/shared/build-failure-analysis-fetch.md b/.github/workflows/shared/build-failure-analysis-fetch.md index e18e276305..537bfc2c25 100644 --- a/.github/workflows/shared/build-failure-analysis-fetch.md +++ b/.github/workflows/shared/build-failure-analysis-fetch.md @@ -47,6 +47,7 @@ jobs: pr-head-sha: ${{ steps.fetch.outputs.pr-head-sha }} pr-merge-sha: ${{ steps.fetch.outputs.pr-merge-sha }} pr-checkout-ref: ${{ steps.fetch.outputs.pr-checkout-ref }} + base-ref: ${{ steps.fetch.outputs.base-ref }} push-blocked: ${{ steps.fetch.outputs.push-blocked }} ado-build-id: ${{ steps.fetch.outputs.ado-build-id }} ado-build-url: ${{ steps.fetch.outputs.ado-build-url }} @@ -327,6 +328,15 @@ jobs: # gh-aw refuses pushes to fork branches, so the loop guard is moot # here and must not suppress the (comment-only) analysis. PUSH_BLOCKED=false + elif [ -n "${CHECK_PR_NUMBER}" ] && [ "${CHECK_PR_NUMBER}" != "${PR_NUMBER}" ]; then + # The push target is bound to `check_run.pull_requests[0].number` + # (see the `safe-outputs` block in the automatic workflow), while + # everything else keys off PR_NUMBER, which prefers the Azure + # Pipelines build's own source branch. Those agree in practice, but + # if they ever disagree the guard below would be checking one pull + # request while a push landed on another, so the loop would no + # longer be bounded. Refuse the run instead. + echo "::warning::The check payload names PR #${CHECK_PR_NUMBER} but the Azure Pipelines build belongs to PR #${PR_NUMBER}; skipping this run because the push target and the loop guard would disagree." elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then @@ -667,6 +677,7 @@ jobs: echo "pr-head-sha=${HEAD_SHA}" echo "pr-merge-sha=${BUILD_MERGE_SHA}" echo "pr-checkout-ref=${CHECKOUT_REF}" + echo "base-ref=${BASE_REF}" echo "push-blocked=${PUSH_BLOCKED}" echo "ado-build-id=${BUILD_ID}" echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}" From 1f82560ba26f2266d62a583685e607dde16d7f87 Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Mon, 17 Aug 2026 15:40:29 +0200 Subject: [PATCH 6/7] Tighten the loop-guard marker match without anchoring it to end of subject Review round 5: the guard matched `[build-failure-analysis]` anywhere in the tip commit subject, which can false-positive on an unrelated commit that happens to quote the marker. It now requires the leading space the handler always inserts, so a subject that merely opens with the marker no longer counts as an automated fix. It is deliberately still not anchored to the end of the subject. gh-aw appends the suffix by rewriting the first `Subject:` line of a `git format-patch` mbox, and git folds subjects longer than ~72 characters onto continuation lines, so `git am` reassembles the title with the marker in the middle: Fix CS1503 after [build-failure-analysis] Microsoft.DotNet.Product... Verified end to end against real `git format-patch` output rewritten with the handler's own regex: the resulting commit subject contains the marker but does not end with it. Anchoring would silently miss those commits and let the fail -> push -> rebuild -> fail loop run unbounded, which is the one direction this guard must never fail in. A false positive only skips one analysis; a false negative removes the brake. The reasoning is now recorded next to the check so it is not "simplified" later, and the analyst playbook asks for commit titles of 60 characters or fewer so the marker lands at the end in practice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 2 +- .../build-failure-analysis-command.lock.yml | 15 +++++++++++++-- .github/workflows/build-failure-analysis.lock.yml | 15 +++++++++++++-- .../shared/build-failure-analysis-fetch.md | 13 ++++++++++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index b14efef871..d9b78a8efc 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -217,7 +217,7 @@ How to push: token by name at both call sites. ``` - Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. + Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. Keep the first line to 60 characters or fewer: the marker is appended to it, and git folds longer subject lines when the commit travels as a patch, which strands the marker in the middle of the title. 4. Call `push_to_pull_request_branch`. You do not choose — and cannot override — which pull request it lands on: the workflow pins the target to the pull request in the `check_run` payload, so the tool always pushes to the branch you are analyzing. 5. Do **not** post a second comment. The run posts exactly one summary comment (Step 5); post it after this step and state near the top that a fix commit has been **requested** on the branch — the push is carried out by a later job and can still be rejected — and that it requires human review either way. Name the files you changed so a reviewer can act even if the push does not land. diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index cb742ad844..920101223f 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5792c2d4c5c2c7129ea2cc5d3995330abad93a4855a5aca529ae4838e4513e68","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4b54166b97b0292966afd0eece3eeb3c2102d4e315a373b608cab3ebfc77017","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1885,7 +1885,18 @@ jobs: elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then - if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + # Deliberately a substring match, not an end-of-subject anchor. + # gh-aw appends the suffix by rewriting the first `Subject:` line + # of a `git format-patch` mbox, and git folds subjects longer than + # ~72 characters onto continuation lines, so `git am` reassembles + # the title with the marker in the *middle*, e.g. + # Fix CS1503 after [build-failure-analysis] Microsoft.DotNet... + # Anchoring to the end would silently miss exactly those commits + # and let the push loop run unbounded — the one direction this + # guard must never fail in. The leading space is required, which + # is what the handler always inserts, so a subject that merely + # opens with the marker is not mistaken for an automated fix. + if printf '%s' "${TIP_SUBJECT}" | grep -qF ' [build-failure-analysis]'; then echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." else PUSH_BLOCKED=false diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index e9c1fe9e66..a7b580e954 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ba1175ecb31670233f429282497c4bc4716186af3679063a347cef22933c95ff","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"63a4b0a630ccbe3812a2b7ffe1c9641f5f63203b24062a033c6f2f46d2455fc7","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1909,7 +1909,18 @@ jobs: elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then - if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + # Deliberately a substring match, not an end-of-subject anchor. + # gh-aw appends the suffix by rewriting the first `Subject:` line + # of a `git format-patch` mbox, and git folds subjects longer than + # ~72 characters onto continuation lines, so `git am` reassembles + # the title with the marker in the *middle*, e.g. + # Fix CS1503 after [build-failure-analysis] Microsoft.DotNet... + # Anchoring to the end would silently miss exactly those commits + # and let the push loop run unbounded — the one direction this + # guard must never fail in. The leading space is required, which + # is what the handler always inserts, so a subject that merely + # opens with the marker is not mistaken for an automated fix. + if printf '%s' "${TIP_SUBJECT}" | grep -qF ' [build-failure-analysis]'; then echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." else PUSH_BLOCKED=false diff --git a/.github/workflows/shared/build-failure-analysis-fetch.md b/.github/workflows/shared/build-failure-analysis-fetch.md index 537bfc2c25..4e09d767be 100644 --- a/.github/workflows/shared/build-failure-analysis-fetch.md +++ b/.github/workflows/shared/build-failure-analysis-fetch.md @@ -340,7 +340,18 @@ jobs: elif [ -z "${PR_TIP_SHA}" ]; then echo "::warning::Could not resolve the head commit of PR #${PR_NUMBER}; skipping this run rather than risking a repeated automated fix." elif TIP_SUBJECT=$(gh api "repos/${GH_AW_REPO}/commits/${PR_TIP_SHA}" --jq '.commit.message | split("\n")[0]'); then - if printf '%s' "${TIP_SUBJECT}" | grep -qF '[build-failure-analysis]'; then + # Deliberately a substring match, not an end-of-subject anchor. + # gh-aw appends the suffix by rewriting the first `Subject:` line + # of a `git format-patch` mbox, and git folds subjects longer than + # ~72 characters onto continuation lines, so `git am` reassembles + # the title with the marker in the *middle*, e.g. + # Fix CS1503 after [build-failure-analysis] Microsoft.DotNet... + # Anchoring to the end would silently miss exactly those commits + # and let the push loop run unbounded — the one direction this + # guard must never fail in. The leading space is required, which + # is what the handler always inserts, so a subject that merely + # opens with the marker is not mistaken for an automated fix. + if printf '%s' "${TIP_SUBJECT}" | grep -qF ' [build-failure-analysis]'; then echo "::warning::PR #${PR_NUMBER}'s tip commit is an automated [build-failure-analysis] fix and the build still fails, so the automated fix is not converging; skipping the automatic run and leaving the pull request to a human. Any further commit on the branch re-enables the analysis." else PUSH_BLOCKED=false From b32cc21a0edc23cfe4a0fa5a2f3e48aa97e0ed9b Mon Sep 17 00:00:00 2001 From: Yuliia Kovalova Date: Tue, 18 Aug 2026 09:53:14 +0200 Subject: [PATCH 7/7] Align the push escape hatch docs with the tip-scoped loop guard The guard has been tip-scoped since it moved into the trusted fetch job: it reads the subject of the PR's head commit and nothing else. Three places still described it as "no earlier [build-failure-analysis] commit on the branch" or "no previous automated attempt". Because the workflow body and the shared playbook are injected into the agent's prompt, that stricter phrasing could teach the analyst to refuse every push after the first one -- permanently disabling the escape hatch instead of re-enabling it once a human commit becomes the tip, which is what the guard actually does. Also correct the token comment. The push runs with `secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN`, so claiming it "is made with GITHUB_TOKEN -- which does not re-trigger GitHub Actions" understates the risk: a repository that sets GH_AW_GITHUB_TOKEN to a PAT or App token gets Actions re-runs as well, which is a further reason the guard is enforced in a trusted job rather than inferred from token behaviour. Drop `git log:*` from the agent's bash allowlist. The checkout is depth-1, so it can only ever show the tip, and the playbook already forbids using it to reconstruct the guard; removing it keeps the allowlist aligned with the documented workflow. Finally, make the commit-title guidance unambiguous. The 60-character budget applies to the title alone -- `commit_title_suffix` is appended by the handler to the already-generated patch, so it does not consume the budget. What does consume it is format-patch's own `Subject: [PATCH] ` prefix, which folds at 62 characters of title (measured, not estimated). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7154460-b0d2-4a80-98c8-6fcf6f5a904d --- .github/agents/build-failure-analyst.agent.md | 4 ++-- .../build-failure-analysis-command.lock.yml | 2 +- .../workflows/build-failure-analysis.lock.yml | 5 ++--- .github/workflows/build-failure-analysis.md | 21 ++++++++++++------- .../shared/build-failure-analysis-shared.md | 7 ++++--- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/agents/build-failure-analyst.agent.md b/.github/agents/build-failure-analyst.agent.md index 09c11f5460..1bfc45cba6 100644 --- a/.github/agents/build-failure-analyst.agent.md +++ b/.github/agents/build-failure-analyst.agent.md @@ -217,7 +217,7 @@ How to push: token by name at both call sites. ``` - Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. Keep the first line to 60 characters or fewer: the marker is appended to it, and git folds longer subject lines when the commit travels as a patch, which strands the marker in the middle of the title. + Do **not** add a `[build-failure-analysis]` marker yourself. The workflow configures `commit-title-suffix`, so the safe-outputs job appends the marker to the commit title as it applies the patch. That is deliberate: the loop guard must not depend on the model remembering — or correctly spelling — a marker. Keep the first line to 60 characters or fewer. That budget is for your title *alone*: the marker is appended afterwards, to the already-generated patch, so it does not count against it. What does count is `git format-patch`'s own `Subject: [PATCH] ` prefix, which starts folding the line at 62 characters of title; a folded subject is reassembled with the appended marker stranded in the middle of the title. 4. Call `push_to_pull_request_branch`. You do not choose — and cannot override — which pull request it lands on: the workflow pins the target to the pull request in the `check_run` payload, so the tool always pushes to the branch you are analyzing. 5. Do **not** post a second comment. The run posts exactly one summary comment (Step 5); post it after this step and state near the top that a fix commit has been **requested** on the branch — the push is carried out by a later job and can still be rejected — and that it requires human review either way. Name the files you changed so a reviewer can act even if the push does not land. @@ -234,7 +234,7 @@ Do not call `submit_pull_request_review` — this workflow uses `add-comment` (g - If a `binlog-mcp` call fails (server crashed, timeout, malformed response), fall back to whatever you have. Posting a partial analysis is better than posting nothing — but be clear about the gap in the summary comment. - If the binlog reports **no errors** but the build exit code says it failed, look for `Targets that failed`, `OnError` handlers, or non-MSBuild process failures (`Process is terminating due to ...`, native crashes). Include any clue in the summary. -- Do not propose fixes to files outside the PR diff in scan mode unless you are extremely confident — those changes are usually load-bearing across other projects. Prefer to explain the root cause in the comment and let a human apply the fix. The single exception is Step 6b, whose conditions (mechanical fix, provable from the compiler error, `src/` or `test/` only, same-repo PR, no previous automated attempt) exist precisely to keep that confidence bar high. +- Do not propose fixes to files outside the PR diff in scan mode unless you are extremely confident — those changes are usually load-bearing across other projects. Prefer to explain the root cause in the comment and let a human apply the fix. The single exception is Step 6b, whose conditions (mechanical fix, provable from the compiler error, `src/` or `test/` only, same-repo PR, and a trusted job having cleared this run) exist precisely to keep that confidence bar high. - Never propose a fix that disables an analyzer (`#pragma warning disable`, `` addition) without explicit reasoning — analyzers exist for a reason. - If you detect that the build failure looks like a **flake** (intermittent NuGet feed timeout, sporadic SDK download error, machine state), say so in the summary and recommend a re-run rather than a code change. diff --git a/.github/workflows/build-failure-analysis-command.lock.yml b/.github/workflows/build-failure-analysis-command.lock.yml index 8969e45f67..1a06e4e325 100644 --- a/.github/workflows/build-failure-analysis-command.lock.yml +++ b/.github/workflows/build-failure-analysis-command.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"40ad042b5c3b948a06d17778c334bb67419ad0b98a62f886885335958e812ddc","body_hash":"5d7fdddc4822b0edeced25d0fcfb825745dca24c4a28df875220fe2354056773","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"40ad042b5c3b948a06d17778c334bb67419ad0b98a62f886885335958e812ddc","body_hash":"e06ee4eaa7c01b6d039468dc215f64e0fb0cdf479c9c70db8f6642cbca3628d4","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml index 61d9f79b64..f70d3e6150 100644 --- a/.github/workflows/build-failure-analysis.lock.yml +++ b/.github/workflows/build-failure-analysis.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5ddb4eba9c8803b2b5fbfa9387918ebd9ab5d62814305745b86c77266bfa6081","body_hash":"2a0d9632a765a3a0777d0904a22f45d2495e7513f36166ded7b60f933475c261","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a258ab65c1951abc8764f14dbb333208d892b14cb3a470180a4db1756788f059","body_hash":"27596f31997c41ec679725edb7a4ca776cf8ce21bae5d5d05f09f2f909240217","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64","digest":"sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c","pinned_image":"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64@sha256:ee7b7e5c6e162f3f0061822aa7183260626f1a1e986d04ba9915ab197a37932c"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -937,7 +937,6 @@ jobs: # --allow-tool shell(git checkout:*) # --allow-tool shell(git commit:*) # --allow-tool shell(git diff:*) - # --allow-tool shell(git log:*) # --allow-tool shell(git merge:*) # --allow-tool shell(git rev-parse:*) # --allow-tool shell(git rm:*) @@ -991,7 +990,7 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 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_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 binlog-mcp --allow-tool '\''binlog-mcp(*)'\'' --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(binlog-mcp:*)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git diff:*)'\'' --allow-tool '\''shell(git log:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rev-parse:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git status:*)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --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 --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 + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 binlog-mcp --allow-tool '\''binlog-mcp(*)'\'' --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(binlog-mcp:*)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git diff:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rev-parse:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git status:*)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --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 --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 diff --git a/.github/workflows/build-failure-analysis.md b/.github/workflows/build-failure-analysis.md index 70ba8eb41c..2835fc6e62 100644 --- a/.github/workflows/build-failure-analysis.md +++ b/.github/workflows/build-failure-analysis.md @@ -112,9 +112,10 @@ timeout-minutes: 30 # # The PR-head checkout is intentionally shallow (`actions/checkout`'s default # depth of 1): gh-aw bundles only the commits the agent creates on top of it, so -# no history is needed. Step 6b's loop guard therefore reads the PR's commit -# list through the GitHub tools rather than `git log`, which cannot see the -# branch's history here. +# no history is needed. Nothing here needs it either: the loop guard runs in the +# trusted fetch job (step 2c) and reads only the subject of the PR's *tip* +# commit through the GitHub API — never `git log`, which cannot see the branch's +# history in a depth-1 checkout. # # It does, however, put PR-controlled `.github/`, `.agents/`, `AGENTS.md` and # every other agent-config path in the workspace, and the agent reads its @@ -320,9 +321,11 @@ tools: - "uniq" - "ls" - "find" + # `git log` is deliberately absent: the checkout is depth-1 so it can only + # ever show the tip, and the loop guard it might be mistaken for lives in + # the trusted fetch job. - "git status:*" - "git diff:*" - - "git log:*" - "git rev-parse:*" - "git add:*" - "git commit:*" @@ -387,9 +390,13 @@ safe-outputs: # `safe_outputs` job (conditioned on the agent not being skipped) is # skipped too, so no push code path remains. The agent playbook explains # the rule, but nothing depends on the agent honouring it. - # This matters because our push is made with GITHUB_TOKEN — which does not - # re-trigger GitHub Actions — but Azure DevOps' GitHub app *does* rebuild, - # so a new run can follow every push. + # This matters because the push runs with + # `secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN`: with the default + # `GITHUB_TOKEN` the push does not re-trigger *Actions*, but a repository + # that configures `GH_AW_GITHUB_TOKEN` as a PAT or App token gets Actions + # re-runs too. Azure DevOps' GitHub app rebuilds either way, so a new run + # can follow every push — which is precisely why the guard is enforced in + # a trusted job rather than left to the token's behaviour. # The guard is scoped to the branch *tip*, not to the whole history, so a # pull request is not abandoned forever after one automated attempt: any # later commit by anyone restores full analysis. diff --git a/.github/workflows/shared/build-failure-analysis-shared.md b/.github/workflows/shared/build-failure-analysis-shared.md index 68113454cb..40ceb33494 100644 --- a/.github/workflows/shared/build-failure-analysis-shared.md +++ b/.github/workflows/shared/build-failure-analysis-shared.md @@ -72,9 +72,10 @@ caller allows it, `git add`/`git commit` for authoring a fix commit). lines). If — and only if — `push_to_pull_request_branch` is available to you, follow **Step 6b** of the playbook to append a mechanical fix commit to the PR branch. Every one of its conditions must hold (same-repo PR, - `src/` or `test/` only, provable from the compiler error, no earlier - `[build-failure-analysis]` commit on the branch); otherwise just describe - the fix in the summary comment. + `src/` or `test/` only, provable from the compiler error); otherwise just + describe the fix in the summary comment. You do not need to check whether + an automated fix ran before — a trusted job already refused to start this + run if the branch *tip* is itself an automated fix. - `submit_pull_request_review` is **not** a safe output for this workflow; inline comments stand alone.