chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
[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
}