Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 137 additions & 0 deletions .github/actions/RestartStalePRChecks/action.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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
)

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

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

# Get all open pull requests
$prs = gh pr list --state open --json number,title,url --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)"

# Get checks for this PR
try {
$checks = gh pr checks $pr.number --json name,state,bucket,completedAt,link | ConvertFrom-Json
}
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, requesting rerun..."

# Try to rerequest the check with retries
$prFailed = $false
for ($retry = 0; $retry -lt $maxRetries; $retry++) {
try {
if ($retry -gt 0) {
# Exponential backoff: 2s, 4s, 8s
$delaySeconds = 2 * [Math]::Pow(2, $retry - 1)
Write-Host " Retry attempt $($retry + 1)/$maxRetries (waiting $delaySeconds seconds)..."
Start-Sleep -Seconds $delaySeconds
}

# Rerequest the check by re-running the workflow
# First, get the workflow run ID from the check link
if ($statusCheck.link -match '/runs/(\d+)') {
$runId = $matches[1]
# Validate run ID is a positive integer
if ([int]$runId -gt 0) {
gh run rerun $runId -R $env:GITHUB_REPOSITORY | Out-Null
Write-Host " ✓ Successfully triggered re-run of workflow (run ID: $runId)"
$restarted++
break
}
else {
Write-Host " ✗ Invalid run ID extracted: $runId"
$prFailed = $true
break
}
}
else {
Write-Host " ✗ Could not extract run ID from link: $($statusCheck.link)"
$prFailed = $true
break
}
}
catch {
$errorMsg = $_.Exception.Message
if ($retry -eq $maxRetries - 1) {
Write-Host " ✗ Failed to restart workflow after $maxRetries attempts: $errorMsg"
$prFailed = $true
}
else {
Write-Host " ⚠ Attempt $($retry + 1) failed: $errorMsg"
}
}
}

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

Write-Host ""
Write-Host "Summary:"
Write-Host " ✓ Successfully restarted: $restarted workflow run(s)"
Write-Host " ✗ Failed attempts: $failed"

# Add GitHub Actions job summary
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "## PR Status Check Restart Summary"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ""
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ✓ Successfully restarted: **$restarted** workflow run(s)"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- ✗ Failed attempts: **$failed**"

# Exit with error if there were any failures
if ($failed -gt 0) {
Write-Host "::error::Failed to process $failed PR(s)"
exit 1
}
32 changes: 32 additions & 0 deletions .github/actions/RestartStalePRChecks/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Restart Stale PR Checks
author: Microsoft Corporation
description: Restart stale PR status checks that are older than a specified threshold
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: Checkout
uses: actions/checkout@v4

- name: Restart 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
31 changes: 31 additions & 0 deletions .github/workflows/RestartPRStatusCheck.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: 'Restart Pull Request Status Check'

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: read

defaults:
run:
shell: pwsh

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

- name: Restart Stale PR Status Checks
uses: ./.github/actions/RestartStalePRChecks
with:
thresholdHours: 72
maxRetries: 3
token: ${{ github.token }}