# 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 } } }