Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
248 lines
7.9 KiB
PowerShell
248 lines
7.9 KiB
PowerShell
# ci-queue-wait.ps1 - Wait until project CI queue is clear (no running/queued pipeline on branch head)
|
|
# Usage: .\ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose merge] [-RequireStatus]
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Alias("B")]
|
|
[string]$Branch = "main",
|
|
|
|
[Alias("t")]
|
|
[int]$TimeoutSeconds = 900,
|
|
|
|
[Alias("i")]
|
|
[int]$IntervalSeconds = 15,
|
|
|
|
[ValidateSet("push", "merge")]
|
|
[string]$Purpose = "merge",
|
|
|
|
[switch]$RequireStatus,
|
|
|
|
[Alias("h")]
|
|
[switch]$Help
|
|
)
|
|
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
. "$ScriptDir\detect-platform.ps1"
|
|
|
|
function Show-Usage {
|
|
@"
|
|
Usage: ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose push|merge] [-RequireStatus]
|
|
|
|
Options:
|
|
-Branch, -B BRANCH Branch head to inspect (default: main)
|
|
-TimeoutSeconds, -t SECONDS Max wait time (default: 900)
|
|
-IntervalSeconds, -i SECONDS Poll interval (default: 15)
|
|
-Purpose VALUE push or merge (default: merge)
|
|
-RequireStatus Fail if no CI status contexts are present
|
|
-Help, -h Show help
|
|
"@
|
|
}
|
|
|
|
if ($Help) {
|
|
Show-Usage
|
|
exit 0
|
|
}
|
|
|
|
if ($TimeoutSeconds -lt 1 -or $IntervalSeconds -lt 1) {
|
|
Write-Error "TimeoutSeconds and IntervalSeconds must be positive integers."
|
|
exit 1
|
|
}
|
|
|
|
function Get-RemoteHost {
|
|
$remoteUrl = git remote get-url origin 2>$null
|
|
if ([string]::IsNullOrEmpty($remoteUrl)) { return $null }
|
|
if ($remoteUrl -match "^https?://([^/]+)/") { return $Matches[1] }
|
|
if ($remoteUrl -match "^git@([^:]+):") { return $Matches[1] }
|
|
return $null
|
|
}
|
|
|
|
function Get-GiteaToken {
|
|
param([string]$Host)
|
|
|
|
if ($env:GITEA_TOKEN) { return $env:GITEA_TOKEN }
|
|
|
|
$credPath = Join-Path $HOME ".git-credentials"
|
|
if (-not (Test-Path $credPath)) { return $null }
|
|
|
|
$line = Get-Content $credPath | Where-Object { $_ -like "*$Host*" } | Select-Object -First 1
|
|
if (-not $line) { return $null }
|
|
|
|
if ($line -match 'https?://[^@]*:([^@/]+)@') {
|
|
return $Matches[1]
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Get-QueueState {
|
|
param([object]$Payload)
|
|
|
|
$pending = @("pending", "queued", "running", "waiting")
|
|
$failure = @("failure", "error", "failed")
|
|
$success = @("success")
|
|
|
|
$state = ""
|
|
if ($null -ne $Payload.state) {
|
|
$state = "$($Payload.state)".ToLowerInvariant()
|
|
}
|
|
|
|
if ($pending -contains $state) { return "pending" }
|
|
if ($failure -contains $state) { return "terminal-failure" }
|
|
if ($success -contains $state) { return "terminal-success" }
|
|
|
|
$values = @()
|
|
$statuses = @()
|
|
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
|
|
|
foreach ($s in $statuses) {
|
|
if ($null -eq $s) { continue }
|
|
$v = ""
|
|
if ($null -ne $s.status) { $v = "$($s.status)".ToLowerInvariant() }
|
|
elseif ($null -ne $s.state) { $v = "$($s.state)".ToLowerInvariant() }
|
|
if (-not [string]::IsNullOrEmpty($v)) { $values += $v }
|
|
}
|
|
|
|
if ($values.Count -eq 0 -and [string]::IsNullOrEmpty($state)) { return "no-status" }
|
|
if (($values | Where-Object { $pending -contains $_ }).Count -gt 0) { return "pending" }
|
|
if (($values | Where-Object { $failure -contains $_ }).Count -gt 0) { return "terminal-failure" }
|
|
if ($values.Count -gt 0 -and ($values | Where-Object { -not ($success -contains $_) }).Count -eq 0) { return "terminal-success" }
|
|
return "unknown"
|
|
}
|
|
|
|
function Print-PendingContexts {
|
|
param([object]$Payload)
|
|
|
|
$pending = @("pending", "queued", "running", "waiting")
|
|
$statuses = @()
|
|
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
|
|
|
if ($statuses.Count -eq 0) {
|
|
Write-Host "[ci-queue-wait] no status contexts reported"
|
|
return
|
|
}
|
|
|
|
$found = $false
|
|
foreach ($s in $statuses) {
|
|
if ($null -eq $s) { continue }
|
|
$name = if ($s.context) { $s.context } elseif ($s.name) { $s.name } else { "unknown-context" }
|
|
$value = if ($s.status) { "$($s.status)".ToLowerInvariant() } elseif ($s.state) { "$($s.state)".ToLowerInvariant() } else { "unknown" }
|
|
$target = if ($s.target_url) { $s.target_url } elseif ($s.url) { $s.url } else { "" }
|
|
if ($pending -contains $value) {
|
|
$found = $true
|
|
if ($target) {
|
|
Write-Host "[ci-queue-wait] pending: $name=$value ($target)"
|
|
}
|
|
else {
|
|
Write-Host "[ci-queue-wait] pending: $name=$value"
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $found) {
|
|
Write-Host "[ci-queue-wait] no pending contexts"
|
|
}
|
|
}
|
|
|
|
$platform = Get-GitPlatform
|
|
$owner = Get-GitRepoOwner
|
|
$repo = Get-GitRepoName
|
|
|
|
if ([string]::IsNullOrEmpty($owner) -or [string]::IsNullOrEmpty($repo)) {
|
|
Write-Error "Could not determine repository owner/name from git remote."
|
|
exit 1
|
|
}
|
|
|
|
$headSha = $null
|
|
$host = $null
|
|
$giteaToken = $null
|
|
|
|
switch ($platform) {
|
|
"github" {
|
|
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
|
|
Write-Error "gh CLI is required for GitHub CI queue guard."
|
|
exit 1
|
|
}
|
|
$headSha = (& gh api "repos/$owner/$repo/branches/$Branch" --jq ".commit.sha").Trim()
|
|
if ([string]::IsNullOrEmpty($headSha)) {
|
|
Write-Error "Could not resolve $Branch head SHA."
|
|
exit 1
|
|
}
|
|
Write-Host "[ci-queue-wait] platform=github purpose=$Purpose branch=$Branch sha=$headSha"
|
|
}
|
|
"gitea" {
|
|
$host = Get-RemoteHost
|
|
if ([string]::IsNullOrEmpty($host)) {
|
|
Write-Error "Could not determine remote host."
|
|
exit 1
|
|
}
|
|
$giteaToken = Get-GiteaToken -Host $host
|
|
if ([string]::IsNullOrEmpty($giteaToken)) {
|
|
Write-Error "Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials."
|
|
exit 1
|
|
}
|
|
try {
|
|
$branchUrl = "https://$host/api/v1/repos/$owner/$repo/branches/$Branch"
|
|
$branchPayload = Invoke-RestMethod -Method Get -Uri $branchUrl -Headers @{ Authorization = "token $giteaToken" }
|
|
$headSha = ($branchPayload.commit.id | Out-String).Trim()
|
|
}
|
|
catch {
|
|
Write-Error "Could not resolve $Branch head SHA from Gitea API."
|
|
exit 1
|
|
}
|
|
if ([string]::IsNullOrEmpty($headSha)) {
|
|
Write-Error "Could not resolve $Branch head SHA."
|
|
exit 1
|
|
}
|
|
Write-Host "[ci-queue-wait] platform=gitea purpose=$Purpose branch=$Branch sha=$headSha"
|
|
}
|
|
default {
|
|
Write-Error "Unsupported platform '$platform'."
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
|
|
while ($true) {
|
|
if ((Get-Date) -gt $deadline) {
|
|
Write-Error "Timed out waiting for CI queue to clear on $Branch after ${TimeoutSeconds}s."
|
|
exit 124
|
|
}
|
|
|
|
try {
|
|
if ($platform -eq "github") {
|
|
$statusJson = & gh api "repos/$owner/$repo/commits/$headSha/status"
|
|
$payload = $statusJson | ConvertFrom-Json
|
|
}
|
|
else {
|
|
$statusUrl = "https://$host/api/v1/repos/$owner/$repo/commits/$headSha/status"
|
|
$payload = Invoke-RestMethod -Method Get -Uri $statusUrl -Headers @{ Authorization = "token $giteaToken" }
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Failed to query commit status for queue guard."
|
|
exit 1
|
|
}
|
|
|
|
$state = Get-QueueState -Payload $payload
|
|
Write-Host "[ci-queue-wait] state=$state purpose=$Purpose branch=$Branch"
|
|
|
|
switch ($state) {
|
|
"pending" {
|
|
Print-PendingContexts -Payload $payload
|
|
Start-Sleep -Seconds $IntervalSeconds
|
|
}
|
|
"no-status" {
|
|
if ($RequireStatus) {
|
|
Write-Error "No CI status contexts found while -RequireStatus is set."
|
|
exit 1
|
|
}
|
|
Write-Host "[ci-queue-wait] no status contexts present; proceeding."
|
|
exit 0
|
|
}
|
|
"terminal-success" { exit 0 }
|
|
"terminal-failure" { exit 0 }
|
|
"unknown" { exit 0 }
|
|
default { exit 0 }
|
|
}
|
|
}
|