scripts/Fix-DependabotPRs.ps1 (160 lines of code) (raw):

# This script is used to fix the dependabot PR dependencies by triggering the lockfiles-command workflow for each PR and then closing and reopening the PRs to force a rebuild. # If you don't want specific PRs to be affected, add a prefix to the title (e.g. "Needs manual intervention: "). This script ignores any PRs that don't start directly with "Bump" $maxPRs = 100 $dryRun = $false function getPrLink($prNumber) { return "https://github.com/azure/bicep/pull/$($prNumber)" } function getPrState($prNumber) { return (gh pr view $prNumber --json state | jq '.state').Trim('"') } function prHasConflicts($prNumber) { return (gh pr view $prNumber --json mergeable | jq '.mergeable').Trim('"') -eq 'CONFLICTING' } function waitForPrRecreate($prNumber) { if (-not (prHasConflicts($prNumber))) { return } Write-Host -NoNewline "PR $(getPrLink($prNumber)) has conflicts. Waiting for it to be recreated..." while ($true) { $lastComment = gh pr view $prNumber --json comments --jq '.comments[-1].body' if ($lastComment -notlike '*@dependabot recreate*') { Write-Host "" write-warning "PR $(getPrLink($prNumber)) last comment: $lastComment" return } if (prHasConflicts($prNumber)) { Write-Host -NoNewline "." } else { Write-Host "`nPR $(getPrLink($prNumber)) has been recreated." return } Start-Sleep -Seconds 60 } Write-Host "" } # returns true if the PR should be removed from the list, otherwise false function processPR { param ( [Parameter(Mandatory = $true)] [int]$prNumber, [Parameter(Mandatory = $true)] [string]$prRef ) Write-Host "`n====================== Processing PR $prNumber ======================`n" $prState = getPrState($prNumber) if ($prState -ne 'OPEN') { Write-Warning "PR $(getPrLink($prNumber)) is not open. Skipping..." $prStatus[$prNumber] = "Unexpected closed" return $true } if ($prStatus[$prNumber] -eq "Conflicts") { Write-Host "Waiting for PR $(getPrLink($prNumber)) with conflicts to be recreated..." waitForPrRecreate $prNumber $prStatus[$prNumber] = "Recreated" $prState = getPrState($prNumber) if ($prState -ne 'OPEN') { Write-Warning "PR $(getPrLink($prNumber)) was closed during @dependabot recreate." $prStatus[$prNumber] = "Closed after @dependabot recreate" return $true } } if (prHasConflicts($prNumber)) { Write-Host "PR $(getPrLink($prNumber)) has conflicts. Recreating PR and putting at the end of the list." if (!$dryRun) { gh pr comment $prNumber --body "@dependabot recreate" } $prStatus[$prNumber] = "Conflicts" return $false } Write-Host "Running lockfiles-command workflow for PR $(getPrLink($prNumber))" if (!$dryRun) { gh workflow run lockfiles-command.yml --ref=$prRef } Write-Host "Waiting for the lockfiles-command workflow to complete for PR $(getPrLink($prNumber))" if (!$dryRun) { Start-Sleep -Seconds 15 } while ($true) { $runningOutput = gh run list --workflow=lockfiles-command.yml --json status,headBranch $running = $runningOutput | ConvertFrom-Json | Where-Object { $_.headBranch -eq $prRef } if (!$running) { write-host "" Write-Warning "No lockfiles-command workflows found for PR $(getPrLink($prNumber))" $prStatus[$prNumber] = "No lockfiles-command workflows found" return $true } $inProgress = $running | Where-Object { $_.status -eq "in_progress" -or $_.status -eq "queued" } if ($inProgress) { Write-Host -NoNewline "." } else { Write-Host "`nlockfiles-command for $(getPrLink($prNumber)) has completed." break } Start-Sleep -Seconds 15 } Write-Host "Closing and reopening $(getPrLink($prNumber)) to force a rebuild..." if (!$dryRun) { gh pr close $prNumber gh pr reopen $prNumber -c "Fix-DependabotPRs.ps1: Forcing rebuild" } Write-Host "Waiting for the required checks to complete for $(getPrLink($prNumber))" if (!$dryRun) { Start-Sleep -Seconds 15 # Wait for the PR to reopen before checking the status } # Use gh checks wait with --fail-fast to exit on the first failure gh pr checks $prNumber --watch --fail-fast --required if ($LASTEXITCODE -ne 0) { $failedChecks = gh pr checks $prNumber --required --json name,state --jq '.[] | select(.state == "FAILURE") | .name' $failedChecksArray = $failedChecks -split "`n" $failedChecksCount = $failedChecksArray.Count $failedChecksString = "$failedChecksCount failed checks: $($failedChecksArray -join ", ")" Write-Warning $failedChecksString $prStatus[$prNumber] = $failedChecksString return $true } # Set to auto-merge Write-Host "All checks for $(getPrLink($prNumber)) have completed successfully." gh pr comment $prNumber --body "Fix-DependabotPRs.ps1: All checks have passed. Setting to auto-merge." gh pr merge $prNumber --squash --auto Write-Host "PR $(getPrLink($prNumber)) has been set to auto-merge." $prStatus[$prNumber] = "Set to auto merge" return $true } function showStatus($prs) { $i = 1 foreach ($pr in $prs) { Write-Host "$($i): $(getPrLink($pr.number)): $($prStatus[[int]$pr.number]) ($(getPrState($pr.number)))" $i++ } } Write-Host "Getting list of matching PRs..." $prsJson = gh pr list --label dependencies --limit $maxPRs --json title,number,headRefName,state,author --jq '.[] | select(.title | startswith("Bump")) | select(.author.login == "app/dependabot")' $allPrs = $prsJson | ConvertFrom-Json $prStatus = @{} $allPrs | ForEach-Object { $prStatus[$_.number] = "" } # Loop through each PR one at a time $prsToBeProcessed = $allPrs write-host "Processing $($prsToBeProcessed.Count) PRs...$($prsToBeProcessed | ForEach-Object { "`n$(getPrLink($_.number)): $($_.title)" })" while ($prsToBeProcessed) { Write-Host "`nStatus:" showStatus $allPrs Write-Host "Still in queue:" showStatus $prsToBeProcessed $pr = $prsToBeProcessed[0] $prNumber = [int]$pr.number $processed = processPR -prNumber $prNumber -prRef $pr.headRefName if ($processed[-1] -eq $true) { Write-Host "PR $(getPrLink($prNumber)) has been processed." $prsToBeProcessed = $prsToBeProcessed[1..$prsToBeProcessed.Length] } elseif ($processed[-1] -eq $false) { Write-Host "PR $(getPrLink($prNumber)) is still being processed." $prsToBeProcessed = $prsToBeProcessed[1..$prsToBeProcessed.Length] + $pr # Put at the end of the list $prStatus[$prNumber] = $prStatus[$prNumber] + " (sent to back of queue)" } else { Write-Error "Unexpected return value from processPR: $processed" } } Write-Host "`nAll PRs processed." Write-Host "Originally there were $($allPrs.Count) PRs. $($prStatus.Count) are still open." showStatus $allPrs