281 lines
8.6 KiB
PowerShell
281 lines
8.6 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^v\d+\.\d+\.\d+.*$')]
|
|
[string]$Tag,
|
|
[string]$NotesPath,
|
|
[string]$BaseUrl,
|
|
[string]$Owner,
|
|
[string]$Repo,
|
|
[string]$ApiToken,
|
|
[string]$DistRoot = (Join-Path (Split-Path -Parent $PSScriptRoot) 'dist')
|
|
)
|
|
|
|
$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]$RemoteUrl
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($RemoteUrl)) {
|
|
$RemoteUrl = Invoke-GitCapture -ArgumentList @('remote', 'get-url', 'origin')
|
|
}
|
|
|
|
$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)?$') {
|
|
$scheme = 'https'
|
|
$giteaHost = $Matches[1]
|
|
$port = if ([string]::IsNullOrWhiteSpace($Matches[2])) { '' } else { ":$($Matches[2])" }
|
|
return [pscustomobject]@{
|
|
BaseUrl = "${scheme}://$giteaHost$port"
|
|
Owner = $Matches[3]
|
|
Repo = $Matches[4]
|
|
}
|
|
}
|
|
|
|
throw "Could not parse Gitea origin remote URL: $normalized"
|
|
}
|
|
|
|
function Resolve-ApiToken {
|
|
param(
|
|
[string]$BaseUrl,
|
|
[string]$Owner,
|
|
[string]$Repo,
|
|
[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]$BaseUrl
|
|
$lines = @(
|
|
'protocol=https'
|
|
"host=$($uri.Authority)"
|
|
"path=$Owner/$Repo.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)]
|
|
[string]$Method,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Uri,
|
|
[string]$Token,
|
|
[object]$Body
|
|
)
|
|
|
|
$headers = @{
|
|
Authorization = "token $Token"
|
|
}
|
|
|
|
$invokeArgs = @{
|
|
Method = $Method
|
|
Uri = $Uri
|
|
Headers = $headers
|
|
ErrorAction = 'Stop'
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('Body')) {
|
|
$invokeArgs.ContentType = 'application/json'
|
|
$invokeArgs.Body = ($Body | ConvertTo-Json -Depth 10)
|
|
}
|
|
|
|
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 Find-ReleaseArtifacts {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RootPath
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $RootPath)) {
|
|
throw "dist root not found: $RootPath"
|
|
}
|
|
|
|
$zip = Get-ChildItem -LiteralPath $RootPath -File -Filter 'MercuryToolbox-*-ReleaseFast.zip' |
|
|
Sort-Object LastWriteTimeUtc -Descending |
|
|
Select-Object -First 1
|
|
if ($null -eq $zip) {
|
|
throw "No packaged archive found under $RootPath. Run just package first."
|
|
}
|
|
|
|
$packageRoot = Join-Path $RootPath $zip.BaseName
|
|
if (-not (Test-Path -LiteralPath $packageRoot -PathType Container)) {
|
|
throw "Package directory matching archive was not found: $packageRoot"
|
|
}
|
|
|
|
$hashPath = Join-Path $packageRoot 'SHA256SUMS.txt'
|
|
$manifestPath = Join-Path $packageRoot 'mercury-toolbox-package.json'
|
|
foreach ($requiredPath in @($hashPath, $manifestPath)) {
|
|
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
|
throw "Expected packaged artifact not found: $requiredPath"
|
|
}
|
|
}
|
|
|
|
return @($zip.FullName, $hashPath, $manifestPath)
|
|
}
|
|
|
|
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
|
Push-Location -LiteralPath $workspaceRoot
|
|
|
|
try {
|
|
$context = Resolve-GiteaRepositoryContext
|
|
if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
|
|
$BaseUrl = $context.BaseUrl
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($Owner)) {
|
|
$Owner = $context.Owner
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($Repo)) {
|
|
$Repo = $context.Repo
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($NotesPath)) {
|
|
$NotesPath = Join-Path $workspaceRoot (Join-Path 'docs\releases' "$Tag.md")
|
|
}
|
|
$ApiToken = Resolve-ApiToken -BaseUrl $BaseUrl -Owner $Owner -Repo $Repo -ExplicitToken $ApiToken
|
|
if (-not (Test-Path -LiteralPath $NotesPath -PathType Leaf)) {
|
|
throw "Release notes not found: $NotesPath"
|
|
}
|
|
|
|
$notes = Get-Content -Raw -LiteralPath $NotesPath
|
|
$releaseName = "Mercury Toolbox $Tag"
|
|
$apiBase = "$BaseUrl/api/v1/repos/$Owner/$Repo"
|
|
$targetCommitish = Invoke-GitCapture -ArgumentList @('rev-parse', 'HEAD')
|
|
|
|
$release = $null
|
|
try {
|
|
$release = Invoke-GiteaApi -Method 'GET' -Uri "$apiBase/releases/tags/$Tag" -Token $ApiToken
|
|
}
|
|
catch {
|
|
if (-not $_.Exception.Message.Contains('HTTP 404')) {
|
|
throw
|
|
}
|
|
}
|
|
|
|
if ($null -eq $release) {
|
|
$release = Invoke-GiteaApi -Method 'POST' -Uri "$apiBase/releases" -Token $ApiToken -Body @{
|
|
tag_name = $Tag
|
|
target_commitish = $targetCommitish
|
|
name = $releaseName
|
|
body = $notes
|
|
draft = $false
|
|
prerelease = $false
|
|
}
|
|
}
|
|
else {
|
|
$release = Invoke-GiteaApi -Method 'PATCH' -Uri "$apiBase/releases/$($release.id)" -Token $ApiToken -Body @{
|
|
tag_name = $Tag
|
|
target_commitish = $targetCommitish
|
|
name = $releaseName
|
|
body = $notes
|
|
draft = $false
|
|
prerelease = $false
|
|
}
|
|
}
|
|
|
|
$assets = @()
|
|
if ($release.PSObject.Properties.Name -contains 'assets' -and $null -ne $release.assets) {
|
|
$assets = @($release.assets)
|
|
}
|
|
|
|
foreach ($assetPath in (Find-ReleaseArtifacts -RootPath $DistRoot)) {
|
|
$assetName = [System.IO.Path]::GetFileName($assetPath)
|
|
$existing = $assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
|
|
if ($null -ne $existing) {
|
|
Invoke-GiteaApi -Method 'DELETE' -Uri "$apiBase/releases/$($release.id)/assets/$($existing.id)" -Token $ApiToken | Out-Null
|
|
}
|
|
|
|
$uploadHeaders = @{
|
|
Authorization = "token $ApiToken"
|
|
}
|
|
$uploadUri = "$apiBase/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($assetName))"
|
|
Invoke-RestMethod -Method 'POST' -Uri $uploadUri -Headers $uploadHeaders -Form @{
|
|
attachment = Get-Item -LiteralPath $assetPath
|
|
} -ErrorAction Stop | Out-Null
|
|
Write-Host "Uploaded release asset: $assetName"
|
|
}
|
|
|
|
Write-Host "Release published: $($release.html_url)"
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|