chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Remote = 'origin',
|
||||
[string]$BaseUrl,
|
||||
[string]$Owner,
|
||||
[string]$Repo,
|
||||
[string]$ApiToken,
|
||||
[string]$Ref = 'main',
|
||||
[string]$WorkflowId = 'ci.yml',
|
||||
[string]$RequiredLabel,
|
||||
[switch]$DispatchIfMissing,
|
||||
[switch]$Wait,
|
||||
[ValidateRange(30, 21600)]
|
||||
[int]$TimeoutSeconds = 1800,
|
||||
[ValidateRange(5, 300)]
|
||||
[int]$PollSeconds = 15
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Invoke-GitCapture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$ArgumentList
|
||||
)
|
||||
|
||||
$output = & git @ArgumentList 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git $($ArgumentList -join ' ') failed: $($output -join "`n")"
|
||||
}
|
||||
|
||||
return (($output | ForEach-Object { [string]$_ }) -join "`n").Trim()
|
||||
}
|
||||
|
||||
function Resolve-GiteaRepositoryContext {
|
||||
param(
|
||||
[string]$RemoteName,
|
||||
[string]$RemoteUrl
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RemoteUrl)) {
|
||||
$RemoteUrl = Invoke-GitCapture -ArgumentList @('remote', 'get-url', $RemoteName)
|
||||
}
|
||||
|
||||
$normalized = $RemoteUrl.Trim()
|
||||
if ($normalized -match '^(https?://[^/]+)/([^/]+)/([^/]+?)(?:\.git)?$') {
|
||||
return [pscustomobject]@{
|
||||
BaseUrl = $Matches[1]
|
||||
Owner = $Matches[2]
|
||||
Repo = $Matches[3]
|
||||
}
|
||||
}
|
||||
|
||||
if ($normalized -match '^ssh://git@([^/:]+)(?::(\d+))?/([^/]+)/([^/]+?)(?:\.git)?$') {
|
||||
$giteaHost = $Matches[1]
|
||||
$port = if ([string]::IsNullOrWhiteSpace($Matches[2])) { '' } else { ":$($Matches[2])" }
|
||||
return [pscustomobject]@{
|
||||
BaseUrl = "https://$giteaHost$port"
|
||||
Owner = $Matches[3]
|
||||
Repo = $Matches[4]
|
||||
}
|
||||
}
|
||||
|
||||
throw "Could not parse Gitea remote URL: $normalized"
|
||||
}
|
||||
|
||||
function Resolve-ApiToken {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedBaseUrl,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoOwner,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoName,
|
||||
[string]$ExplicitToken
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExplicitToken)) {
|
||||
return $ExplicitToken
|
||||
}
|
||||
|
||||
foreach ($name in @('GITEA_API_TOKEN', 'GITEA_TOKEN', 'GITHUB_TOKEN')) {
|
||||
$value = [Environment]::GetEnvironmentVariable($name)
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
$uri = [Uri]$ResolvedBaseUrl
|
||||
$lines = @(
|
||||
'protocol=https'
|
||||
"host=$($uri.Authority)"
|
||||
"path=$RepoOwner/$RepoName.git"
|
||||
''
|
||||
) | git credential fill
|
||||
$passwordLine = $lines | Where-Object { $_ -like 'password=*' } | Select-Object -First 1
|
||||
if ($null -eq $passwordLine) {
|
||||
throw 'Could not resolve a Gitea API token from the current environment or git credential manager.'
|
||||
}
|
||||
|
||||
return $passwordLine.Substring(9)
|
||||
}
|
||||
|
||||
function Invoke-GiteaApi {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('GET', 'POST')]
|
||||
[string]$Method,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Uri,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Token,
|
||||
[object]$Body
|
||||
)
|
||||
|
||||
$invokeArgs = @{
|
||||
Method = $Method
|
||||
Uri = $Uri
|
||||
Headers = @{ Authorization = "token $Token" }
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
if ($PSBoundParameters.ContainsKey('Body')) {
|
||||
$invokeArgs.ContentType = 'application/json'
|
||||
$invokeArgs.Body = $Body | ConvertTo-Json -Depth 8
|
||||
}
|
||||
|
||||
try {
|
||||
return Invoke-RestMethod @invokeArgs
|
||||
}
|
||||
catch {
|
||||
$response = $_.Exception.Response
|
||||
if ($null -eq $response) {
|
||||
throw
|
||||
}
|
||||
|
||||
$statusCode = [int]$response.StatusCode
|
||||
$payload = ''
|
||||
if ($response -is [System.Net.Http.HttpResponseMessage]) {
|
||||
if ($null -ne $_.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($_.ErrorDetails.Message)) {
|
||||
$payload = $_.ErrorDetails.Message
|
||||
}
|
||||
elseif ($null -ne $response.Content) {
|
||||
try {
|
||||
$payload = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||
}
|
||||
catch {
|
||||
$payload = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($response.PSObject.Methods.Name -contains 'GetResponseStream') {
|
||||
$stream = $response.GetResponseStream()
|
||||
if ($null -ne $stream) {
|
||||
$reader = [System.IO.StreamReader]::new($stream)
|
||||
try {
|
||||
$payload = $reader.ReadToEnd()
|
||||
}
|
||||
finally {
|
||||
$reader.Dispose()
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw "Gitea API $Method $Uri failed with HTTP ${statusCode}: $payload"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-OptionalProperty {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$InputObject,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$property = $InputObject.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Resolve-WorkflowRunnerLabel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WorkspaceRoot,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WorkflowFileName,
|
||||
[string]$ExplicitLabel
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExplicitLabel)) {
|
||||
return $ExplicitLabel
|
||||
}
|
||||
|
||||
$workflowPath = Join-Path $WorkspaceRoot (Join-Path '.gitea\workflows' $WorkflowFileName)
|
||||
if (-not (Test-Path -LiteralPath $workflowPath -PathType Leaf)) {
|
||||
throw "Workflow file not found: $workflowPath"
|
||||
}
|
||||
|
||||
$content = Get-Content -Raw -LiteralPath $workflowPath
|
||||
$match = [regex]::Match($content, '(?m)^\s*runs-on:\s*[''"]?([^''"\r\n\[\], ]+)')
|
||||
if (-not $match.Success) {
|
||||
throw "Could not resolve runs-on label from $workflowPath"
|
||||
}
|
||||
|
||||
return $match.Groups[1].Value
|
||||
}
|
||||
|
||||
function Find-HeadRun {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Runs,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$HeadSha,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExpectedWorkflowId
|
||||
)
|
||||
|
||||
$matching = @(
|
||||
$Runs.workflow_runs |
|
||||
Where-Object { $_.head_sha -eq $HeadSha } |
|
||||
Where-Object {
|
||||
$workflowId = ''
|
||||
$path = ''
|
||||
if ($_.PSObject.Properties.Name -contains 'workflow_id') {
|
||||
$workflowId = [string]$_.workflow_id
|
||||
}
|
||||
if ($_.PSObject.Properties.Name -contains 'path') {
|
||||
$path = [string]$_.path
|
||||
}
|
||||
|
||||
($workflowId -eq $ExpectedWorkflowId) -or
|
||||
($path -like "$ExpectedWorkflowId@*") -or
|
||||
[string]::IsNullOrWhiteSpace($workflowId)
|
||||
} |
|
||||
Sort-Object id -Descending
|
||||
)
|
||||
|
||||
if ($matching.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $matching[0]
|
||||
}
|
||||
|
||||
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
||||
Push-Location -LiteralPath $workspaceRoot
|
||||
|
||||
try {
|
||||
$context = Resolve-GiteaRepositoryContext -RemoteName $Remote
|
||||
if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
|
||||
$BaseUrl = $context.BaseUrl
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Owner)) {
|
||||
$Owner = $context.Owner
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Repo)) {
|
||||
$Repo = $context.Repo
|
||||
}
|
||||
|
||||
$ApiToken = Resolve-ApiToken -ResolvedBaseUrl $BaseUrl -RepoOwner $Owner -RepoName $Repo -ExplicitToken $ApiToken
|
||||
$requiredRunnerLabel = Resolve-WorkflowRunnerLabel -WorkspaceRoot $workspaceRoot -WorkflowFileName $WorkflowId -ExplicitLabel $RequiredLabel
|
||||
$apiBase = "$BaseUrl/api/v1/repos/$Owner/$Repo"
|
||||
$workflow = Invoke-GiteaApi -Method GET -Uri "$apiBase/actions/workflows/$WorkflowId" -Token $ApiToken
|
||||
if ($workflow.state -ne 'active') {
|
||||
throw "Workflow $WorkflowId is not active; current state is $($workflow.state)."
|
||||
}
|
||||
|
||||
$branch = Invoke-GiteaApi -Method GET -Uri "$apiBase/branches/$Ref" -Token $ApiToken
|
||||
$headSha = [string]$branch.commit.id
|
||||
if ([string]::IsNullOrWhiteSpace($headSha)) {
|
||||
throw "Could not resolve head SHA for ref $Ref."
|
||||
}
|
||||
|
||||
$runners = Invoke-GiteaApi -Method GET -Uri "$apiBase/actions/runners" -Token $ApiToken
|
||||
$matchingRunners = @(
|
||||
$runners.runners |
|
||||
Where-Object { $_.status -eq 'online' } |
|
||||
Where-Object {
|
||||
$labelNames = @($_.labels | ForEach-Object { $_.name })
|
||||
$labelNames -contains $requiredRunnerLabel
|
||||
}
|
||||
)
|
||||
if ($matchingRunners.Count -eq 0) {
|
||||
throw "No online Gitea runner exposes required label '$requiredRunnerLabel'."
|
||||
}
|
||||
|
||||
$runs = Invoke-GiteaApi -Method GET -Uri "$apiBase/actions/runs?limit=30" -Token $ApiToken
|
||||
$run = Find-HeadRun -Runs $runs -HeadSha $headSha -ExpectedWorkflowId $WorkflowId
|
||||
$dispatched = $false
|
||||
if ($null -eq $run -and $DispatchIfMissing) {
|
||||
Invoke-GiteaApi -Method POST -Uri "$apiBase/actions/workflows/$WorkflowId/dispatches" -Token $ApiToken -Body @{ ref = $Ref } | Out-Null
|
||||
$dispatched = $true
|
||||
}
|
||||
elseif ($null -eq $run) {
|
||||
throw "No Gitea Actions run found for $WorkflowId at $Ref ($headSha). Pass -DispatchIfMissing to trigger one."
|
||||
}
|
||||
|
||||
$deadline = [DateTimeOffset]::UtcNow.AddSeconds($TimeoutSeconds)
|
||||
while ($Wait -and ($null -eq $run -or $run.status -ne 'completed')) {
|
||||
if ([DateTimeOffset]::UtcNow -ge $deadline) {
|
||||
$status = if ($null -eq $run) { 'missing' } else { [string]$run.status }
|
||||
throw "Timed out waiting for $WorkflowId at $headSha; last status was $status."
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
$runs = Invoke-GiteaApi -Method GET -Uri "$apiBase/actions/runs?limit=30" -Token $ApiToken
|
||||
$run = Find-HeadRun -Runs $runs -HeadSha $headSha -ExpectedWorkflowId $WorkflowId
|
||||
}
|
||||
|
||||
if ($null -eq $run) {
|
||||
throw "No Gitea Actions run found for $WorkflowId at $Ref ($headSha)."
|
||||
}
|
||||
|
||||
$jobs = Invoke-GiteaApi -Method GET -Uri "$apiBase/actions/runs/$($run.id)/jobs" -Token $ApiToken
|
||||
$jobSummaries = @(
|
||||
$jobs.jobs | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
id = $_.id
|
||||
name = $_.name
|
||||
status = $_.status
|
||||
conclusion = Get-OptionalProperty -InputObject $_ -Name 'conclusion'
|
||||
runner_name = Get-OptionalProperty -InputObject $_ -Name 'runner_name'
|
||||
labels = @($_.labels)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$runConclusion = Get-OptionalProperty -InputObject $run -Name 'conclusion'
|
||||
if ($run.status -eq 'completed' -and $runConclusion -ne 'success') {
|
||||
throw "Gitea Actions run $($run.id) completed with conclusion '$runConclusion'."
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
ok = ($run.status -eq 'completed' -and $runConclusion -eq 'success')
|
||||
base_url = $BaseUrl
|
||||
owner = $Owner
|
||||
repo = $Repo
|
||||
ref = $Ref
|
||||
head_sha = $headSha
|
||||
workflow_id = $WorkflowId
|
||||
workflow_state = $workflow.state
|
||||
required_runner_label = $requiredRunnerLabel
|
||||
online_runner_count = $matchingRunners.Count
|
||||
dispatched = $dispatched
|
||||
run = [pscustomobject]@{
|
||||
id = $run.id
|
||||
run_number = $run.run_number
|
||||
event = $run.event
|
||||
status = $run.status
|
||||
conclusion = $runConclusion
|
||||
head_sha = $run.head_sha
|
||||
head_branch = $run.head_branch
|
||||
display_title = $run.display_title
|
||||
}
|
||||
jobs = $jobSummaries
|
||||
} | ConvertTo-Json -Depth 8
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user