Skip to content

Build Failure Analysis: reuse Azure DevOps binlogs instead of rebuilding#10002

Draft
YuliiaKovalova wants to merge 18 commits into
microsoft:mainfrom
YuliiaKovalova:copilot/build-failure-analysis-reuse-binlog
Draft

Build Failure Analysis: reuse Azure DevOps binlogs instead of rebuilding#10002
YuliiaKovalova wants to merge 18 commits into
microsoft:mainfrom
YuliiaKovalova:copilot/build-failure-analysis-reuse-binlog

Conversation

@YuliiaKovalova

@YuliiaKovalova YuliiaKovalova commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworks both Build Failure Analysis workflows so they reuse the binary logs from the failed Azure DevOps build instead of rebuilding with ./build.sh.

testfx's authoritative PR build runs on Azure DevOps (dnceng-public/public, pipeline microsoft.testfx, definitionId 209) and publishes each build leg's binlog as a Logs_Build_<leg> pipeline artifact. The workflows now download those already-produced binlogs and analyze them.

Mirrors the arcade onboarding (dotnet/arcade#17125); the only per-repo differences are the pipeline check name, the ADO definition id, and the base-branch scope (main / rel/*).

What changed

  • Auto workflow: trigger changed from pull_request (which self-built) to check_run: completed. The fetch-binlog job filters to the microsoft.testfx check reporting failure, resolves the ADO build id from the check details_url, and downloads the binlogs anonymously (public project — no PAT). PR number is derived from the ADO build's authoritative source branch (
    efs/pull//merge), falling back to check_run.pull_requests[0] only if that is unavailable; the build's revision (PR head and merge commit) must still match the PR's current state before analysis.
  • Command workflow (/analyze-build-failure): finds the PR's most recent failed microsoft.testfx build (refs/pull/<n>/merge) and downloads its binlogs.
  • All build legs analyzed: every Logs_Build_* binlog is fetched into a directory mounted at /data/binlogs; the agent triages each leg via GH_AW_BINLOG_LIST and focuses on the leg(s) with errors. Test-only / Helix failures leave the build binlogs clean, so it reports "build compiled cleanly" rather than inventing fixes.
  • Both workflows are scoped to PRs targeting main / rel/*.
  • check_run has no native PR context in gh-aw, so safe-outputs use target: "*" and the agent posts to the resolved GH_AW_PR_NUMBER.

Why this is better

  • No duplicate build (fetch takes a few seconds vs a full testfx build).
  • No fork-PR code execution — we only download artifacts (data); nothing is checked out or run. The previous forks: [] / job-level fork guards are no longer needed.

Relationship to #9982

This PR retains the applicable agent-definition fixes from #9982 (inline-suggestion cap of 25, no hard-coded model, raw HTML marker). It does not carry #9982's NuGet.Mcp.Server invocation: the reuse-binlog design never builds or restores the PR locally, so there is no live restore for that MCP server to inspect — NuGet failures are now diagnosed from the binlog evidence plus the PR's package files read via the GitHub API. So this PR supersedes #9982 for everything except that deliberately-removed NuGet MCP path; #9982 can be closed once this lands, or merge #9982 first and I'll rebase.

Validation

The arcade equivalent was validated end-to-end (multi-leg fetch + agent analysis) via workflow_dispatch against a real public build. The testfx logic is identical apart from the three per-repo constants above. Lock files compiled with gh aw v0.82.8 (matching the repo) — 0 errors / 0 warnings.

Reworks both build-failure-analysis workflows so they no longer run
`./build.sh`. testfx's authoritative PR build runs on Azure DevOps
(dnceng-public/public, pipeline "microsoft.testfx", definitionId 209) and
publishes each build leg's binary log as a `Logs_Build_<leg>` pipeline
artifact. The workflows now download those already-produced binlogs and
analyze them — no duplicate build, and no fork-PR code execution (we only
download artifacts, never check out or run PR code).

- Auto workflow: trigger changed from `pull_request` (self-build) to
  `check_run: completed`; the fetch-binlog job filters to the
  `microsoft.testfx` check reporting failure, resolves the ADO build id from
  the check details URL, and downloads every `Logs_Build_*` binlog
  anonymously (public project). PR number comes from the check payload with a
  commits->PR fallback for fork PRs. Scoped to PRs targeting main / rel/*.
- Command workflow (`/analyze-build-failure`): finds the PR's most recent
  failed `microsoft.testfx` build (refs/pull/<n>/merge) and downloads its
  binlogs. Same base-branch scope.
- All build legs are fetched into a directory mounted at /data/binlogs; the
  agent triages every leg via GH_AW_BINLOG_LIST and focuses on the one(s)
  with errors (test-only failures leave build binlogs clean -> it says so
  rather than inventing fixes).
- check_run has no native PR context in gh-aw, so safe-outputs use
  `target: "*"` and the agent posts to the resolved GH_AW_PR_NUMBER.
- Also folds in the agent-doc fixes from microsoft#9982 (NuGet.Mcp.Server invocation,
  inline-suggestion cap 25, no hard-coded model, raw HTML marker).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 09:07

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

Reuses Azure DevOps build binlogs for automated and command-triggered failure analysis instead of rebuilding PR code.

Changes:

  • Downloads and analyzes binlogs from all Azure Pipelines build legs.
  • Adds check_run triggering and failed-build lookup for slash commands.
  • Updates agent context, NuGet tooling, safe outputs, and generated workflows.

Reviewed changes

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

Show a summary per file
File Description
.github/workflows/shared/build-failure-analysis-shared.md Updates shared agent delegation instructions.
.github/workflows/build-failure-analysis.md Implements automatic Azure DevOps binlog retrieval.
.github/workflows/build-failure-analysis.lock.yml Regenerates the automatic workflow.
.github/workflows/build-failure-analysis-command.md Retrieves failed-build binlogs for slash commands.
.github/workflows/build-failure-analysis-command.lock.yml Regenerates the command workflow.
.github/agents/build-failure-analyst.agent.md Adds multi-leg binlog analysis guidance.

task (`task` tool, `agent_type: "general-purpose"`,
`model: "claude-opus-4.6"`, `mode: "background"`). In the sub-agent prompt
include:
task (`task` tool, `agent_type: "general-purpose"`, `mode: "background"`).
## Inputs the Calling Workflow Provides

The caller (typically `build-failure-analysis.md` or `build-failure-analysis-command.md`) runs the build, uploads the `.binlog` file as an artifact, and the gh-aw MCP gateway mounts it read-only into the `binlog-mcp` container at `/data/build.binlog`. The caller also sets the environment variables below. You must read all of them before doing anything else.
The caller (typically `build-failure-analysis.md` or `build-failure-analysis-command.md`) locates the failed **Azure DevOps** `microsoft.testfx` build, downloads the `.binlog` that build already produced (it does **not** rebuild), uploads it as an artifact, and the gh-aw MCP gateway mounts it read-only into the `binlog-mcp` container at `/data/build.binlog`. The caller also sets the environment variables below. You must read all of them before doing anything else.
Comment on lines +25 to +26
check_run:
types: [completed]
Comment on lines +110 to +114
builds_json=$(curl -sSL --retry 3 \
"${ADO_API}/build/builds?definitions=${ADO_BUILD_DEFINITION_ID}&branchName=refs/pull/${PR_NUMBER}/merge&statusFilter=completed&resultFilter=failed&queryOrder=finishTimeDescending&\$top=1&api-version=7.1")
BUILD_ID=$(printf '%s' "${builds_json}" | jq -r '.value // [] | .[0].id // empty')
echo "Latest failed microsoft.testfx build for PR #${PR_NUMBER}: '${BUILD_ID}'"
[ -z "${BUILD_ID}" ] && { echo "::warning::No completed+failed microsoft.testfx build found for PR #${PR_NUMBER}."; emit_none; }
Comment on lines +164 to +168
RESULT=$(curl -sSL --retry 3 "${ADO_API}/build/builds/${BUILD_ID}?api-version=7.1" | jq -r '.result // empty')
echo "ADO build ${BUILD_ID} result: '${RESULT}'"
if [ "${RESULT}" != "failed" ]; then
echo "::warning::ADO build ${BUILD_ID} did not fail (result='${RESULT}'); nothing to analyze."
emit_none
…alidation

- Shared body + agent doc: finish the multi-leg migration. The delegation
  prompt now forwards GH_AW_BINLOG_LIST / GH_AW_BINLOG_DIR and instructs the
  sub-agent to query every leg's binlog (not just the first); the obsolete
  "/data/build.binlog" single-file mount wording is replaced with the
  /data/binlogs directory model.
- Agent doc: the workspace is NOT a checkout of the failing PR (check_run /
  slash command / dispatch run on the default branch), so read PR source
  through the GitHub API at GH_AW_PR_HEAD_SHA rather than GH_AW_WORKSPACE.
- Auto workflow (dispatch): validate the supplied build is definition 209
  AND its sourceBranch is refs/pull/<PR>/merge (plus result=failed) before
  analyzing, so a build-id/PR mismatch can't post to the wrong PR.
- Command workflow: select the PR's latest *completed* build and require its
  result to be `failed`, so a PR that has since gone green (or been
  force-pushed to passing) is not re-analyzed with a stale binlog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 09:50
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Addressed this round in cc45973:

  • Multi-binlog context not propagated (shared.md, agent.md): the delegation prompt and the agent's intro now use the /data/binlogs directory model and forward GH_AW_BINLOG_LIST / GH_AW_BINLOG_DIR, instructing the sub-agent to query every leg's binlog (not just the first). The obsolete /data/build.binlog single-file mount wording is gone.
  • Workspace on default branch (build-failure-analysis.md): because check_run / slash command / dispatch leave the workspace off the failing PR, the agent doc now instructs reading PR source through the GitHub API at GH_AW_PR_HEAD_SHA (not GH_AW_WORKSPACE), so source context, NuGet remediation, permalinks, and suggestions are based on the PR head rather than main.
  • Stale build after success/force-push (build-failure-analysis-command.md): the slash command now selects the PR's latest completed build and requires result == failed; if the PR has since gone green it emits nothing (no pairing of stale binlog errors with current source).
  • Dispatch build/PR mismatch (build-failure-analysis.md): the dispatch path now verifies the build is definition 209, result == failed, and sourceBranch == refs/pull/<PR>/merge before downloading artifacts.

Recompiled with gh aw v0.82.8 (matching the repo, LF) — 0 errors / 0 warnings.

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 3 comments.

Comment on lines +18 to +20
# errors. Reusing the binlogs avoids a duplicate build and removes any fork-PR
# code-execution risk: this workflow only downloads artifacts (data), it never
# checks out or runs PR code.
rm -rf /tmp/ax /tmp/a.zip
mkdir -p /tmp/ax
curl -sSL --retry 3 "${url}" -o /tmp/a.zip || continue
unzip -q /tmp/a.zip -d /tmp/ax || continue
rm -rf /tmp/ax /tmp/a.zip
mkdir -p /tmp/ax
curl -sSL --retry 3 "${url}" -o /tmp/a.zip || continue
unzip -q /tmp/a.zip -d /tmp/ax || continue
…space docs

- Security: extract Azure DevOps artifacts with `unzip -j -o '*.binlog'`
  (junk paths, binlogs only) so a malicious zip entry can't zip-slip outside
  the destination. Artifacts come from PR builds and are untrusted. (both
  workflows)
- Auto workflow: set `roles: all` so advisory analysis runs for every failing
  PR — including external contributors' PRs. gh-aw's default author gate
  would otherwise skip them, and on `check_run` the actor is the pipeline app
  anyway. Safe: read-only binlog + advisory comments, no PR code execution.
- Delegation: launch the sub-agent with
  `agent_type: "build-failure-analyst"` so the custom agent definition is
  loaded (was `general-purpose`).
- Docs: clarify the workspace may or may not be a PR checkout depending on
  trigger; the GitHub API at GH_AW_PR_HEAD_SHA is the source of truth. Header
  comment reworded: PR code is never built/executed (gh-aw still does a sparse
  checkout of the base repo's own .github config).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 10:03
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Addressed this round in 03c5b61 (and folded in the parallel hardening from the arcade port):

  • Zip-slip hardening (both workflows): artifacts are now extracted with unzip -j -o '*.binlog' -d … — paths junked, only *.binlog entries — so a malicious/relative entry in a PR-produced artifact can't write outside the destination directory.
  • "never checks out PR code" wording: reworded to "PR code is never built or executed", since gh-aw still performs a sparse checkout of the base repo's own .github config. No PR code is fetched, built, or run.
  • Also applied the matching arcade fixes: agent_type: "build-failure-analyst" (load the custom agent, not general-purpose), roles: all on the auto workflow so advisory analysis runs for every failing PR including external contributors, and the GH_AW_WORKSPACE doc clarification (workspace may/may not be the PR depending on trigger; GitHub API at GH_AW_PR_HEAD_SHA is the source of truth).

Recompiled with gh aw v0.82.8 (matching the repo, LF) — 0 errors / 0 warnings.

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 5 comments.

ADO_BUILD_UI: "https://dev.azure.com/dnceng-public/public/_build/results"
# microsoft.testfx pipeline definition id in dnceng-public/public.
ADO_BUILD_DEFINITION_ID: "209"
PR_NUMBER: ${{ github.event.issue.number }}
Comment on lines +209 to +213
# Extract ONLY `*.binlog` entries with paths junked (`-j`) so a
# malicious/relative artifact entry (zip-slip, `../…`) can never
# write outside /tmp/ax. Artifacts originate from PR builds, so
# treat them as untrusted input.
unzip -j -o /tmp/a.zip '*.binlog' -d /tmp/ax >/dev/null 2>&1 || continue
Comment on lines +139 to +143
# Extract ONLY `*.binlog` entries with paths junked (`-j`) so a
# malicious/relative artifact entry (zip-slip, `../…`) can never
# write outside /tmp/ax. Artifacts originate from PR builds, so
# treat them as untrusted input.
unzip -j -o /tmp/a.zip '*.binlog' -d /tmp/ax >/dev/null 2>&1 || continue
Comment on lines +113 to +117
For each root cause, identify the **smallest set of files** that need to change. Depending on the trigger the runner workspace may not be a checkout of the failing PR (the `check_run` / dispatch paths run on the default branch or an unrelated ref), so treat the **GitHub API / `github` MCP tool at the `GH_AW_PR_HEAD_SHA` ref** as the source of truth for PR source (convert the absolute compiler paths in the binlog to repo-relative paths first) rather than reading the local workspace.

- For Roslyn / C# errors: read 6 lines above and 10 lines below the reported line.
- For MSBuild errors: read the offending element and the surrounding `<PropertyGroup>` / `<ItemGroup>` / `<Target>`.
- For NuGet failures: read the `.csproj`, `Directory.Packages.props`, and `eng/Versions.props` rows mentioning the package. Then run `NuGet.Mcp.Server` (the global tool's command name — not `dotnet NuGet.Mcp.Server`) to get a concrete resolution plan.
- For NuGet failures: read the `.csproj`, `Directory.Packages.props`, and `eng/Versions.props` rows mentioning the package. Then run `NuGet.Mcp.Server` (the tool's command name on `PATH` — not `dotnet NuGet.Mcp.Server`) to get a concrete resolution plan.
# (sourceBranch refs/pull/<PR>/merge). Build id and PR number
# are independent dispatch inputs, so guard against a mismatch
# that would post one build's analysis onto the wrong PR.
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
… tool, fix command no-op

- Command workflow no-op fix: resolve the PR number from `aw_context`
  (the compiled slash-command entrypoint is workflow_dispatch-based, so
  github.event.issue.number is empty) — previously `/analyze-build-failure`
  exited immediately.
- Build-metadata validation (definition 209, result=failed, sourceBranch ==
  refs/pull/<PR>/merge) now runs for BOTH check_run and workflow_dispatch, so
  a malformed/mismatched check payload can't download an unrelated build or
  post to the wrong PR.
- Zip-bomb / disk-exhaustion guards on untrusted PR artifacts: cap the
  compressed download (curl --max-filesize 500 MB), reject >2 GB uncompressed,
  and run extraction under `timeout 120` (both workflows), on top of the
  existing zip-slip `-j`.
- Remove the NuGet.Mcp.Server local tool: it needs a local checkout, but the
  reuse-binlog design never checks out the failing PR, so it would analyze the
  wrong dependency graph. The agent now diagnoses NU#### failures from the
  binlog errors plus the PR's package files read via the GitHub API at
  GH_AW_PR_HEAD_SHA.
- Header wording: the pipeline does not build/execute PR code; a checkout may
  occur for agent context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 11:15
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Addressed this round in cde7187:

  • /analyze-build-failure was a no-op (critical): the compiled slash-command entrypoint is workflow_dispatch-based, so github.event.issue.number is empty and the fetch step exited immediately. The PR number is now resolved from aw_context (fromJSON(...).item_number), matching the concurrency expression, and the lock is regenerated.
  • Build validation on all triggers: definition (209) / result==failed / sourceBranch==refs/pull/<PR>/merge now validated for check_run as well as dispatch, so a malformed/mismatched check payload can't download an unrelated build or post to the wrong PR.
  • Zip-bomb / disk guards: untrusted PR artifacts capped on compressed download (--max-filesize 500 MB), rejected if >2 GB uncompressed, and extracted under timeout 120, on top of the zip-slip -j (both workflows).
  • NuGet.Mcp.Server removed: it requires a local checkout, but the reuse-binlog design never checks out the failing PR (so it would analyze the wrong dependency graph — the concern you raised). The agent now diagnoses NU#### failures from the binlog + the PR's package files via the GitHub API at GH_AW_PR_HEAD_SHA.
  • Header wording: the pipeline does not build/execute PR code; a checkout may occur for agent context.

Recompiled with gh aw v0.82.8 (matching the repo, LF) — 0 errors / 0 warnings.

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 6 comments.

Comment on lines +213 to +216
UNCOMP=$(unzip -l /tmp/a.zip 2>/dev/null | tail -1 | awk '{print $1}')
if [ -n "${UNCOMP}" ] && [ "${UNCOMP}" -gt "${MAX_UNZIP_BYTES}" ]; then
echo "::warning::Skipping ${name}: uncompressed size ${UNCOMP} exceeds ${MAX_UNZIP_BYTES} guard (possible zip bomb)."; continue
fi
Comment on lines +142 to +145
UNCOMP=$(unzip -l /tmp/a.zip 2>/dev/null | tail -1 | awk '{print $1}')
if [ -n "${UNCOMP}" ] && [ "${UNCOMP}" -gt "${MAX_UNZIP_BYTES}" ]; then
echo "::warning::Skipping ${name}: uncompressed size ${UNCOMP} exceeds ${MAX_UNZIP_BYTES} guard (possible zip bomb)."; continue
fi
| `GH_AW_BUILD_OUTCOME` | Always `failure` when this agent runs — the workflow only activates after the Azure DevOps `microsoft.testfx` build failed. |
| `GH_AW_PR_NUMBER` | Pull request number to post the analysis on. Pass it explicitly on every `add_comment` / `create_pull_request_review_comment` call (the workflows use `target: "*"`). |
| `GH_AW_PR_HEAD_SHA` | Commit SHA at the PR head. Use for permalinks **and** as the ref when reading source files (see below). |
| `GH_AW_WORKSPACE` | `$GITHUB_WORKSPACE`. The workspace **may or may not** be a checkout of the failing PR depending on the trigger (the slash-command path checks out the PR branch; the `check_run` / dispatch paths do not). Do not rely on it for PR source — the GitHub API at `GH_AW_PR_HEAD_SHA` is the source of truth (see Step 4). |
Comment on lines +112 to +115
builds_json=$(curl -sSL --retry 3 \
"${ADO_API}/build/builds?definitions=${ADO_BUILD_DEFINITION_ID}&branchName=refs/pull/${PR_NUMBER}/merge&statusFilter=completed&queryOrder=finishTimeDescending&\$top=1&api-version=7.1")
BUILD_ID=$(printf '%s' "${builds_json}" | jq -r '.value // [] | .[0].id // empty')
BUILD_RESULT=$(printf '%s' "${builds_json}" | jq -r '.value // [] | .[0].result // empty')
Comment on lines +163 to +165
PR_JSON=$(gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}" 2>/dev/null)
BASE_REF=$(printf '%s' "${PR_JSON}" | jq -r '.base.ref // empty')
[ -z "${HEAD_SHA}" ] && HEAD_SHA=$(printf '%s' "${PR_JSON}" | jq -r '.head.sha // empty')
Comment thread .github/agents/build-failure-analyst.agent.md
If `unzip -l` produces unexpected output (corrupt zip / different format),
`UNCOMP` could be non-numeric and the `[ "$UNCOMP" -gt … ]` test would error
and be treated as false — silently bypassing the zip-bomb size guard. Now
require `UNCOMP` to match ^[0-9]+$ and skip the artifact (fail safe) when it
is not parseable. Applied to both workflows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 11:25
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Proactively hardened the same zip-bomb guard here (e80c098), matching the arcade review: UNCOMP is now validated as a plain integer (^[0-9]+$) before the -gt comparison, and the artifact is skipped (fail safe) if unzip -l output is unparseable — otherwise a non-numeric value would error the test and silently bypass the size guard. Both workflows; recompiled with gh aw v0.82.8 — 0 errors / 0 warnings.

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 3 comments.

Comment on lines +112 to +115
builds_json=$(curl -sSL --retry 3 \
"${ADO_API}/build/builds?definitions=${ADO_BUILD_DEFINITION_ID}&branchName=refs/pull/${PR_NUMBER}/merge&statusFilter=completed&queryOrder=finishTimeDescending&\$top=1&api-version=7.1")
BUILD_ID=$(printf '%s' "${builds_json}" | jq -r '.value // [] | .[0].id // empty')
BUILD_RESULT=$(printf '%s' "${builds_json}" | jq -r '.value // [] | .[0].result // empty')
Comment on lines +199 to +203
# Guards for untrusted PR-produced archives: cap the compressed
# download and the reported uncompressed size, and bound extraction
# time, so a zip bomb / oversized artifact can't exhaust the runner.
MAX_ZIP_BYTES=524288000 # 500 MB compressed per artifact
MAX_UNZIP_BYTES=2147483648 # 2 GB uncompressed per artifact
Comment on lines +128 to +132
# Guards for untrusted PR-produced archives: cap the compressed
# download and the reported uncompressed size, and bound extraction
# time, so a zip bomb / oversized artifact can't exhaust the runner.
MAX_ZIP_BYTES=524288000 # 500 MB compressed per artifact
MAX_UNZIP_BYTES=2147483648 # 2 GB uncompressed per artifact
…west-build selection

- Revision consistency: derive the head SHA from the analyzed build's
  `triggerInfo["pr.sourceSha"]`, not the PR's current head, so permalinks and
  inline-suggestion positions match the binlog rather than pairing stale
  errors with newer source.
- Command workflow: query the newest build regardless of status
  (queueTimeDescending) and require it completed AND failed, so a
  queued/running build right after a force-push isn't skipped in favour of an
  older completed failure.
- Cumulative uncompressed budget (4 GB) across all build legs, on top of the
  per-artifact compressed/uncompressed caps, so many individually-small
  artifacts can't collectively exhaust the runner disk.
- Docs: the runner workspace is not a reliable PR checkout on any trigger;
  the GitHub API at GH_AW_PR_HEAD_SHA (the build's own revision) is the
  source of truth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 12:39
@YuliiaKovalova

Copy link
Copy Markdown
Contributor Author

Addressed this round in 4197834 (mirrors the arcade fixes):

  • Analysis anchored to the build's revision (triggerInfo["pr.sourceSha"]), so permalinks/suggestions match the analyzed binlog rather than the PR's current head — fixes the stale-binlog-vs-current-source concern.
  • Command workflow selects the newest build regardless of status (queueTimeDescending) and requires completed + failed, so a queued/running build after a force-push isn't skipped in favour of an older completed failure.
  • Cumulative 4 GB uncompressed budget across all build legs, on top of the per-artifact caps, closing the multi-artifact disk-exhaustion gap.
  • Numeric-size guard already validated UNCOMP as ^[0-9]+$ before comparison (e80c098) — non-numeric totals are skipped fail-safe.
  • Workspace docs clarified: not a reliable PR checkout on any trigger; GitHub API at GH_AW_PR_HEAD_SHA is the source of truth.

Also updated the PR description: this PR removes the NuGet.Mcp.Server local tool (it needs a local checkout the reuse-binlog design never does) rather than folding in #9982's invocation — #9982 can be closed once this lands.

Recompiled with gh aw v0.82.8 (LF) — 0 errors / 0 warnings.

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 5 comments.

Comment on lines +254 to +255
cp "${bl}" "${dest}.binlog"
count=$((count + 1))
Comment on lines +185 to +186
cp "${bl}" "${dest}.binlog"
count=$((count + 1))
Comment on lines +6 to +7
NOT rebuild: it finds the PR's most recent **failed** Azure Pipelines
`microsoft.testfx` build, downloads the binary logs that build already produced
Comment on lines +263 to +269
{
echo "binlog-found=true"
echo "pr-number=${PR_NUMBER}"
echo "pr-head-sha=${HEAD_SHA}"
echo "ado-build-id=${BUILD_ID}"
echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}"
} >> "$GITHUB_OUTPUT"
Comment on lines +194 to +200
{
echo "binlog-found=true"
echo "pr-number=${PR_NUMBER}"
echo "pr-head-sha=${HEAD_SHA}"
echo "ado-build-id=${BUILD_ID}"
echo "ado-build-url=${ADO_BUILD_UI}?buildId=${BUILD_ID}"
} >> "$GITHUB_OUTPUT"
gh-aw emits a separate safe_outputs job (granted pull-requests: write +
issues: write) for all PR writes; the agent job stays read-only. Add a
frontmatter comment explaining this split-permission model so the
least-privilege pull-requests: read default isn't repeatedly mistaken for
a misconfiguration. No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a

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 7 out of 7 changed files in this pull request and generated 2 comments.

Comment on lines +298 to +299
{
echo "binlog-found=true"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Added a re-check of the PR head immediately before emitting binlog-found=true, after the download/extract loop: LATEST_HEAD=$(gh api repos/${GH_AW_REPO}/pulls/${PR_NUMBER} --jq .head.sha); if it is empty or differs from the head validated earlier, the job fails closed (emit_none) so a force-push during the (potentially multi-minute) download can no longer activate the agent against a new diff. Applied to both workflows.

Comment on lines +233 to +234
{
echo "binlog-found=true"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Added a re-check of the PR head immediately before emitting binlog-found=true, after the download/extract loop: LATEST_HEAD=$(gh api repos/${GH_AW_REPO}/pulls/${PR_NUMBER} --jq .head.sha); if it is empty or differs from the head validated earlier, the job fails closed (emit_none) so a force-push during the (potentially multi-minute) download can no longer activate the agent against a new diff. Applied to both workflows.

…line failures

When every build leg compiles cleanly (no errors, no failed-target/process
evidence), the pipeline failure is a non-build failure (test/Helix/publishing)
and is out of scope. The agent now calls noop and posts nothing in that case
instead of a 'compiled cleanly' summary, so it only comments on genuine build
failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 15:25
…ify prompt

- Read the Logs_Build_* artifact names into a bash array (mapfile) and iterate
  with "${names[@]}" so a name containing whitespace can't be word-split.
- Re-read the PR head immediately before emitting binlog-found=true and fail
  closed if it moved or can't be resolved: the download/extract loop can take
  minutes, and a force-push in that window would otherwise post stale-build
  suggestions against the new diff.
- Reword the shared prompt: the agent produces the review via safe-output
  tools (a later safe_outputs job writes it), not "directly, in this job".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a

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 7 out of 7 changed files in this pull request and generated 6 comments.

Comment on lines +263 to +267
if ! printf '%s' "${UNCOMP}" | grep -qE '^[0-9]+$'; then
echo "::warning::Skipping ${name}: could not determine uncompressed size (unparseable unzip output)."; continue
fi
if [ "${UNCOMP}" -gt "${MAX_UNZIP_BYTES}" ]; then
echo "::warning::Skipping ${name}: uncompressed size ${UNCOMP} exceeds ${MAX_UNZIP_BYTES} guard (possible zip bomb)."; continue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Before the numeric comparison, the guard now rejects on decimal length: if [ "${#UNCOMP}" -gt "${#MAX_UNZIP_BYTES}" ] — any value with more digits than the limit (10) is unambiguously oversized, so a ~20-digit ZIP64 size is skipped before it can reach -gt/$((...)) and overflow Bash's signed 64-bit range. After this check UNCOMP is <= 10 digits and fits safely in the integer range used below (including the cumulative budget). Applied to both workflows and recompiled.

Comment on lines +198 to +202
if ! printf '%s' "${UNCOMP}" | grep -qE '^[0-9]+$'; then
echo "::warning::Skipping ${name}: could not determine uncompressed size (unparseable unzip output)."; continue
fi
if [ "${UNCOMP}" -gt "${MAX_UNZIP_BYTES}" ]; then
echo "::warning::Skipping ${name}: uncompressed size ${UNCOMP} exceeds ${MAX_UNZIP_BYTES} guard (possible zip bomb)."; continue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Before the numeric comparison, the guard now rejects on decimal length: if [ "${#UNCOMP}" -gt "${#MAX_UNZIP_BYTES}" ] — any value with more digits than the limit (10) is unambiguously oversized, so a ~20-digit ZIP64 size is skipped before it can reach -gt/$((...)) and overflow Bash's signed 64-bit range. After this check UNCOMP is <= 10 digits and fits safely in the integer range used below (including the cumulative budget). Applied to both workflows and recompiled.

TOTAL_BYTES=0
mkdir -p /tmp/binlogs
count=0
for name in ${names}; do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed. The loop now reads the names into an array (mapfile -t names < <(... jq ...)) and iterates for name in "${names[@]}", so a whitespace-containing name is no longer split. This comment predates that change (commit d041cc1).

TOTAL_BYTES=0
mkdir -p /tmp/binlogs
count=0
for name in ${names}; do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed. The loop now reads the names into an array (mapfile -t names < <(... jq ...)) and iterates for name in "${names[@]}", so a whitespace-containing name is no longer split. This comment predates that change (commit d041cc1).

> **Note:** The NuGet MCP server operates on the workspace's actual project files and NuGet configuration. It has access to the repository's NuGet feeds and can resolve transitive dependency chains that are impossible to reason about from error messages alone.
Notes:
- `NU1605` (downgrade): find where the lower version is pinned and raise it to satisfy the transitive requirement named in the error.
- `NU1102` / `NU1100` (not found): confirm the exact package **and version** the error names, then verify availability before proposing a remedy. If that version is published on nuget.org but absent from the repo's configured feeds, feed mirroring is the likely cause; if it is **not** on nuget.org either, treat it as a typo or an unpublished/incorrect version rather than a mirroring gap. Do not assume mirroring without evidence that the version exists upstream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Reworded the NU1102/NU1100 guidance: the agent has no network/NuGet tool, so it must base conclusions only on the binlog's feed/version evidence and the PR's package files, and explicitly defer upstream-availability verification to a maintainer (or a local restore) rather than asserting nuget.org state it cannot check. No more unverifiable mirroring claims.

| `GH_AW_BINLOG_HOST_PATH` | URL of the originating Azure DevOps build (`https://dev.azure.com/dnceng-public/public/_build/results?buildId=…`). Use only for permalinks / human-facing references — read the binlog data via MCP. |
| `GH_AW_BUILD_OUTCOME` | Always `failure` when this agent runs — the workflow only activates after the Azure DevOps `microsoft.testfx` build failed. |
| `GH_AW_PR_NUMBER` | Pull request number to post the analysis on. Pass it explicitly on every `add_comment` / `create_pull_request_review_comment` call (the workflows use `target: "*"`). |
| `GH_AW_PR_HEAD_SHA` | Commit SHA the analysis targets. The workflow guarantees this equals **both** the analyzed build's revision (`triggerInfo["pr.sourceSha"]`) **and** the PR's current head — it skips stale builds where they differ. Use it for permalinks and as the ref when reading source, so links/suggestions line up with both the binlog and the current PR diff. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Added an agent-level re-check: before its first safe-output call, the agent re-reads the current head of GH_AW_PR_NUMBER via the GitHub API (get_pull_request -> head.sha) and noops if it cannot be read or no longer equals GH_AW_PR_HEAD_SHA. This closes the force-push window that remains after the fetch job's point-in-time check (the fetch job also re-checks just before activation). The GH_AW_PR_HEAD_SHA doc row now states it is a point-in-time guarantee requiring this re-verification.

Copilot AI review requested due to automatic review settings July 16, 2026 15:35

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 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines +110 to +112
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, post **exactly one** summary comment via `add_comment` (targeting the pull request `GH_AW_PR_NUMBER`). Mark it with the HTML marker `<!-- build-failure-analysis -->` 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in the previous push (commit 874bff6): the agent now re-reads the PR head immediately before its first safe-output call and noops if it changed. This comment predates that change.

> **Note:** The NuGet MCP server operates on the workspace's actual project files and NuGet configuration. It has access to the repository's NuGet feeds and can resolve transitive dependency chains that are impossible to reason about from error messages alone.
Notes:
- `NU1605` (downgrade): find where the lower version is pinned and raise it to satisfy the transitive requirement named in the error.
- `NU1102` / `NU1100` (not found): confirm the exact package **and version** the error names, then verify availability before proposing a remedy. If that version is published on nuget.org but absent from the repo's configured feeds, feed mirroring is the likely cause; if it is **not** on nuget.org either, treat it as a typo or an unpublished/incorrect version rather than a mirroring gap. Do not assume mirroring without evidence that the version exists upstream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in the previous push (commit 874bff6): the NU1102/NU1100 guidance now bases conclusions only on binlog feed/version evidence + PR package files and requires deferring upstream-availability to a maintainer, since the agent has no network/NuGet tool. This comment predates that change.

Comment on lines +164 to +165
if [ -z "${PR_NUMBER}" ] && [ -n "${HEAD_SHA}" ]; then
PR_NUMBER=$(gh api "repos/${GH_AW_REPO}/commits/${HEAD_SHA}/pulls" --jq '.[0].number // empty' 2>/dev/null)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The PR number is now taken from the build's own sourceBranch (refs/pull/<n>/merge), which names the PR unambiguously, instead of the first entry of the commit->PRs association. The build metadata is fetched once up front and reused by the step-4 validation, so the fork fallback no longer depends on .[0] ordering and won't skip the real PR.

…ead before posting

- Reject artifact uncompressed sizes by decimal length before the numeric
  comparison. A ZIP64 size can have ~20 digits, exceeding Bash's signed
  64-bit range, where `-gt` errors out and (under `set +e`) an oversized
  archive could bypass the disk-exhaustion guard.
- NU1102/NU1100 guidance: the agent has no network/NuGet tool, so it must
  base conclusions on binlog feed/version evidence and PR package files and
  defer upstream-availability checks to a maintainer instead of asserting
  nuget.org state it cannot verify.
- Agent re-reads the PR head via the GitHub API before its first safe-output
  call and noops if it changed, closing the force-push window that remains
  after the fetch job's point-in-time revision check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 15:45

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 7 out of 7 changed files in this pull request and generated 5 comments.

Comment on lines +303 to +305
echo "Extracted ${count} binlog(s) into /tmp/binlogs:"
ls -la /tmp/binlogs || true
[ "${count}" -eq 0 ] && { echo "::warning::No *.binlog found in any Logs_Build_* artifact of build ${BUILD_ID}."; emit_none; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The fetch now tracks how many Logs_Build_* legs produced a usable binlog (staged_legs) and fails closed (emit_none) when that is fewer than the number of artifacts, so a partial set can no longer activate the agent. This prevents the agent from treating the retrieved legs as the whole build and mis-classifying a missing leg's failure as a clean/non-build result. (Chose the reviewer's "fail closed on a partial set" option for simplicity/safety; a later build/check re-triggers.) Applied to both workflows.

Comment on lines +238 to +240
echo "Extracted ${count} binlog(s) into /tmp/binlogs:"
ls -la /tmp/binlogs || true
[ "${count}" -eq 0 ] && { echo "::warning::No *.binlog found in any Logs_Build_* artifact of build ${BUILD_ID}."; emit_none; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The fetch now tracks how many Logs_Build_* legs produced a usable binlog (staged_legs) and fails closed (emit_none) when that is fewer than the number of artifacts, so a partial set can no longer activate the agent. This prevents the agent from treating the retrieved legs as the whole build and mis-classifying a missing leg's failure as a clean/non-build result. (Chose the reviewer's "fail closed on a partial set" option for simplicity/safety; a later build/check re-triggers.) Applied to both workflows.

Always post **exactly one** summary comment via `add_comment`. Mark it with the HTML marker `<!-- build-failure-analysis -->` 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.
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 the current head of PR `GH_AW_PR_NUMBER` via the GitHub API (`get_pull_request` → `head.sha`). If it cannot be read, or no longer equals `GH_AW_PR_HEAD_SHA`, the PR moved (e.g. a force-push) while you were downloading/analyzing — `noop` with a short reason and stop, because your inline suggestions carry no `commit_id` and would land on the wrong lines of the new diff. Otherwise post **exactly one** summary comment via `add_comment` (targeting the pull request `GH_AW_PR_NUMBER`). Mark it with the HTML marker `<!-- build-failure-analysis -->` 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The playbook no longer names get_pull_request; it refers to the github MCP server's pull-request "get"/read operation generically (pull_requests read tool), so it matches the actual tool surface (e.g. pull_request_read with method: get) rather than a nonexistent name.

Comment on lines +164 to +166
if [ -z "${PR_NUMBER}" ] && [ -n "${HEAD_SHA}" ]; then
PR_NUMBER=$(gh api "repos/${GH_AW_REPO}/commits/${HEAD_SHA}/pulls" --jq '.[0].number // empty' 2>/dev/null)
fi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The PR number is now taken from the build's own sourceBranch (refs/pull/<n>/merge), which names the PR unambiguously, instead of the first entry of the commit->PRs association. The build metadata is fetched once up front and reused by the step-4 validation, so the fork fallback no longer depends on .[0] ordering and won't skip the real PR.

Comment on lines +56 to +58
The failed Azure DevOps build publishes **one binlog per build leg** (e.g. Linux Debug, Windows Release, macOS Debug). They are mounted read-only under `GH_AW_BINLOG_DIR` (`/data/binlogs`) and enumerated, one path per line, in `GH_AW_BINLOG_LIST`. A build failure usually surfaces in only one leg, and some pipeline failures (e.g. test-only / Helix failures) leave every build binlog clean — so triage across all of them:

1. `binlog_overview { binlog_file: "$GH_AW_BINLOG_PATH" }` — high-level summary (build configuration, projects, targets executed, totals). Use it to confirm what was built and where it broke.
2. `binlog_errors { binlog_file: "$GH_AW_BINLOG_PATH" }` — primary input: every error with `{ severity, code, message, file, line, column, project }`. If empty, drop to Step 6 with a "build failed but no MSBuild errors captured" comment.
3. `binlog_warnings { binlog_file: "$GH_AW_BINLOG_PATH", top: 10 }` — useful when the failure is caused by a `WarnAsError` promotion.
1. For **each** path in `GH_AW_BINLOG_LIST`, call `binlog_errors { binlog_file: "<path>" }`. Concentrate your analysis on the leg(s) that actually report errors (each error has `{ severity, code, message, file, line, column, project }`).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Added an explicit trust-boundary note to the agent playbook: binlog contents (MSBuild properties, error text, paths) and PR source are untrusted data, not instructions — the agent must never obey directives embedded in them, never let them change the task, and always target safe outputs at GH_AW_PR_NUMBER (never a PR/repo/user named inside a log). This counters injection attempts via crafted error text under the wildcard target.

…anch, fail closed on partial legs; agent trust-boundary + tool-name fixes

- Sanitize the PR-controlled ADO artifact name before using it in any on-disk
  path (tr to [A-Za-z0-9._-]) so a crafted name can't traverse out of
  /tmp/binlogs; the original name is still used for the artifacts_json lookup.
- Resolve the PR number from the build's own sourceBranch (refs/pull/<n>/merge)
  instead of the first entry of the commit->PRs association, which can return
  several PRs in an unspecified order and made the run skip the real fork PR.
- Fail closed when only some Logs_Build_* legs produced a usable binlog, so
  the agent can't mistake a partial set for the whole build and call a missing
  leg's failure "clean".
- Agent playbook: add an explicit trust boundary (binlog/source strings are
  untrusted data, not instructions; always target GH_AW_PR_NUMBER), and refer
  to the github MCP pull-request read operation generically instead of the
  nonexistent get_pull_request tool.
- Clarify the shared prompt (github MCP is read-only; writes go via
  safeoutputs) and the checkout note in the workflow header.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 16:10

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 7 out of 7 changed files in this pull request and generated 4 comments.

# `^Logs_Build_` filter only anchors the prefix, so sanitize it
# before using it in any on-disk path (guards against `/` or `..`
# traversal); keep the original `name` for the artifacts_json lookup.
safe_name=$(printf '%s' "${name}" | tr -c 'A-Za-z0-9._-' '_')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Staging is now collision-safe: every binlog is copied to a destination uniquely prefixed with the artifact index and a per-file counter (/tmp/binlogs/${ai}_${i}_${safe_name}.binlog), so two artifacts that sanitize to the same name can no longer overwrite each other while the counters still increment. staged_legs is now driven by a per-artifact leg_staged flag, so the completeness check reflects real staged legs. Applied to both workflows and recompiled.

Comment on lines +300 to +301
timeout 120 unzip -j -o /tmp/a.zip '*.binlog' -d /tmp/ax >/dev/null 2>&1 \
|| { echo "::warning::Skipping ${name}: extraction failed or timed out."; continue; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Dropped unzip -j (which flattened and overwrote same-basename entries): extraction now preserves in-archive paths into a fresh dir and each *.binlog found (recursive find) is staged under a unique ${ai}_${i}_... name, so two binlogs sharing a basename in different folders are both kept and counted. Zip-slip safety is preserved by refusing any archive whose entry paths are absolute or contain .. (pre-scan via unzip -Z1), and the on-disk destination never uses archive-relative paths. Applied to both workflows.

# `^Logs_Build_` filter only anchors the prefix, so sanitize it
# before using it in any on-disk path (guards against `/` or `..`
# traversal); keep the original `name` for the artifacts_json lookup.
safe_name=$(printf '%s' "${name}" | tr -c 'A-Za-z0-9._-' '_')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Staging is now collision-safe: every binlog is copied to a destination uniquely prefixed with the artifact index and a per-file counter (/tmp/binlogs/${ai}_${i}_${safe_name}.binlog), so two artifacts that sanitize to the same name can no longer overwrite each other while the counters still increment. staged_legs is now driven by a per-artifact leg_staged flag, so the completeness check reflects real staged legs. Applied to both workflows and recompiled.

Comment on lines +227 to +228
timeout 120 unzip -j -o /tmp/a.zip '*.binlog' -d /tmp/ax >/dev/null 2>&1 \
|| { echo "::warning::Skipping ${name}: extraction failed or timed out."; continue; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Dropped unzip -j (which flattened and overwrote same-basename entries): extraction now preserves in-archive paths into a fresh dir and each *.binlog found (recursive find) is staged under a unique ${ai}_${i}_... name, so two binlogs sharing a basename in different folders are both kept and counted. Zip-slip safety is preserved by refusing any archive whose entry paths are absolute or contain .. (pre-scan via unzip -Z1), and the on-disk destination never uses archive-relative paths. Applied to both workflows.

…ing collision-safe

- Reference workflow_dispatch inputs with bracket notation
  (inputs['ado-build-id'], inputs['pr-number']) in the concurrency group and
  env, the documented-correct form for hyphenated input names.
- Stage every extracted binlog under a unique destination prefixed by the
  artifact index and a per-file counter, and extract preserving in-archive
  paths (dropping `unzip -j`) so neither a cross-artifact sanitize collision
  (e.g. "a b" vs "a_b") nor two same-basename entries in one archive can
  silently overwrite another leg's binlog and let the completeness check pass
  with data missing. Archives with absolute or `..` entry paths are refused
  up front (defense-in-depth over unzip's own guard); the final destination
  never uses archive-relative paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 16:33

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 7 out of 7 changed files in this pull request and generated 4 comments.

PR_NUMBER="${DISPATCH_PR_NUMBER}"
HEAD_SHA=""
else
PR_NUMBER="${CHECK_PR_NUMBER}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. On the check_run path the PR number is now taken from the build's authoritative sourceBranch (refs/pull/<n>/merge, parsed into BUILD_PR_NUM) in preference to check_run.pull_requests[0]PR_NUMBER="${BUILD_PR_NUM:-${CHECK_PR_NUMBER}}". So a commit shared by multiple PRs no longer risks selecting the wrong one and then aborting at the sourceBranch check.

Comment on lines +215 to +216
BUILD_PR_SHA=$(printf '%s' "${build_json}" | jq -r '.triggerInfo["pr.sourceSha"] // empty')
CURRENT_HEAD=$(printf '%s' "${PR_JSON}" | jq -r '.head.sha // empty')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Verified empirically that Azure builds GitHub's refs/pull/<n>/merge, so build_json.sourceVersion equals the PR's merge_commit_sha at build time (checked build 1511668: sourceVersion == PR 17126 merge_commit_sha == 45b7c79b…). The workflow now captures the build's merge SHA and skips when it differs from the PR's current merge_commit_sha — in step 4 and again in the pre-activation recheck (which now re-reads head and merge in one call). This catches base-branch advances that leave the PR head unchanged but invalidate the analyzed merge. Applied to both workflows.

Comment on lines +146 to +148
build_json=$(curl -sSL --retry 3 "${ADO_API}/build/builds/${BUILD_ID}?api-version=7.1")
BUILD_PR_SHA=$(printf '%s' "${build_json}" | jq -r '.triggerInfo["pr.sourceSha"] // empty')
CURRENT_HEAD=$(gh api "repos/${GH_AW_REPO}/pulls/${PR_NUMBER}" --jq '.head.sha // empty' 2>/dev/null)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Verified empirically that Azure builds GitHub's refs/pull/<n>/merge, so build_json.sourceVersion equals the PR's merge_commit_sha at build time (checked build 1511668: sourceVersion == PR 17126 merge_commit_sha == 45b7c79b…). The workflow now captures the build's merge SHA and skips when it differs from the PR's current merge_commit_sha — in step 4 and again in the pre-activation recheck (which now re-reads head and merge in one call). This catches base-branch advances that leave the PR head unchanged but invalidate the analyzed merge. Applied to both workflows.

| `GH_AW_BINLOG_HOST_PATH` | URL of the originating Azure DevOps build (`https://dev.azure.com/dnceng-public/public/_build/results?buildId=…`). Use only for permalinks / human-facing references — read the binlog data via MCP. |
| `GH_AW_BUILD_OUTCOME` | Always `failure` when this agent runs — the workflow only activates after the Azure DevOps `microsoft.testfx` build failed. |
| `GH_AW_PR_NUMBER` | Pull request number to post the analysis on. Pass it explicitly on every `add_comment` / `create_pull_request_review_comment` call (the workflows use `target: "*"`). |
| `GH_AW_PR_HEAD_SHA` | Commit SHA the analysis targets. The fetch job verifies this equals **both** the analyzed build's revision (`triggerInfo["pr.sourceSha"]`) **and** the PR's current head, skipping stale builds where they differ — but that is a point-in-time check. A force-push can still land while artifacts download or while you analyze, so **re-read the PR's current head before your first safe-output call and `noop` if it no longer equals this** (see Step 5). Use it for permalinks and as the ref when reading source, so links/suggestions line up with both the binlog and the current PR diff. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The build's merge revision is now carried to the agent as GH_AW_PR_MERGE_SHA (= build_json.sourceVersion). Before its first safe output the agent re-reads the PR's head.sha and merge_commit_sha, and noops if either the head changed or the merge SHA moved from GH_AW_PR_MERGE_SHA (base advanced) — closing the base-advance window during analysis, not just PR-head force-pushes. The workflow's own pre-activation recheck does the same before activating.

…ch PR; fix budget accounting

- Guard against stale merges: Azure builds GitHub's refs/pull/<n>/merge, so
  build_json.sourceVersion equals the PR's merge_commit_sha at build time.
  Compare them in step 4 and again in the pre-activation recheck (and re-read
  both head and merge in one call), and carry the build merge SHA to the agent
  (GH_AW_PR_MERGE_SHA) so it re-verifies merge_commit_sha before its first safe
  output. This catches base-branch advances that leave the PR head unchanged
  but invalidate the analyzed merge — which the head-only check missed.
- On the check_run path, resolve the PR from the build's authoritative
  sourceBranch (refs/pull/<n>/merge) rather than check_run.pull_requests[0],
  whose order isn't guaranteed and can name a different PR sharing the commit.
- Only add an artifact's uncompressed size to the cumulative budget after it
  actually extracts, so a suspicious-path/extract-failure skip no longer
  consumes budget and wrongly forces later legs to be dropped as incomplete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 16:58

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 7 out of 7 changed files in this pull request and generated 1 comment.

Comment on lines +173 to +176
# Prefer the PR named by the build's own sourceBranch (authoritative:
# `refs/pull/<n>/merge`) over check_run.pull_requests[0], whose order
# isn't guaranteed and can name a different PR that shares the commit.
PR_NUMBER="${BUILD_PR_NUM:-${CHECK_PR_NUMBER}}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Updated the PR description: the check_run path now derives the PR number from the ADO build's authoritative refs/pull/<n>/merge source branch, falling back to check_run.pull_requests[0] only if unavailable — there is no commits/{sha}/pulls fallback anymore. The description now also notes the head+merge revision match requirement.

… perms and merge-SHA env

- Reject a resolved ADO build id or PR number that is not purely numeric
  before it feeds ADO/GitHub API URLs, so a malformed workflow_dispatch input
  or event/aw_context payload can't alter the request path/query ahead of the
  later definition/result validation.
- Clarify the command workflow's permissions comment: the slash-command
  acknowledgement reaction is emitted by gh-aw's own generated job, separate
  from the read-only agent job and from safe-outputs.
- List GH_AW_PR_MERGE_SHA in the shared prompt's Step 1 environment variables
  so the agent consistently reads it for the staleness recheck.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7b11dbc-6cf1-440c-a28a-dfcc5ddff35a
Copilot AI review requested due to automatic review settings July 16, 2026 17:20

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 7 out of 7 changed files in this pull request and generated 1 comment.


concurrency:
group: build-failure-analysis-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
group: build-failure-analysis-${{ github.event.check_run.pull_requests[0].number || inputs['pr-number'] || github.event.check_run.head_sha || github.run_id }}
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.

2 participants