Fix rerun review scanner dispatch (batch decisions + harden PR-not-found guard) - #35955
Conversation
…uard The rerun review scanner never dispatched any AzDO run because of two bugs: 1. gh-aw custom safe-output jobs are capped at one invocation per run, so every agent decision after the first was silently dropped. Replace the per-PR scalar tool inputs with a single `decisions` JSON array passed in one `trigger_rerun_review` call, and expand it in Get-AgentItems (Expand-RerunDecisionItems) while keeping back-compat for legacy scalar items. 2. Test-GhApiPrNotFound false-positived on proxy/auth error bodies that merely contained the words 'Not Found'/'Gone', causing open PRs to be skipped. Require an explicit HTTP 404/410 status, log the raw gh error, and re-probe before skipping (fail loud instead of silent skip). Adds Pester coverage for batched decision parsing and the guard regression. Recompiles rerun-review-scanner.lock.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35955Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35955" |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial multi-model review — PR #35955
Methodology: 3 independent reviewers (different model families) with adversarial consensus. Findings below survived cross-validation; single-reviewer claims were dropped or noted.
Overall: The core premise is sound and verified. A custom safe-outputs.jobs.<name> job runs once per workflow run, so batching all candidate decisions into one array-typed decisions input is the correct way to lift the one-dispatch-per-run cap (confirmed against gh-aw guidance). The .lock.yml was regenerated consistently with the .md source (single decisions string input, compiler v0.79.8). The not-found tightening is also correct: a genuine gh api 404 emits gh: Not Found (HTTP 404), which still matches \bHTTP\s+404\b, so dropping the bare Not Found|Gone alternation introduces no false-negative for real deletions. No security, data-corruption, or shared-file regressions found.
Two findings undercut the PR's own reliability goal and are worth addressing:
❌ Must fix
- Logic — recovered probe discarded (
Invoke-RerunReviewTrigger.ps1~L415, 3/3): the double-probethrows instead of using a successful recheck, turning a transient 404 into a dropped dispatch. See inline detail.
⚠️ Should fix
- Error Handling — malformed batch aborts all decisions (
Invoke-RerunReviewTrigger.ps1~L45, 3/3):ConvertFrom-Jsonthrows outside the per-item try/catch, so one baddecisionsstring kills every PR in the scan. See inline detail.
💡 Consider
- API Design — back-compat is unreachable (
Invoke-RerunReviewTrigger.ps1~L35, 2/3): the strict new schema prevents the legacy scalar shape from ever arriving; the fallback is dead code for new runs. - Testing — risky paths uncovered (
Invoke-RerunReviewTrigger.Tests.ps1, 2/3): the 5 new tests cover only happy paths. Add coverage for (a) a malformeddecisionsstring and (b) the double-probe transient-recovery case (extracting the fetch/confirm block into a small testable function would enable this). - Doc nit (PR description, non-blocking): the description cites "gh-aw v0.77.5 does not support
max: N," but the committed.lock.ymlwas compiled with v0.79.8. The rationale (custom job runs once per run) holds regardless of version.
Discarded (single reviewer, not corroborated): a "top-level fields dropped when an item carries both decisions and pr_number" data-loss concern — unreachable for the same reason the back-compat path is dead (the schema forbids a top-level pr_number).
Test coverage: new Pester tests are added for the happy-path expansion and the guard regex, but the two failure paths above are untested.
Prior reviews: none existing on this PR.
3 independent reviewers · adversarial consensus · COMMENT only (no approve / request-changes).
| foreach ($item in $Items) { | ||
| $rawDecisions = $item.PSObject.Properties['decisions'] | ||
| if (-not $rawDecisions -or $null -eq $rawDecisions.Value) { | ||
| if ($item.PSObject.Properties['pr_number']) { |
There was a problem hiding this comment.
💡 API Design — This legacy scalar pass-through is effectively dead code for new runs. The regenerated schema (rerun-review-scanner.md / .lock.yml) now declares only a single required decisions input and removed the per-field inputs, so gh-aw validates every tool call against that schema before this script runs — the agent can no longer emit an item that lacks decisions or carries a top-level pr_number. Consider either removing the fallback to simplify, or adding a comment that it only covers pre-existing replayed artifacts.
Flagged by: 2/3 reviewers
…very Address adversarial review findings: ❌ Must fix — Logic (L415): When the first gh api returns 404-like but the confirmation probe succeeds, reuse the valid result instead of throwing. This prevents transient proxy/rate-limit 404s from aborting dispatch for PRs that actually exist.⚠️ Should fix — Error Handling (L45): Wrap ConvertFrom-Json in try/catch so one malformed decisions string (trailing comma, unescaped quote, truncated JSON) emits ::warning:: and skips only that item instead of aborting the entire batch and dropping every PR's dispatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🤖 AI Agent — Addressed adversarial review findings (commit 3e11e6a): ✅ Fixed (2 findings):
💡 Not addressed:
Both must-fix and should-fix findings are resolved. Ready for re-review! 🔍 |
PureWeen
left a comment
There was a problem hiding this comment.
Follow-up verification — ❌ not ready to merge
Thanks for pushing 3e11e6a3 to address the earlier review (transient-404 recovery + malformed-JSON isolation). Both of those fixes are logically correct — but the commit also introduced a syntax error that breaks the entire script.
- ✅ Finding A (transient-404 recovery) — correctly reuses
$confirminstead of throwing. - ✅ Finding B (malformed-JSON isolation) —
ConvertFrom-Jsonis now wrapped in try/catch with skip+warning. - ❌ New blocker — brace imbalance at
Invoke-RerunReviewTrigger.ps1:433means the script no longer parses (The Try statement is missing its Catch or Finally block). The reviewed commit1f482be8parsed fine; the current head3e11e6a3does not. This crashes the dispatch job on every run and fails the PesterBeforeAllparse check. See the inline comment for the one-line fix.
Still open from the prior review (non-blocking):
- 💡 Testing — no tests added for the two new paths (malformed-JSON skip, transient-404 recovery). Worth adding now that the behavior exists.
- 💡 API Design — the legacy scalar back-compat in
Expand-RerunDecisionItemsis unreachable given the strict new schema.
Verdict: not mergeable until the brace imbalance is fixed and the Pester suite passes again. Once that's in, this looks good — the core batching + guard-hardening approach is sound.
Deterministic verification (PowerShell AST parse on both commits) · COMMENT only.
Address follow-up verification finding: ❌ Commit 3e11e6a introduced a brace imbalance at L433 that broke PowerShell parsing. The 'if (Test-GhApiPrNotFound)' else-branch was added but the outer 'if ($prFetch.ExitCode -ne 0)' block was never closed, causing 'Missing closing }' error. Fixed by adding the missing closing brace after the non-404 throw. Verified: pwsh Parser::ParseFile succeeds + all 32 Pester tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial re-review — round 2 (head dc4d87ab)
Methodology: 3 independent reviewers (different model families) with adversarial consensus. This round re-ran after the author's latest commits; one reviewer pass was repeated against a freshly-synced checkout to rule out a stale-worktree artifact.
✅ Prior blocking findings are all resolved
Verified against the current head, not the description:
- Logic — transient-404 recovery (was ❌): the double-probe block now reuses the recovered result (
$prFetch = $confirm) and falls through to normal processing; the ambiguous case throws (fails loud). Correct across all four branches. 3/3 confirm fixed. - Error Handling — malformed batch JSON (was ❌/
⚠️ ):ConvertFrom-JsoninExpand-RerunDecisionItemsis now wrapped intry/catch(warn +continueper item), so one bad payload no longer aborts every decision under$ErrorActionPreference='Stop'. 3/3 confirm fixed. - Syntax — brace imbalance (was ❌, my prior follow-up review): commit
dc4d87ab("Fix brace imbalance") repairs it; both the script and its Pester file parse cleanly (Parser::ParseFile→ 0 errors). 3/3 confirm fixed. - Not-found guard:
Test-GhApiPrNotFoundnow requires\bHTTP\s+(404|410)\b. A genuineghdeletion still emitsgh: Not Found (HTTP 404)(matches → no false-negative), while bare proxy/auth "Not Found" bodies no longer misclassify open PRs. Correct. - Schema consistency: the single required
decisionsstring input inrerun-review-scanner.mdmatches the compiler-generatedrerun-review-scanner.lock.ymland the script's per-object field reads. Consistent.
No security, data-loss, race-condition, or resource-leak issues found (temp files cleaned in finally on both probes; untrusted values routed through ConvertTo-SafeLogValue; pipeline_ref hardened via Normalize-PipelineRef).
Remaining items — all non-blocking 💡
- 💡 Testing (3/3) — no regression test for the malformed-JSON skip path (inline comment on the test file). This is the riskiest new branch and is currently uncovered.
- 💡 Observability / low Data-Loss (1/3, verified) — because every decision now travels in one batched
decisionsstring, if that string is malformed the script drops all items, logsNo trigger_rerun_review decisions found, and exits0— a green run that silently processed nothing. The only signal is the::warning::. Consider treating a batch-payload parse failure as a processing failure (non-zero exit) so a wholesale drop is visible in run status. (Raised by one reviewer; I confirmed the control flow.) - 💡 API Design (2/3) — the legacy scalar pass-through branch in
Expand-RerunDecisionItemsis effectively dead now thatdecisionsisrequired: true. Harmless and defensive; keeping it for back-compat is reasonable.
Test coverage
The 5 new Expand-RerunDecisionItems tests + the bare-text not-found regression test cover the happy paths and the guard well. The one gap is the malformed-JSON catch branch noted above.
Prior review status
My two earlier review comments on this PR (the round-1 logic/error-handling findings and the round-1.5 brace blocker) are now fully addressed by 3e11e6a3 and dc4d87ab. Nothing from those rounds remains open.
Verdict: Code is correct and safe to merge. The remaining items are optional polish — adding the malformed-JSON test is the most worthwhile follow-up.
| $result[0].pr_number | Should -Be '9' | ||
| } | ||
|
|
||
| It 'ignores empty or null decisions payloads' { |
There was a problem hiding this comment.
💡 Testing — The Expand-RerunDecisionItems suite covers JSON-string arrays, object arrays, multi-item aggregation, legacy scalar pass-through, and empty/null — but not the malformed-JSON catch path (the try/catch around ConvertFrom-Json in Invoke-RerunReviewTrigger.ps1). That catch is the only thing preventing a single bad decisions payload from aborting the entire batch under $ErrorActionPreference='Stop', so it's worth locking in with a regression test. Suggestion: feed a malformed decisions string (e.g. '[{"pr_number":"1"') alongside a valid item and assert the valid one still expands (and no throw).
Flagged by: 3/3 reviewers
Brings in merged PRs including #35955 (rerun scanner fixes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…plementing AzDO (#36080) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Problem The `/review rerun` scanner (`rerun-review-scanner`) dispatched **zero** AzDO reviews after #35955, and the few it should dispatch would target the wrong branch. Two independent bugs: ### Bug 1 — every dispatch aborted with a spurious 404 In the **gh-aw safe-output job**, the `gh` CLI returns `HTTP 404` for `repos/dotnet/maui/pulls/N` — even with `pull-requests: write` on a public repo. #35955 misread these as transient and "hardened" the not-found guard, so it now faithfully *confirms* the bogus 404 and skips every PR. Evidence it is not a permission/token problem: the `pre_activation` job (only `Metadata: read`) reads the same PRs fine, and the built-in `safe_outputs` job (octokit) succeeds in the same run. ### Bug 2 — custom review branches silently downgraded to `main` The candidate builder decided whose `/review -b <branch> -p <platform>` to trust using the comment's `author_association`. Under the Actions `GITHUB_TOKEN`, a maintainer whose org membership is private reads as `CONTRIBUTOR` (not `MEMBER`), so their command was dropped and the rerun fell back to the `main` pipeline. A live scan confirmed it dispatched four net11 (`feature/enhanced-reviewer`) PRs on `main`. ## Fix — do exactly what a maintainer `/review` does **Bug 1:** instead of re-implementing PR validation + OIDC + the AzDO trigger inside the safe job, the scanner now **dispatches the same `review-trigger.yml` workflow `/review` runs**, via `workflow_dispatch`. That workflow owns PR validation, the `s/agent-review-in-progress` lock, platform inference, OIDC, and the AzDO trigger. (`workflow_dispatch` via `GITHUB_TOKEN` always creates a run — it is exempt from Actions recursion-prevention.) - `Invoke-RerunReviewTrigger.ps1` is now pure: validate batched `decisions` against `candidates.json`, emit `actions.json`. No `gh`/AzDO/OIDC/lock/rate-limit I/O. - A `github-script` (octokit) step performs all GitHub writes — octokit works in the safe-job context where the `gh` CLI does not. `trigger` → `createWorkflowDispatch(review-trigger.yml,{pr_number,platform,pipeline_ref})` + 👍; `skip` → 👎 + remove the queue label. - Permissions: `+actions:write`, `−id-token:write`; dropped the `AZDO_TRIGGER_*` secrets from the job. **Bug 2:** authorize `/review` options by a **live collaborator-permission lookup** (`collaborators/<user>/permission` → write/maintain/admin) — the exact call `review-trigger.yml`'s auth step makes. It only needs `metadata: read` (every token has it) and reflects current access. **No new secret or permission.** - `Resolve-RerunEligibility.ps1`: add `Test-ReviewOptionLoginTrusted` (cached per login); `Get-LatestReviewCommandOptions` computes trust through it. Removed the `author_association` gate. - `Query-RerunReadyPRs.ps1`: drop the `author_association` helpers; pass `-Owner/-Repo`. ## Validation - **69 Pester tests pass** (36 dispatch + 33 resolver), incl. a regression test that an `author_association=NONE` command is still honored when the login has write access. - **Live, real (non-dry) scan**: the safe job validated all decisions with **no 404s** and octokit performed real `createWorkflowDispatch`, producing two `review-trigger.yml` runs that triggered real AzDO `maui-copilot` builds (HTTP 200, Run IDs 14459427 & 14459428). - **Real-data check for Bug 2**: PR #34564's history now resolves to `pipelineRef=feature/enhanced-reviewer` (author `kubaflo`, `author_association=CONTRIBUTOR`) instead of `main`, using the live permission lookup. ## Files - `.github/scripts/Invoke-RerunReviewTrigger.ps1`, `.github/scripts/Invoke-RerunReviewTrigger.Tests.ps1` - `.github/scripts/Resolve-RerunEligibility.ps1`, `.github/scripts/Resolve-RerunEligibility.Tests.ps1` - `.github/scripts/Query-RerunReadyPRs.ps1` - `.github/workflows/rerun-review-scanner.md` + recompiled `.lock.yml` - `.github/docs/agent-labels.md` --- 🔍 _This PR was created by an AI agent (GitHub Copilot CLI) on behalf of @kubaflo._ --------- Co-authored-by: kubaflo <kubaflo@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Problem
The
/review rerunscanner never dispatched a single AzDO run in production. Investigation across recent scheduled runs showed the agent correctly decidingdecision=triggerfor open PRs, yet every dispatch was aborted. Two independent bugs were responsible:Only one decision per run was ever processed. gh-aw custom safe-output jobs run exactly once per scan. The agent was instructed to call the
trigger_rerun_reviewtool once per candidate, so every call after the first was silently dropped by gh-aw's "max 1 item" enforcement. Even when several PRs were eligible, at most one reached the dispatch script.A false "PR not found" guard cancelled dispatches for open PRs.
Test-GhApiPrNotFoundclassified any gh error text containing "Not Found"/"Gone" (e.g. transient proxy/auth bodies) as a deleted PR, so the script loggedPR #N no longer exists; skipping stale decisionand bailed — for PRs that are demonstrably open.Fix
Batch all decisions into one tool call. The agent now calls
trigger_rerun_reviewexactly once per run, passing a singledecisionsJSON array with one object per candidate PR.Get-AgentItemsexpands the array (Expand-RerunDecisionItems) into individual decisions, with back-compat for the legacy scalar shape. This processes all eligible PRs in a scan instead of just one.Harden the not-found guard.
Test-GhApiPrNotFoundnow requires an explicitHTTP 404/410status. The fetch block logs the raw gh error and performs a second confirmation probe before skipping — it fails loud (throws) rather than silently cancelling a dispatch when the cause is ambiguous.Tests
Invoke-RerunReviewTrigger.Tests.ps1), including:decisionsparsing (JSON string array, object array, multi-item aggregation, legacy scalar pass-through, empty/null payloads).gh aw compile rerun-review-scannersucceeds (0 errors/warnings); the regenerated.lock.ymlis committed and embeds the newdecisionstool schema.Files
.github/scripts/Invoke-RerunReviewTrigger.ps1— batched decision expansion + hardened guard + raw-error logging/re-probe..github/scripts/Invoke-RerunReviewTrigger.Tests.ps1— new coverage..github/workflows/rerun-review-scanner.md— single batcheddecisionstool schema + updated agent prompt..github/workflows/rerun-review-scanner.lock.yml— recompiled.