Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6c7664
Initial plan
Copilot Jan 20, 2026
c10c2ef
Add workflow to restart old PR status checks
Copilot Jan 20, 2026
1e77a3f
Fix null check for pull_requests in workflow
Copilot Jan 20, 2026
50ff79e
Address code review feedback: improve API efficiency and date parsing
Copilot Jan 20, 2026
1392419
Refactor to use gh pr checks and add retry logic
Copilot Jan 20, 2026
4cc39a1
Add exponential backoff and run ID validation
Copilot Jan 20, 2026
4431d75
Extract script to action and address review comments
Copilot Jan 22, 2026
49ff744
Use gh run rerun instead of API call
Copilot Jan 22, 2026
9cfd852
Remove redundant bucket check and unused headRefName property
Copilot Jan 22, 2026
60eddd1
Remove unused success variable and fail on processing errors
Copilot Jan 22, 2026
bffa485
Count failed PRs instead of failed attempts
Copilot Jan 22, 2026
557c998
Use Invoke-CommandWithRetry, filter mergeable PRs, remove default params
Copilot Jan 23, 2026
97e67bf
Add WhatIf parameter for testing without triggering reruns
Copilot Jan 23, 2026
327b978
Remove WhatIf from action.yaml, keep in script only
Copilot Jan 23, 2026
05a4f0d
Remove redundant checkout from action
Copilot Jan 23, 2026
b9c4a69
Fix Int32 overflow by using Int64 for run ID validation
Copilot Jan 23, 2026
f274fa6
Add null check for GITHUB_STEP_SUMMARY to fix Path error
Copilot Jan 23, 2026
02f54ba
Clarify summary labels: failed PRs not retry attempts
Copilot Jan 26, 2026
5cda9ed
Change from rerun to delete workflow run + comment approach
Copilot Jan 29, 2026
ea32524
Rename files to match new "Clean Up" approach
Copilot Jan 29, 2026
1e1f36f
Enhance comment for deleted stale PR status checks
mazhelez Jan 29, 2026
5d20075
Update .github/actions/CleanUpStalePRChecks/action.ps1
mazhelez Jan 29, 2026
e6b186a
Fix cross-platform path and update WhatIf messaging
Copilot Jan 29, 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
179 changes: 179 additions & 0 deletions .github/actions/CleanUpStalePRChecks/action.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
param (
[Parameter(Mandatory = $false, HelpMessage="Threshold in hours for considering a check stale")]
[int] $thresholdHours = 72,
[Parameter(Mandatory = $false, HelpMessage="Maximum number of retry attempts")]
[int] $maxRetries = 3,
[Parameter(Mandatory = $false, HelpMessage="If specified, only performs read operations without deleting workflow runs or adding PR comments")]
[switch] $WhatIf
)

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
Set-StrictMode -Version 2.0

# Import EnlistmentHelperFunctions module
Import-Module "$PSScriptRoot/../../../build/scripts/EnlistmentHelperFunctions.psm1" -DisableNameChecking

if ($WhatIf) {
Write-Host "::notice::Running in WhatIf mode - no workflow runs will be deleted and no comments will be added"
}

Write-Host "Fetching open pull requests..."

# Get all open pull requests with mergeable state
$prs = gh pr list --state open --json number,title,url,mergeable --limit 1000 | ConvertFrom-Json

Write-Host "Found $($prs.Count) open pull requests"

if ($prs.Count -eq 0) {
Write-Host "::notice::No open pull requests found"
exit 0
}

$now = [DateTime]::UtcNow
$restarted = 0
$failed = 0

foreach ($pr in $prs) {
Write-Host ""
Write-Host "Checking PR #$($pr.number): $($pr.title)"

# Check if PR is mergeable
if ($pr.mergeable -ne "MERGEABLE") {
Write-Host " PR is not in MERGEABLE state (current: $($pr.mergeable)), skipping"
continue
}

# Get checks for this PR with retry
$checks = $null
try {
$checks = Invoke-CommandWithRetry -ScriptBlock {
gh pr checks $pr.number --json name,state,bucket,completedAt,link | ConvertFrom-Json
} -RetryCount $maxRetries -FirstDelay 2 -MaxWaitBetweenRetries 8
}
catch {
Write-Host " ✗ Failed to get checks for PR: $_"
$failed++
continue
}

# Find the "Pull Request Status Check"
$statusCheck = $checks | Where-Object { $_.name -eq "Pull Request Status Check" }

if (-not $statusCheck) {
Write-Host " No 'Pull Request Status Check' found for this PR"
continue
}

Write-Host " Check state: $($statusCheck.state)"

# Check if the check is completed and successful
if ($statusCheck.state -ne "SUCCESS") {
Write-Host " Check state is '$($statusCheck.state)', not 'SUCCESS', skipping"
continue
}

$completedAt = [DateTime]::Parse($statusCheck.completedAt, [System.Globalization.CultureInfo]::InvariantCulture)
$ageInHours = ($now - $completedAt).TotalHours

Write-Host " Completed at: $completedAt UTC (Age: $([Math]::Round($ageInHours, 2)) hours)"

if ($ageInHours -le $thresholdHours) {
Write-Host " Status check is recent enough, no action needed"
continue
}

Write-Host " Status check is older than $thresholdHours hours, deleting stale workflow run..."

# Try to delete the workflow run and add a comment with retries using Invoke-CommandWithRetry
$prFailed = $false
try {
# Extract run ID from the check link
if ($statusCheck.link -match '/runs/(\d+)') {
$runId = $matches[1]
# Validate run ID is a positive integer
if ([int64]$runId -gt 0) {
if ($WhatIf) {
Write-Host " [WhatIf] Would delete workflow run (run ID: $runId) and add comment to PR #$($pr.number)"
$restarted++
}
else {
# Delete the workflow run
Invoke-CommandWithRetry -ScriptBlock {
gh run delete $runId -R $env:GITHUB_REPOSITORY | Out-Null
} -RetryCount $maxRetries -FirstDelay 2 -MaxWaitBetweenRetries 8
Write-Host " ✓ Successfully deleted workflow run (run ID: $runId)"

# Add a comment to the PR with instructions
$commentBody = @"
## ⚠️ Stale Status Check Deleted

The **Pull Request Build** workflow run for this PR was older than **$thresholdHours hours** and has been deleted.

### 📋 Why was it deleted?

Status checks that are too old may no longer reflect the current state of the target branch. To ensure this PR is validated against the latest code and passes up-to-date checks, a fresh build is required.

---

### 🔄 How to trigger a new status check:

1. 📤 **Push a new commit** to the PR branch, or
2. 🔁 **Close and reopen** the PR

This will automatically trigger a new **Pull Request Build** workflow run.
"@
Invoke-CommandWithRetry -ScriptBlock {
gh pr comment $pr.number --body $commentBody -R $env:GITHUB_REPOSITORY | Out-Null
} -RetryCount $maxRetries -FirstDelay 2 -MaxWaitBetweenRetries 8
Write-Host " ✓ Added comment to PR #$($pr.number) with instructions"
$restarted++
}
}
else {
Write-Host " ✗ Invalid run ID extracted: $runId"
$prFailed = $true
}
}
else {
Write-Host " ✗ Could not extract run ID from link: $($statusCheck.link)"
$prFailed = $true
}
}
catch {
Write-Host " ✗ Failed to delete workflow run or add comment: $_"
$prFailed = $true
}

# Increment failed counter once per PR if any attempt failed
if ($prFailed) {
$failed++
}
}

Write-Host ""
Write-Host "Summary:"
Write-Host " ✓ Successfully processed: $restarted PR(s)"
Write-Host " ✗ Failed to process: $failed PR(s)"

# Add GitHub Actions job summary
if ($env:GITHUB_STEP_SUMMARY) {
$summaryTitle = if ($WhatIf) { "## Stale PR Status Check Cleanup Summary (WhatIf Mode)" } else { "## Stale PR Status Check Cleanup Summary" }
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summaryTitle
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ""
if ($WhatIf) {
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ℹ️ Running in **WhatIf mode** - no workflow runs were deleted and no comments were added"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ""
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ✓ Successfully processed: **$restarted** PR(s) (would have deleted stale workflow runs and added comments)"
}
else {
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ✓ Successfully processed: **$restarted** PR(s) (deleted stale workflow runs and added comments)"
}
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ✗ Failed to process: **$failed** PR(s)"
}

# Exit with error if there were any failures (not in WhatIf mode)
if ($failed -gt 0 -and -not $WhatIf) {
Write-Host "::error::Failed to process $failed PR(s)"
exit 1
}
29 changes: 29 additions & 0 deletions .github/actions/CleanUpStalePRChecks/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Clean Up Stale PR Checks
author: Microsoft Corporation
description: Delete stale PR status check workflow runs and add comments with instructions to unblock PRs
inputs:
thresholdHours:
description: Threshold in hours for considering a check stale
required: false
default: '72'
maxRetries:
description: Maximum number of retry attempts
required: false
default: '3'
token:
description: The GitHub token running the action
required: false
default: ${{ github.token }}
runs:
using: composite
steps:
- name: Clean Up Stale PR Checks
shell: pwsh
env:
GH_TOKEN: ${{ inputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
${{ github.action_path }}/action.ps1 -thresholdHours ${{ inputs.thresholdHours }} -maxRetries ${{ inputs.maxRetries }}
branding:
icon: rotate-cw
color: blue
29 changes: 29 additions & 0 deletions .github/workflows/CleanUpStalePRChecks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: 'Clean Up Stale PR Status Checks'

on:
schedule:
- cron: '0 0 */3 * *' # Run every 3 days at midnight UTC
workflow_dispatch: # Allow manual trigger for testing

permissions:
actions: write
checks: read
contents: read
pull-requests: write

defaults:
run:
shell: pwsh

jobs:
CleanUpStatusChecks:
runs-on: ubuntu-latest
name: Clean Up Stale PR Status Checks
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Clean Up Stale PR Status Checks
uses: ./.github/actions/CleanUpStalePRChecks
with:
token: ${{ github.token }}
Loading