|
| 1 | +#Requires -Version 7.0 |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + Pushes the current branch and, for gitlab.com remotes, auto-retries a pipeline that was |
| 5 | + killed by GitLab.com's compute-minutes quota gate at creation time. |
| 6 | +
|
| 7 | +.DESCRIPTION |
| 8 | + GitLab.com evaluates the namespace compute-minutes quota when a pipeline is CREATED (on a |
| 9 | + fresh push). When the namespace is over quota, the pipeline is marked 'failed' within ~1s |
| 10 | + with every job stuck in 'created' (started_at is null) - even when every job is tagged for a |
| 11 | + self-hosted runner fleet that does not consume shared minutes. Retrying the pipeline skips |
| 12 | + that creation-time gate, so the retry runs normally on the fleet. |
| 13 | +
|
| 14 | + This wrapper performs a normal 'git push', then (only for gitlab.com remotes) waits for the |
| 15 | + resulting pipeline and, if it died at the creation gate, retries it automatically via glab. |
| 16 | + For non-gitlab.com remotes it just pushes and returns. |
| 17 | +
|
| 18 | + Permanent fixes (GitLab-side, not automatable here): add compute minutes to the namespace, |
| 19 | + or wait for the monthly quota reset. |
| 20 | +
|
| 21 | +.PARAMETER Remote |
| 22 | + Git remote to push to. Defaults to 'origin'. |
| 23 | +
|
| 24 | +.PARAMETER RefSpec |
| 25 | + Optional branch or refspec to push (e.g. 'main'). Defaults to the current branch. |
| 26 | +
|
| 27 | +.PARAMETER WaitSeconds |
| 28 | + Seconds to wait between polls while the pipeline is being created. Defaults to 8. |
| 29 | +
|
| 30 | +.PARAMETER PollAttempts |
| 31 | + Number of polls to locate the created pipeline. Defaults to 6. |
| 32 | +
|
| 33 | +.EXAMPLE |
| 34 | + pwsh -File scripts/Push-Main.ps1 |
| 35 | + Pushes the current branch and auto-retries a creation-gated GitLab pipeline. |
| 36 | +
|
| 37 | +.EXAMPLE |
| 38 | + git pushm |
| 39 | + Same, via the optional alias: git config alias.pushm '!pwsh -File scripts/Push-Main.ps1' |
| 40 | +#> |
| 41 | +[CmdletBinding()] |
| 42 | +param( |
| 43 | + [string]$Remote = 'origin', |
| 44 | + [string]$RefSpec, |
| 45 | + [int]$WaitSeconds = 8, |
| 46 | + [int]$PollAttempts = 6 |
| 47 | +) |
| 48 | + |
| 49 | +Set-StrictMode -Version Latest |
| 50 | +$ErrorActionPreference = 'Stop' |
| 51 | + |
| 52 | +$pushArgs = @('push', $Remote) |
| 53 | +if ($RefSpec) { $pushArgs += $RefSpec } |
| 54 | +& git @pushArgs |
| 55 | +if ($LASTEXITCODE -ne 0) { throw 'git push failed.' } |
| 56 | + |
| 57 | +$remoteUrl = (& git remote get-url $Remote).Trim() |
| 58 | +if ($remoteUrl -notmatch 'gitlab\.com') { |
| 59 | + Write-Host "Remote '$Remote' is not gitlab.com; no pipeline retry workaround needed." |
| 60 | + return |
| 61 | +} |
| 62 | + |
| 63 | +if (-not (Get-Command glab -ErrorAction SilentlyContinue)) { |
| 64 | + Write-Warning 'glab CLI not found; cannot auto-retry. If the pipeline died at creation, retry it manually.' |
| 65 | + return |
| 66 | +} |
| 67 | + |
| 68 | +$sha = (& git rev-parse HEAD).Trim() |
| 69 | +$projectPath = ($remoteUrl -replace '^.*gitlab\.com[:/]', '') -replace '\.git$', '' |
| 70 | +$projectEnc = [uri]::EscapeDataString($projectPath) |
| 71 | + |
| 72 | +$pipeline = $null |
| 73 | +for ($attempt = 1; $attempt -le $PollAttempts; $attempt++) { |
| 74 | + Start-Sleep -Seconds $WaitSeconds |
| 75 | + $pipes = @(glab api "projects/$projectEnc/pipelines?sha=$sha&per_page=1" | ConvertFrom-Json) |
| 76 | + if ($pipes.Count -gt 0) { |
| 77 | + $pipeline = $pipes[0] |
| 78 | + if ($pipeline.status -in @('failed', 'running', 'success', 'pending')) { break } |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +if (-not $pipeline) { |
| 83 | + Write-Warning "No pipeline found for commit $sha after $($PollAttempts * $WaitSeconds)s. Check GitLab manually." |
| 84 | + return |
| 85 | +} |
| 86 | + |
| 87 | +if ($pipeline.status -ne 'failed') { |
| 88 | + Write-Host "Pipeline $($pipeline.id) status: $($pipeline.status). No creation-gate retry needed." |
| 89 | + return |
| 90 | +} |
| 91 | + |
| 92 | +# Distinguish a creation-time quota kill (all jobs still 'created') from a real job failure. |
| 93 | +$jobs = @(glab api "projects/$projectEnc/pipelines/$($pipeline.id)/jobs?per_page=100" | ConvertFrom-Json) |
| 94 | +$startedJobs = @($jobs | Where-Object { $_.status -ne 'created' }) |
| 95 | +if ($startedJobs.Count -gt 0) { |
| 96 | + Write-Warning "Pipeline $($pipeline.id) failed after jobs started - this is a real failure, not the creation gate. Inspect the jobs." |
| 97 | + return |
| 98 | +} |
| 99 | + |
| 100 | +Write-Host "Pipeline $($pipeline.id) died at the GitLab.com creation-time quota gate; retrying on the fleet..." |
| 101 | +$retry = glab api --method POST "projects/$projectEnc/pipelines/$($pipeline.id)/retry" | ConvertFrom-Json |
| 102 | +Write-Host "Retried pipeline $($retry.id) - status: $($retry.status)." |
0 commit comments