Skip to content

Let the build-failure analyst push fixes it cannot suggest inline - #10607

Merged
YuliiaKovalova merged 8 commits into
microsoft:mainfrom
YuliiaKovalova:build-agent-push-fix
Aug 18, 2026
Merged

Let the build-failure analyst push fixes it cannot suggest inline#10607
YuliiaKovalova merged 8 commits into
microsoft:mainfrom
YuliiaKovalova:build-agent-push-fix

Conversation

@YuliiaKovalova

@YuliiaKovalova YuliiaKovalova commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Why

GitHub only accepts inline 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 build-failure analyst can only describe the fix and ask a human to commit it.

Concrete example that motivated this: dotnet/arcade#17348 (comment) — the analysis correctly identified two CS1503s in PublishBuildToMaestro.cs (a flowed Microsoft.DotNet.ProductConstructionService.Client inserted a parameter before CancellationToken) but had to end with "the fix needs to be committed separately", because that file is not in the dependency-bump diff.

What

Enable the push-to-pull-request-branch safe output on the automatic build-failure-analysis workflow (the /analyze-build-failure command workflow is deliberately left comment-only), so the agent can append the fix commit when — and only when — a suggestion structurally cannot carry it.

Guardrails

  • The push target is not the model's to choose. With target: "*" gh-aw takes the pull request number out of the agent's own tool call and only then checks whether that pull request is a fork, so an injected agent could redirect the push at an unrelated same-repo PR. The target is instead bound to check_run.pull_requests[0].number, which GitHub fills in itself and which never passes through the model.
  • Fork PRs can never be pushed to. GitHub leaves check_run.pull_requests empty for fork-originated check runs, so a fork PR has no push target at all (verified against live check runs in all three repos: same-repo PRs report exactly one entry, fork ones report none) — and gh-aw's handler refuses fork branches regardless. That bounds roles: all to same-repo branches: dependency flow plus people who already have write access. Pushes are append-only; force-push is impossible. The comment-only analysis still runs on fork PRs.
  • allowed-files is an exclusive allowlist, so build infrastructure (eng/, global.json, NuGet.config, .github/) is out of reach no matter what the agent produces; protected-files keeps its default blocked policy on top.
  • max: 1 plus a loop guard enforced entirely in trusted code. commit-title-suffix makes gh-aw's push handler stamp [build-failure-analysis] onto the commit title as it applies the patch, so the marker is written by the handler and never by the model. This requires patch-format: am: the default bundle transport never rewrites commit titles, which would have made the marker — and therefore the guard — a silent no-op. Before anything else runs, the fetch job reads the PR's head commit; if that tip is already such a commit and the build still fails, it publishes push-blocked, and the workflow's job-level if: skips the activation and agent jobs — gh-aw's safe_outputs job is itself conditioned on the agent not being skipped, so no push code path remains. The check fails closed (an unreadable commit blocks) and is scoped to the branch tip, so any later commit by anyone restores full analysis rather than abandoning the PR after one attempt. The push uses GITHUB_TOKEN, which does not re-trigger GitHub Actions — but the Azure DevOps GitHub app does rebuild, so this guard is the real brake on a fail → push → rebuild → fail loop.
  • Step 6b of the analyst playbook additionally requires the fix to be outside the diff, mechanical, and provable from the compiler error itself. Anything needing a design decision, changing behavior, or suppressing an analyzer stays a comment.
  • fallback-as-pull-request: false so a diverged branch cannot silently become a surprise PR.

Supporting changes

  • PR-head checkout. Authoring the commit needs the PR's tree — push-to-pull-request-branch ships file contents, so a fix authored against main would silently revert whatever else changed in that file. The fetch job now resolves pr-checkout-ref: the head branch name for same-repo PRs (gh-aw derives the push target from git rev-parse --abbrev-ref HEAD, so a detached SHA checkout would break bundle generation), falling back to refs/pull/<n>/head for forks.
  • Prompt-injection mitigation. gh-aw's own base-branch config restore is gated on its built-in PR-checkout step, which never fires for check_run (that event carries no pull_request payload). Checking out the PR head therefore puts PR-controlled agent-config content in the workspace — where the analyst reads its own playbook. A second sparse checkout of the base branch plus a pre-agent-steps step restores it before the agent starts, replaying gh-aw's inline sub-agent/skill restores afterwards. That checkout uses the PR's own base ref (resolved from the GitHub API by the fetch job), not the repository default branch, so a release-branch PR is analysed with the playbook that branch actually carries. The restore covers gh-aw's full protected set — folders .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 — including paths that do not exist on the base branch, which are deleted rather than left as PR-authored content.
  • No new secret is required. GH_AW_CI_TRIGGER_TOKEN appears in the lock because gh-aw wires that magic secret into every workflow using this safe output; it only exists to add an empty commit under a PAT so GitHub Actions CI re-runs on the pushed commit. It is deliberately left unset — our CI is Azure DevOps, which rebuilds on its own — and when unset the value is empty and gh-aw simply skips that step.
  • No PR code is built or executed. The workflow's bash allowlist gains only scoped git status/diff/log/rev-parse/add/commit — no interpreters, package managers or build tools. Note that gh-aw itself appends git branch/checkout/merge/rm/switch to the generated --allow-tool list whenever push-to-pull-request-branch is enabled; that cannot be narrowed from the workflow config, so the analyst playbook forbids those commands explicitly. git push is not injected — the agent can never write to the remote, and the push is performed by the safe_outputs job from a bundle of the agent's local commits.

Validation

  • Compiled with the pinned gh-aw compiler version, --strict, clean.
  • Lock diff reviewed: no action pin changes; contents: write is added only to the safe_outputs and conclusion jobs, the agent job stays least-privilege.
  • Verified git bundle of the incremental commit range works from the shallow agent checkout.

Residual risks

  • Azure DevOps rebuilds on the pushed commit; the marker guard bounds it to one automated attempt.
  • Fix commits land on the PR branch as verified bot commits and still require human review — the agent is required to say so in its summary comment.

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
dotnet/arcade#17348 (comment)).

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/<n>/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
Copilot AI balanced review requested due to automatic review settings August 17, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Enables automatic build-failure analysis to commit mechanical fixes when affected files are outside the PR diff.

Changes:

  • Adds guarded PR-branch push support.
  • Checks out the PR head and restores trusted agent configuration.
  • Extends the analyst playbook and regenerates workflow locks.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.github/workflows/shared/build-failure-analysis-shared.md Documents conditional fix pushing.
.github/workflows/shared/build-failure-analysis-fetch.md Resolves the appropriate PR checkout ref.
.github/workflows/build-failure-analysis.md Configures checkout, restoration, Git tools, and push output.
.github/workflows/build-failure-analysis.lock.yml Regenerates the automatic workflow.
.github/workflows/build-failure-analysis-command.lock.yml Regenerates shared fetch behavior.
.github/agents/build-failure-analyst.agent.md Adds mechanical-fix commit instructions and guardrails.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/build-failure-analysis.md Outdated
Comment thread .github/workflows/build-failure-analysis.md Outdated
Comment thread .github/workflows/build-failure-analysis.md Outdated
- 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
Copilot AI review requested due to automatic review settings August 17, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.github/agents/build-failure-analyst.agent.md:205

  • The loop guard must explicitly paginate the PR commits. The get_commits operation is paginated, so checking only its default first page can miss the newest [build-failure-analysis] commit on a longer PR and allow another fail → push cycle. Require fetching every page (using the maximum page size and continuing until exhausted) before concluding that the marker is absent.
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.

.github/workflows/build-failure-analysis.md:119

  • The generated safe_outputs job reuses this checkout expression, but that job only declares activation, agent, and detection in needs; it does not directly need fetch-binlog. GitHub Actions therefore cannot resolve needs.fetch-binlog.outputs.pr-checkout-ref there (see the generated lock around lines 2233 and 2312), so the write-capable checkout falls back to the event/default ref instead of the PR branch and the new push path cannot reliably apply its bundle. Ensure fetch-binlog is a direct dependency of safe_outputs (or relay this output through one of its direct dependencies), then regenerate the lock file.
  - ref: ${{ needs.fetch-binlog.outputs.pr-checkout-ref }}

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
Copilot AI review requested due to automatic review settings August 17, 2026 12:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

.github/agents/build-failure-analyst.agent.md:223

  • Step 5 already tells the agent to post exactly one summary before reaching Step 6b, but this instruction asks it to post the summary again and claim the commit was pushed. That either creates duplicate summaries or leaves the first one unable to mention the push; moreover, the push safe output runs in a later job and may still fail. Defer the single add_comment request until after deciding whether to queue a push, and describe the push as requested/pending rather than completed.
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.

.github/workflows/shared/build-failure-analysis-fetch.md:313

  • This loop check fails open. The fetch script has disabled pipefail and suppresses gh errors, so a transient API or pagination failure makes grep return no marker and leaves PUSH_BLOCKED=false; the next failed rebuild can then push again. Default to blocked and clear it only after the complete commit list was fetched successfully.
          if gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}/commits" --paginate \
               --jq '.[].commit.message' 2>/dev/null | grep -qF '[build-failure-analysis]'; then

.github/workflows/build-failure-analysis.md:213

  • The refusing hook is not visible where git commit runs. The compiled AWF command mounts only ${RUNNER_TEMP}/gh-aw plus the workspace (build-failure-analysis.lock.yml:990-991), while this hook lives in the sibling ${RUNNER_TEMP}/gh-aw-refuse-commits; Git therefore sees a nonexistent hooks directory and proceeds. Even if mounted, the allowed git commit:* permits --no-verify. Enforce the block in the trusted safe_outputs path before processing a push rather than with an agent-side pre-commit hook.
        HOOKS_DIR="${RUNNER_TEMP}/gh-aw-refuse-commits"

.github/agents/build-failure-analyst.agent.md:219

  • The marker that activates the next-run guard is itself only an agent instruction. If the model omits or misspells it, the fetch job never recognizes the automated commit, so every Azure rebuild can make another attempt despite the claimed deterministic limit. Trusted workflow/handler code must append or validate the marker before allowing the push.

This issue also appears on line 223 of the same file.

   [build-failure-analysis]

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
Copilot AI review requested due to automatic review settings August 17, 2026 12:58
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Addressed the latest review round (including the suppressed comments, which contained the most important findings).

The loop guard did not actually work, so it was rebuilt in trusted code.
The pre-commit hook lived in ${RUNNER_TEMP}/gh-aw-refuse-commits, but the agent firewall only mounts ${RUNNER_TEMP}/gh-aw and the workspace — git saw a nonexistent core.hooksPath and would have committed anyway, and even mounted, the allowed git commit:* permits --no-verify. Anything installed inside the agent sandbox is a suggestion, not a guarantee. The hook is gone. Instead:

  • The fetch job publishes push-blocked and the 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.
  • The [build-failure-analysis] marker is now stamped by commit-title-suffix, i.e. by gh-aw's push handler while it applies the patch. The guard no longer depends on the model writing (or correctly spelling) its own marker, and the playbook explicitly tells the agent not to add one.

The check fails closed now. It previously ran gh api … 2>/dev/null | grep -q, so a transient API error silently produced "not blocked". It defaults to blocked and clears only after the tip commit was read successfully. It also resolves the tip from the PR's head.sha instead of the ambient HEAD_SHA, which can hold the check run's merge commit — that alone would have made the guard never fire. Fork PRs are exempt, since gh-aw refuses to push to them and the guard must not suppress their comment-only analysis.

Scoped to the branch tip, not the whole history, so one automated attempt does not disable analysis on that PR forever: any later commit by anyone restores it.

Real bug fixed. Two echos had been joined onto one line in the fetch job's outputs block, which would have written a malformed $GITHUB_OUTPUT entry and lost ado-build-id entirely. Good catch.

No duplicate summary comment. Step 5 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 runs in a later job and can still be rejected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/build-failure-analysis.md
Comment thread .github/workflows/build-failure-analysis.md
…port

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
Copilot AI review requested due to automatic review settings August 17, 2026 13:30
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Round 4 of review feedback, applied to all three repos (arcade #17353, testfx #10607, sdk #55807).

1. The push target was model-controlled. With target: "*", gh-aw's handler resolves the pull request number supplied in the agent's own tool call and only then checks whether that pull request is a fork — so an injection could have aimed the push at an unrelated same-repo PR, escaping both the fork boundary and the loop guard. The target is now bound to check_run.pull_requests[0].number, straight from GitHub's webhook payload.

Because GitHub leaves that field empty for fork-originated check runs, this removes the push target entirely on fork PRs while leaving the comment-only analysis untouched. Verified against live check runs in all three repos: same-repo PRs report exactly one entry, fork PRs report none.

2. commit-title-suffix was a silent no-op. patch-format defaults to bundle, and the handler only rewrites commit titles on the git am path. The [build-failure-analysis] marker would never have been stamped, so the loop guard added in round 3 would never have fired. Pinned to patch-format: am, verified as "patch_format":"am" in the regenerated locks.

3. The agent-config restore used the wrong branch. It checked out github.event.repository.default_branch, so a release-branch PR was analysed with main's playbook and agent instructions. It now uses the PR's own base ref, which the fetch job already resolves from the GitHub API, falling back to the default branch if that lookup returns nothing.

4. Target/guard consistency. The fetch job prefers the ADO build's source branch when resolving the PR number. If that ever disagreed with the check payload, the guard and the push would be scoped to different branches, so the run is now refused outright in that case.

5. Reworded a comment that claimed a deliberately non-fatal diagnostic listing would "fail loudly".

All locks recompiled --strict with 0 errors and 0 warnings — including the target: "*" warning gh-aw used to emit, which the bound target removes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

…bject

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
Copilot AI review requested due to automatic review settings August 17, 2026 13:40
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Round 5. One finding this time (arcade, suppressed): the loop guard matched [build-failure-analysis] anywhere in the tip commit subject, which can false-positive on an unrelated commit that quotes the marker. The suggested fix was to anchor the match to the end of the subject.

Tightened, but deliberately not anchored — anchoring would have introduced a fail-open bug.

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. git am then reassembles the title with the marker in the middle. Verified end to end, using real git format-patch output rewritten with the handler's own regex:

SUBJECT: Fix CS1503 after [build-failure-analysis] Microsoft.DotNet.ProductConstructionService.Client bump changed GetBuildAsync signature at both call sites
ends with marker? False
contains " [marker]"? True

So an end-of-subject anchor would silently miss exactly the commits the guard exists to catch, and the fail → push → rebuild → fail loop would run unbounded. The asymmetry matters: a false positive skips one analysis (and any later commit on the branch restores it), while a false negative removes the brake entirely.

What did change:

  • The match now requires the leading space the handler always inserts, so a subject that merely opens with the marker is no longer treated as an automated fix. That removes the plausible false positive without giving up the substring semantics.
  • The reasoning is recorded next to the check, so it is not "simplified" into an anchored match later.
  • The analyst playbook now asks for commit titles of 60 characters or fewer, so the marker lands at the end in practice and the title stays readable.

Also confirmed there is nothing outstanding elsewhere: the two sdk threads (git checkout/git merge in the generated lock, and GH_AW_CI_TRIGGER_TOKEN) were addressed in earlier rounds and are resolved, and testfx has no comments newer than my round-4 replies.

All locks recompiled --strict: 0 errors, 0 warnings. testfx pin audit passes (2181 references across 47 files).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

# Conflicts:
#	.github/workflows/build-failure-analysis-command.lock.yml
#	.github/workflows/build-failure-analysis.lock.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

.github/workflows/shared/build-failure-analysis-shared.md:76

  • This condition contradicts the tip-scoped loop guard described in Step 6b and the PR: after any human commit, an earlier automated commit remains in history, so this wording can make the analyst refuse all future pushes even though the trusted guard intentionally re-enables them. Refer to the trusted guard allowing the current run, not to absence of a marker anywhere on the branch.
     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

.github/agents/build-failure-analyst.agent.md:237

  • The summarized condition “no previous automated attempt” conflicts with Step 6b item 6, which permits another attempt once a later human commit becomes the branch tip. Since this file is the analyst's instruction set, the contradiction can permanently disable the escape hatch after its first use. Summarize the actual tip-scoped trusted guard instead.
- 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.

.github/workflows/build-failure-analysis.md:117

  • This describes a full commit-list check, but the trusted guard reads only the current tip subject (PR_TIP_SHA) and Step 6b explicitly tells the agent not to inspect history. Because this workflow body is runtime-imported into the prompt, the inaccurate comment can reinforce the conflicting “no earlier commit” instruction and prevent the intended re-enable after a human commit. Describe the tip-only API check instead.
# 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.

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
Copilot AI review requested due to automatic review settings August 18, 2026 07:53
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Round 6 — all six suppressed findings triaged; five applied, one respectfully disputed with evidence.

Applied

  1. Tip-scoped guard vs. "no earlier commit on the branch" (3 findings on Let the build-failure analyst push fixes it cannot suggest inline #10607, same root cause). Correct and the most serious of the batch. The guard reads only PR_JSON.head.sha's subject, so a human commit on top legitimately re-enables the escape hatch — but the shared playbook, the analyst instructions and a workflow comment all still said "no earlier [build-failure-analysis] commit on the branch" / "no previous automated attempt". Since those files are injected into the prompt, that phrasing could have taught the analyst to refuse every push after the first, permanently disabling the feature. All three now describe the tip-only check and tell the agent it does not need to check history at all.

  2. Token comment (#55807). Correct. The push runs with secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN — verified in the generated lock — so a repo that sets GH_AW_GITHUB_TOKEN to a PAT/App token does get Actions re-runs. The comment now states both cases and notes this is a further reason the guard lives in a trusted job.

  3. git log:* allowlist entry (#17353). Correct — dropped. The checkout is depth-1 so it could only ever show the tip, and the playbook already forbade using it to reconstruct the guard.

Disputed — the 60-character commit-title guidance

The suggestion was to lower the limit to ~47 because the ~25-char suffix is appended. That would be right if the suffix were part of the commit before git format-patch ran, but it is not. From actions/setup/js/push_to_pull_request_branch.cjs at the pinned v0.77.5, the suffix is applied to the already-generated patch text:

patchContent.replace(/^Subject: (?:\[PATCH\] )?(.*)$/gm,
  (match, title) => `Subject: [PATCH] ${title}${commitTitleSuffix}`);

So folding is decided by format-patch on the bare title; the suffix never competes for that budget. Measured threshold, not estimated:

title length Subject: line folded?
60 77 no
61 78 no
62 yes

61 is the exact limit (Subject: [PATCH] = 17 chars of overhead against a 78-column header), so 60 is correct with one character of margin; 47 would have cost usable title space for no benefit.

That said, the original sentence was ambiguous enough to invite exactly this reading, so I rewrote it to say explicitly that the budget is for the title alone, that the marker is appended afterwards to the generated patch, and that the real constraint is the Subject: [PATCH] prefix folding at 62.

Note this is also why the round-5 guard match is deliberately unanchored: if a title does fold, git am reassembles it with the marker mid-string, so anchoring to end-of-subject would fail open.

All three repos recompiled with their pinned compilers — 0 errors, 0 warnings. testfx pin audit passes (2274 refs / 49 files).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.github/workflows/build-failure-analysis.md:433

  • This allowlist makes the escape hatch reject the dependency-flow PRs it is intended to fix. In the pinned gh-aw v0.84.3 implementation, configuring allowed-files triggers a preflight over every file in origin/<base>..<local tip>, not just the analyst's incremental commit. A dependency-flow branch already changes files such as eng/Version.Details.xml outside src//test/, so push_to_pull_request_branch returns an error before generating the patch. Please use a gh-aw version/configuration that applies this policy only to the incremental commit (while retaining apply-time enforcement), then regenerate the locks.
    allowed-files:
      - "src/**"
      - "test/**"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good as is.

If there are any future concerns with agent pushing directly into PRs - lets flip it to create PR targetting the underlying PR

@YuliiaKovalova
YuliiaKovalova enabled auto-merge (squash) August 18, 2026 11:55
@YuliiaKovalova
YuliiaKovalova merged commit 1ea28e3 into microsoft:main Aug 18, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants