mirror of https://github.com/microsoft/vcpkg.git
221 lines
7.3 KiB
PowerShell
221 lines
7.3 KiB
PowerShell
#Requires -Version 7.0
|
|
|
|
param(
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Age = 120,
|
|
|
|
[string]$Repo = 'microsoft/vcpkg',
|
|
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Limit = 1000,
|
|
|
|
[string]$OutputFile,
|
|
|
|
[ValidateScript({
|
|
if ($_ -and -not (Test-Path -LiteralPath $_ -PathType Leaf)) {
|
|
throw '-CloseWithComment requires a valid markdown file path.'
|
|
}
|
|
|
|
$true
|
|
})]
|
|
[string]$CloseWithComment
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$NoStaleLabel = 'no-stale'
|
|
|
|
function Invoke-GhCommand {
|
|
param([Parameter(Mandatory)][string[]]$Arguments)
|
|
|
|
$previousGhToken = $env:GH_TOKEN
|
|
$previousGithubToken = $env:GITHUB_TOKEN
|
|
|
|
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
|
Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
|
|
|
|
try {
|
|
$output = & gh @Arguments 2>&1
|
|
return @{ Output = $output; ExitCode = $LASTEXITCODE }
|
|
}
|
|
finally {
|
|
if ($null -eq $previousGhToken) {
|
|
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
$env:GH_TOKEN = $previousGhToken
|
|
}
|
|
|
|
if ($null -eq $previousGithubToken) {
|
|
Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
$env:GITHUB_TOKEN = $previousGithubToken
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-GhPrList {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Repo,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int]$Limit,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string]$CutoffDate
|
|
)
|
|
|
|
$searchQuery = "is:pr updated:<$CutoffDate -label:$NoStaleLabel sort:updated-asc"
|
|
$command = @('pr', 'list', '--repo', $Repo, '--state', 'open', '--limit', $Limit.ToString(), '--search', $searchQuery, '--json', 'number,title,url,updatedAt,labels,author')
|
|
$result = Invoke-GhCommand -Arguments $command
|
|
|
|
if ($result.ExitCode -ne 0) {
|
|
$message = ($result.Output | Out-String).Trim()
|
|
throw "Unable to list PRs with gh: $message"
|
|
}
|
|
|
|
return ($result.Output | Out-String) | ConvertFrom-Json -AsHashtable
|
|
}
|
|
|
|
function Get-AgeDays {
|
|
param([Parameter(Mandatory)][string]$UpdatedAt)
|
|
|
|
$updatedUtc = [datetime]::Parse(
|
|
$UpdatedAt,
|
|
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
[System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal
|
|
)
|
|
|
|
return [int][Math]::Floor(((Get-Date).ToUniversalTime() - $updatedUtc).TotalDays)
|
|
}
|
|
|
|
function Write-CandidateReport {
|
|
param(
|
|
[Parameter(Mandatory)][hashtable]$Pr,
|
|
[Parameter(Mandatory)][int]$AgeDays
|
|
)
|
|
|
|
$labels = @($Pr.labels | ForEach-Object { $_.name })
|
|
$labelText = if ($labels.Count -gt 0) { ($labels | Sort-Object -Unique) -join ', ' } else { '(none)' }
|
|
$age = Get-AgeDays -UpdatedAt $Pr.updatedAt
|
|
$authorObject = $Pr['author']
|
|
$author = if ($authorObject -and $authorObject.Contains('login')) { [string]$authorObject['login'] } else { '(unknown)' }
|
|
|
|
return @(
|
|
"PR #$($Pr.number): $($Pr.title)",
|
|
" author: $author",
|
|
" last updated $age days ago (threshold: $AgeDays days)",
|
|
" labels: $labelText",
|
|
" url: $($Pr.url)"
|
|
) -join [Environment]::NewLine
|
|
}
|
|
|
|
function Write-ReportLine {
|
|
param(
|
|
[Parameter(Mandatory)][AllowEmptyString()][string]$Text,
|
|
[System.Collections.Generic.List[string]]$ReportLines
|
|
)
|
|
|
|
if ($null -ne $ReportLines) {
|
|
$ReportLines.Add($Text)
|
|
}
|
|
|
|
[Console]::WriteLine($Text)
|
|
}
|
|
|
|
function Test-GhAuthentication {
|
|
$result = Invoke-GhCommand -Arguments @('auth', 'status')
|
|
|
|
if ($result.ExitCode -ne 0) {
|
|
$message = ($result.Output | Out-String).Trim()
|
|
throw "GitHub CLI is not authenticated. Run 'gh auth login' first. $message"
|
|
}
|
|
}
|
|
|
|
function Close-PullRequest {
|
|
param(
|
|
[Parameter(Mandatory)][hashtable]$Pr,
|
|
[Parameter(Mandatory)][string]$Repo,
|
|
[ValidateScript({ -not $_ -or (Test-Path -LiteralPath $_ -PathType Leaf) })]
|
|
[string]$CommentFile
|
|
)
|
|
|
|
$command = @('pr', 'close', [string]$Pr.number, '--repo', $Repo)
|
|
if ($CommentFile) {
|
|
$comment = Get-Content -LiteralPath $CommentFile -Raw
|
|
$command += @('--comment', $comment)
|
|
}
|
|
|
|
$result = Invoke-GhCommand -Arguments $command
|
|
|
|
if ($result.ExitCode -ne 0) {
|
|
$message = ($result.Output | Out-String).Trim()
|
|
throw "Unable to close PR #$($Pr.number): $message"
|
|
}
|
|
|
|
return ($result.Output | Out-String)
|
|
}
|
|
|
|
function Main {
|
|
$reportLines = [System.Collections.Generic.List[string]]::new()
|
|
$resolvedRepo = if ($env:GITHUB_REPOSITORY) { $env:GITHUB_REPOSITORY } else { $Repo }
|
|
|
|
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
|
|
Write-Error "'gh' is not available on PATH."
|
|
return 1
|
|
}
|
|
|
|
Test-GhAuthentication
|
|
$cutoffDate = (Get-Date).ToUniversalTime().AddDays(-$Age).ToString('yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture)
|
|
$prs = Get-GhPrList -Repo $resolvedRepo -Limit $Limit -CutoffDate $cutoffDate
|
|
|
|
$stalePrs = @($prs | Sort-Object { [datetime]::Parse($_.updatedAt, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal) })
|
|
|
|
if ($stalePrs.Count -eq 0) {
|
|
Write-ReportLine -Text "No open PRs with no activity for $Age days or more found in $resolvedRepo." -ReportLines $reportLines
|
|
|
|
if ($OutputFile) {
|
|
$directory = Split-Path -Path $OutputFile -Parent
|
|
if ($directory -and -not (Test-Path -LiteralPath $directory -PathType Container)) {
|
|
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
|
}
|
|
|
|
Set-Content -LiteralPath $OutputFile -Value $reportLines -Encoding utf8NoBOM
|
|
}
|
|
return
|
|
}
|
|
|
|
Write-ReportLine -Text "Found $($stalePrs.Count) stale PR(s) in $resolvedRepo (threshold: $Age days since last update):" -ReportLines $reportLines
|
|
Write-ReportLine -Text '' -ReportLines $reportLines
|
|
|
|
foreach ($pr in $stalePrs) {
|
|
$reportText = Write-CandidateReport -Pr $pr -AgeDays $Age
|
|
Write-ReportLine -Text $reportText -ReportLines $reportLines
|
|
Write-ReportLine -Text '' -ReportLines $reportLines
|
|
|
|
if ($CloseWithComment) {
|
|
Close-PullRequest -Pr $pr -Repo $resolvedRepo -CommentFile $CloseWithComment | Out-Null
|
|
Write-ReportLine -Text " closed PR #$($pr.number) via gh pr close" -ReportLines $reportLines
|
|
}
|
|
else {
|
|
Write-ReportLine -Text " would close PR #$($pr.number); pass -CloseWithComment to close it" -ReportLines $reportLines
|
|
}
|
|
}
|
|
|
|
if ($OutputFile) {
|
|
$directory = Split-Path -Path $OutputFile -Parent
|
|
if ($directory -and -not (Test-Path -LiteralPath $directory -PathType Container)) {
|
|
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
|
}
|
|
|
|
Set-Content -LiteralPath $OutputFile -Value $reportLines -Encoding utf8NoBOM
|
|
}
|
|
}
|
|
|
|
if ($MyInvocation.InvocationName -ne '.') {
|
|
exit (Main)
|
|
}
|