Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c98bc94
Auto-apply s/agent-ready-for-rerun from the PR Review Queue workflow
Copilot Jul 3, 2026
152406b
Grant pull-requests: write so the auto-label step can apply the label
Copilot Jul 3, 2026
a2fc3e9
Allow scanner to trigger reruns from autonomous queue labels
Copilot Jul 4, 2026
fb233b4
Harden auto-rerun labeler: per-PR error isolation, arg-passing, fetch…
Copilot Jul 7, 2026
9b0a5aa
Auto-rerun labeler: single-source label metadata, fail-loud label fet…
Copilot Jul 7, 2026
f2cd01e
auto-rerun labeler: fail loud on gh api errors in activity/commit fet…
Copilot Jul 7, 2026
b60eadf
Merge remote-tracking branch 'origin/main' into feature/auto-rerun-la…
Copilot Jul 8, 2026
5c53948
auto-rerun labeler: fix -Limit help default, fail-loud label re-read,…
Copilot Jul 10, 2026
5b4acfe
Address review: verify-read regression, label-desc drift, redundant l…
Copilot Jul 16, 2026
8d72e52
Fix autonomous-rerun daily flap on scanner skip (last-declined checkp…
Copilot Jul 16, 2026
fe99a54
Doc: note head-SHA-differs as qualifying rerun activity
Copilot Jul 16, 2026
6b46aac
Surface gh api stderr in Get-IssueLabels on failure
Copilot Jul 16, 2026
0fa7212
Make -ApplyLabel post-apply verification failure-tolerant
Copilot Jul 16, 2026
2637ec3
Merge origin/main into feature/auto-rerun-labeler
kubaflo Jul 26, 2026
620a7d9
Merge main into feature/auto-rerun-labeler
Copilot Aug 6, 2026
5886164
Harden autonomous rerun candidate scanning
Aug 10, 2026
d4ed6b3
Make rerun tests hermetic in full suite
Aug 10, 2026
3a3c1ab
Make rerun decline lifecycle race-safe
Aug 10, 2026
1967d88
Address auto-rerun review feedback
Aug 12, 2026
9657be3
Handle concurrent rerun label creation
Aug 12, 2026
e60d765
Harden autonomous rerun queue handling
Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/scripts/Invoke-RerunReviewTrigger.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,17 @@ Describe 'Get-RerunActions' {
$result.Actions.Count | Should -Be 0
}

It 'refuses to trigger when the candidate has no rerun comment id' {
It 'triggers when the candidate has no proven current-cycle rerun comment id' {
$items = @(New-TestDecision -PRNumber '5' -Decision 'trigger' -ExpectedHeadSha 'x')
$candidates = @(New-TestCandidate -PRNumber 5 -HeadSha 'x' -RerunCommentId 0)

$result = Get-RerunActions -Items $items -Candidates $candidates

$result.HadFailure | Should -BeTrue
$result.Actions.Count | Should -Be 0
$result.HadFailure | Should -BeFalse
$result.Actions.Count | Should -Be 1
$result.Actions[0].prNumber | Should -Be 5
$result.Actions[0].decision | Should -Be 'trigger'
$result.Actions[0].rerunCommentId | Should -Be 0
}

It 'continues processing valid decisions after a failed one' {
Expand Down
5 changes: 4 additions & 1 deletion .github/scripts/Invoke-RerunReviewTrigger.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,11 @@ function Get-RerunActions {
$platform = Get-PlatformFromLabels -Labels @() -Fallback ([string]$candidate.platform)
$pipelineRef = Normalize-PipelineRef -Value ([string]$candidate.pipelineRef) -Fallback $DefaultPipelineRef

# The queue label does not prove which command created the current cycle,
# so source-ambiguous candidates intentionally carry no reaction target.
# review-trigger.yml does not need a comment to dispatch.
if ($decision -eq 'trigger' -and $rerunCommentId -le 0) {
throw "Candidate for PR #$prNumber has no rerun comment id; cannot trigger."
Write-Host "PR #$prNumber trigger has no proven current-cycle rerun comment; dispatching without a reaction target."
}

$actions.Add([pscustomobject]@{
Expand Down
373 changes: 373 additions & 0 deletions .github/scripts/Query-AutoRerunCandidates.Tests.ps1

Large diffs are not rendered by default.

374 changes: 374 additions & 0 deletions .github/scripts/Query-AutoRerunCandidates.ps1

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions .github/scripts/Query-RerunReadyPRs.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env pwsh
#Requires -Modules Pester

BeforeAll {
$scriptPath = Join-Path $PSScriptRoot 'Query-RerunReadyPRs.ps1'
$outputDir = Join-Path $PSScriptRoot '../../CustomAgentLogsTmp/QueryRerunReadyTests'
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null

. $scriptPath -Owner 'test-owner' -Repo 'test-repo'

function ConvertTo-GhLines {
param([object[]]$Items)
return @($Items | ForEach-Object { $_ | ConvertTo-Json -Depth 10 -Compress })
}
}

AfterAll {
Remove-Item -LiteralPath $outputDir -Recurse -Force -ErrorAction SilentlyContinue
}

Describe 'Query-RerunReadyPRs' {
BeforeEach {
$script:OutputPath = Join-Path $outputDir "$([Guid]::NewGuid().ToString('N')).json"
$script:issueComments = @(
[pscustomobject]@{
id = 100
body = "<!-- AI Summary -->`n<!-- SESSION:1111111 START -->"
created_at = '2026-05-31T09:00:00Z'
updated_at = '2026-05-31T09:00:00Z'
user = [pscustomobject]@{ login = 'MauiBot'; type = 'User' }
author_association = 'MEMBER'
},
[pscustomobject]@{
id = 200
body = '/review rerun'
created_at = '2026-05-31T10:00:00Z'
updated_at = '2026-05-31T10:00:00Z'
user = [pscustomobject]@{ login = 'maintainer'; type = 'User' }
author_association = 'MEMBER'
}
)

Mock Get-LatestReviewCommandOptions {
[pscustomobject]@{
Platform = ''
PipelineRef = 'main'
CommentId = 200
Body = '/review rerun'
}
}

Mock gh {
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$GhArgs
)

$command = $GhArgs -join ' '
$global:LASTEXITCODE = 0

if ($command -match '^pr list ') {
return ([pscustomobject]@{
number = 1
title = 'Autonomously queued PR'
url = 'https://example.test/1'
headRefOid = '2222222abcdef'
isDraft = $false
labels = @([pscustomobject]@{ name = 's/agent-ready-for-rerun' })
author = [pscustomobject]@{ login = 'dev-user' }
} | ConvertTo-Json -Depth 10 -Compress)
}
if ($command -match '/issues/1/labels') {
return 's/agent-ready-for-rerun'
}
if ($command -match '/issues/1/comments') {
return ConvertTo-GhLines $script:issueComments
}
if ($command -match '/pulls/1/(reviews|comments|commits)') {
return @()
}

throw "Unexpected gh call: $command"
}
}

It 'does not reuse a historical rerun command as the current queue reaction target' {
Invoke-RerunReadyPRQuery `
-QueryMaxPRs 5 `
-QueryOwner 'test-owner' `
-QueryRepo 'test-repo' `
-QueryOutputPath $script:OutputPath | Out-Null

$result = Get-Content -Raw -LiteralPath $script:OutputPath | ConvertFrom-Json
$result.candidates.Count | Should -Be 1
$result.candidates[0].reviewCommandId | Should -Be 200
$result.candidates[0].rerunCommentId | Should -Be 0
}
}
25 changes: 22 additions & 3 deletions .github/scripts/Query-RerunReadyPRs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ function Get-PlatformFromLabels {
return 'android'
}

function Invoke-RerunReadyPRQuery {
param(
[int]$QueryMaxPRs = $MaxPRs,
[string]$QueryOwner = $Owner,
[string]$QueryRepo = $Repo,
[string]$QueryOutputPath = $OutputPath
)

$MaxPRs = $QueryMaxPRs
$Owner = $QueryOwner
$Repo = $QueryRepo
$OutputPath = $QueryOutputPath

$searchJson = gh pr list `
--repo "$Owner/$Repo" `
--state open `
Expand All @@ -80,10 +93,8 @@ foreach ($pr in @($searchResult)) {
if ($labels -contains $ReviewInProgressLabel -and -not (Test-AgentReviewInProgressIsStale -PRNumber $number -Owner $Owner -Repo $Repo)) {
continue
}

$activity = @(Get-ActivityForPR -Number $number)
$commits = @(Get-CommitsForPR -Number $number)
$latestRerun = Get-LatestRerunComment -Comments $activity
$reviewOptions = Get-LatestReviewCommandOptions -Comments $activity -Owner $Owner -Repo $Repo
$rawAuthorLogin = if ($pr.author -and $pr.author.login) { [string]$pr.author.login } else { '' }
$authorLogin = Normalize-GitHubActorLogin $rawAuthorLogin
Expand All @@ -103,7 +114,10 @@ foreach ($pr in @($searchResult)) {
reviewCommandId = $reviewOptions.CommentId
reviewCommand = $reviewOptions.Body
labels = $labels
rerunCommentId = if ($latestRerun) { [Int64]$latestRerun.id } else { $null }
# The ready label does not encode whether this queue cycle came from a
# specific command or the autonomous labeler. Never reuse a historical
# /review rerun comment as this cycle's reaction target.
rerunCommentId = [Int64]0
contextMarkdown = $contextMarkdown
}
}
Expand All @@ -118,3 +132,8 @@ $json | Set-Content -LiteralPath $OutputPath -Encoding UTF8

Write-Host "Wrote $($candidates.Count) rerun-ready candidate(s) to $OutputPath"
Write-Output $json
}

if ($MyInvocation.InvocationName -ne '.') {
Invoke-RerunReadyPRQuery
}
183 changes: 183 additions & 0 deletions .github/scripts/Resolve-RerunEligibility.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,186 @@ new
$context | Should -Match 'New non-command author comments: 0'
}
}

Describe 'Resolve-AutonomousRerunEligibility' {
It 'rejects a PR that was never AI-reviewed (no AI Summary)' {
$comments = @(
New-TestComment -Id 1 -Body 'Some author update.' -CreatedAt '2026-05-31T09:00:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'no-ai-summary'
}

It 'accepts a new PR-author comment after the latest AI Summary' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the requested update.' -CreatedAt '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-author-comment-after-ai-summary'
}

It 'accepts a new commit after the latest AI Summary' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)
$commits = @(
New-TestCommit -Sha 'aaaaaaa' -Date '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits $commits -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-commit-after-ai-summary'
}

It 'accepts a head SHA that differs from the last reviewed SHA' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha '2222222abcdef' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-head-commit'
}

It 'reports eligible with reason label-already-present when the ready-for-rerun label is already applied' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the requested update.' -CreatedAt '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user' -CurrentLabels @('s/agent-ready-for-rerun')
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'label-already-present'
}

It 'skips when a review is already in progress' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the requested update.' -CreatedAt '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user' -CurrentLabels @('s/agent-review-in-progress')
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'review-in-progress'
}

It 'rejects when there is no new activity since the AI Summary' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'no-new-comments-or-commits'
}

It 'ignores a maintainer (non-author) comment after the AI Summary' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody) -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'Please address the AI feedback.' -CreatedAt '2026-05-31T09:45:00Z' -Login 'maintainer'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha 'abcdef123' -PRAuthorLogin 'dev-user'
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'no-new-comments-or-commits'
}

Context 'anti-flap: last-declined checkpoint (scanner skip)' {
It 'does not re-qualify a head that only differs from the summary when the scanner already declined it and nothing new landed' {
# AI Summary reviewed 1111111; head is 2222222 (differs). An author comment
# landed after the summary but BEFORE the scanner removed the label. With the
# decline checkpoint that state is already-declined, so it must not flap back on.
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the update.' -CreatedAt '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha '2222222abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt '2026-05-31T10:00:00Z'
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'declined-state-unchanged'
}

It 're-qualifies (new-head-commit) when a commit lands after the decline' {
Comment thread
kubaflo marked this conversation as resolved.
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)
$commits = @(
New-TestCommit -Sha '2222222abcdef' -Date '2026-05-31T10:30:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits $commits -CurrentHeadSha '2222222abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt '2026-05-31T10:00:00Z'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-head-commit'
}

It 'does not re-qualify when the differing-head commit predates the decline' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)
$commits = @(
New-TestCommit -Sha '2222222abcdef' -Date '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits $commits -CurrentHeadSha '2222222abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt '2026-05-31T10:00:00Z'
$result.Eligible | Should -BeFalse
$result.Reason | Should -Be 'declined-state-unchanged'
}

It 're-qualifies when the current head differs from the exact declined head even if the marker is newer' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
)

$result = Resolve-AutonomousRerunEligibility `
-Comments $comments `
-Commits @() `
-CurrentHeadSha '3333333333333333333333333333333333333333' `
-PRAuthorLogin 'dev-user' `
-LastDeclinedAt '2026-05-31T10:00:00Z' `
-LastDeclinedHeadSha '2222222222222222222222222222222222222222'

$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-head-commit'
}

It 're-qualifies on a fresh PR-author comment posted after the decline' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'Any update on this?' -CreatedAt '2026-05-31T10:30:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha '1111111abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt '2026-05-31T10:00:00Z'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-author-comment-after-ai-summary'
}

It 'ignores a decline that predates the latest AI Summary (trigger-path removal superseded by the fresh summary)' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the update.' -CreatedAt '2026-05-31T09:45:00Z'
)

# Label removed at 08:00 (before the 09:00 summary) — a completed review's removal,
# not a skip — so it must not suppress the genuine post-summary author comment.
$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha '1111111abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt '2026-05-31T08:00:00Z'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-author-comment-after-ai-summary'
}

It 'falls back to the summary checkpoint when LastDeclinedAt is malformed' {
$comments = @(
New-TestComment -Id 1 -Body (New-AISummaryBody -Sha '1111111') -CreatedAt '2026-05-31T09:00:00Z' -UpdatedAt '2026-05-31T09:30:00Z' -Login 'MauiBot' -Type 'User'
New-TestComment -Id 2 -Body 'I pushed the update.' -CreatedAt '2026-05-31T09:45:00Z'
)

$result = Resolve-AutonomousRerunEligibility -Comments $comments -Commits @() -CurrentHeadSha '1111111abcdef' -PRAuthorLogin 'dev-user' -LastDeclinedAt 'not-a-real-date'
$result.Eligible | Should -BeTrue
$result.Reason | Should -Be 'new-author-comment-after-ai-summary'
}
}
}
Loading
Loading