Files
SyncVito/publish_gitea_release.ps1

202 lines
8.5 KiB
PowerShell

param(
[string]$Version = "",
[switch]$ValidateOnly
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Net.Http
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$BaseUrl = "https://gitea.davito.synology.me"
$Owner = "Davito"
$Repository = "SyncVito"
$LocalAppData = [Environment]::GetFolderPath("LocalApplicationData")
$TokenPaths = @(
(Join-Path $LocalAppData "SyncVito\developer\gitea-token.dpapi"),
(Join-Path $LocalAppData "MusicBrain\developer\gitea-token.dpapi")
)
if ([string]::IsNullOrWhiteSpace($Version)) {
$Version = (Get-Content -LiteralPath (Join-Path $Root "version.txt") -Raw -Encoding UTF8).Trim()
}
if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "Ungültige Release-Version: $Version" }
$Tag = "v$Version"
$ReleaseDir = Join-Path $Root "release"
$InstallerName = "SyncVitoSetup-$Version.exe"
$ChecksumName = "$InstallerName.sha256"
$ChangelogName = "CHANGELOG-$Version.md"
$AssetPaths = @(
(Join-Path $ReleaseDir $InstallerName),
(Join-Path $ReleaseDir $ChecksumName),
(Join-Path $ReleaseDir $ChangelogName)
)
foreach ($assetPath in $AssetPaths) {
if (-not (Test-Path -LiteralPath $assetPath -PathType Leaf)) { throw "Release-Datei fehlt: $assetPath" }
}
$actualHash = (Get-FileHash -LiteralPath $AssetPaths[0] -Algorithm SHA256).Hash.ToLowerInvariant()
$checksumText = (Get-Content -LiteralPath $AssetPaths[1] -Raw -Encoding UTF8).Trim()
$checksumPattern = '^([0-9a-fA-F]{64})\s+\*?' + [regex]::Escape($InstallerName) + '$'
if ($checksumText -notmatch $checksumPattern -or $Matches[1].ToLowerInvariant() -ne $actualHash) {
throw "SHA-256-Prüfung fehlgeschlagen. Der Release wird nicht hochgeladen."
}
$plainToken = $null
$tokenPointer = [IntPtr]::Zero
if (-not [string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
$plainToken = $env:GITEA_TOKEN.Trim()
}
else {
$tokenPath = $TokenPaths | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
if (-not $tokenPath) {
throw "Gitea-Token fehlt. Erwarte GITEA_TOKEN oder eine lokale DPAPI-Token-Datei."
}
try {
$secureToken = (Get-Content -LiteralPath $tokenPath -Raw -Encoding UTF8).Trim() | ConvertTo-SecureString
$tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)
$plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer)
}
catch {
throw "Der lokal gespeicherte Gitea-Token konnte nicht entschlüsselt werden: $($_.Exception.Message)"
}
}
$client = $null
function Read-ResponseJson {
param([System.Net.Http.HttpResponseMessage]$Response, [string]$Action)
$text = $Response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
if (-not $Response.IsSuccessStatusCode) {
$detail = $text
try {
$errorPayload = $text | ConvertFrom-Json
if ($errorPayload.message) { $detail = $errorPayload.message }
}
catch { }
throw "$Action fehlgeschlagen (HTTP $([int]$Response.StatusCode)): $detail"
}
if ([string]::IsNullOrWhiteSpace($text)) { return $null }
return $text | ConvertFrom-Json
}
function Send-GiteaJson {
param([string]$Method, [string]$Path, [object]$Body, [string]$Action)
$request = [System.Net.Http.HttpRequestMessage]::new(
[System.Net.Http.HttpMethod]::new($Method),
"$BaseUrl/api/v1$Path"
)
try {
if ($null -ne $Body) {
$json = $Body | ConvertTo-Json -Depth 8 -Compress
$request.Content = [System.Net.Http.StringContent]::new(
$json,
[System.Text.Encoding]::UTF8,
"application/json"
)
}
$response = $client.SendAsync($request).GetAwaiter().GetResult()
try { return Read-ResponseJson -Response $response -Action $Action }
finally { $response.Dispose() }
}
finally { $request.Dispose() }
}
try {
if ([string]::IsNullOrWhiteSpace($plainToken)) { throw "Der gespeicherte Gitea-Token ist leer." }
$client = [System.Net.Http.HttpClient]::new()
$client.Timeout = [TimeSpan]::FromMinutes(20)
$client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new("token", $plainToken)
$client.DefaultRequestHeaders.UserAgent.ParseAdd("SyncVito-Release-Publisher/$Version")
$client.DefaultRequestHeaders.Accept.ParseAdd("application/json")
$repositoryInfo = Send-GiteaJson -Method "GET" -Path "/repos/$Owner/$Repository" -Body $null -Action "Repository-Zugriff prüfen"
if (-not $repositoryInfo.permissions.push) { throw "Der Token besitzt keine Schreibrechte für $Owner/$Repository." }
if ($ValidateOnly) {
Write-Host "Gitea-Publisher ist bereit: $($repositoryInfo.full_name), Version $Version"
return
}
$encodedTag = [Uri]::EscapeDataString($Tag)
$release = $null
$lookup = [System.Net.Http.HttpRequestMessage]::new(
[System.Net.Http.HttpMethod]::Get,
"$BaseUrl/api/v1/repos/$Owner/$Repository/releases/tags/$encodedTag"
)
try {
$lookupResponse = $client.SendAsync($lookup).GetAwaiter().GetResult()
try {
if ($lookupResponse.IsSuccessStatusCode) {
$release = Read-ResponseJson -Response $lookupResponse -Action "Release lesen"
}
elseif ([int]$lookupResponse.StatusCode -ne 404) {
Read-ResponseJson -Response $lookupResponse -Action "Release prüfen" | Out-Null
}
}
finally { $lookupResponse.Dispose() }
}
finally { $lookup.Dispose() }
$releaseBody = (Get-Content -LiteralPath $AssetPaths[2] -Raw -Encoding UTF8).Trim()
$metadata = @{
tag_name = $Tag
target_commitish = "main"
name = "SyncVito $Version"
body = $releaseBody
draft = $false
prerelease = $false
}
if ($null -eq $release) {
Write-Host "Lege Gitea-Release $Tag an …"
$release = Send-GiteaJson -Method "POST" -Path "/repos/$Owner/$Repository/releases" -Body $metadata -Action "Release anlegen"
}
else {
Write-Host "Aktualisiere Gitea-Release $Tag …"
$release = Send-GiteaJson -Method "PATCH" -Path "/repos/$Owner/$Repository/releases/$($release.id)" -Body $metadata -Action "Release aktualisieren"
}
if (-not $release.id) { throw "Gitea hat keine Release-ID zurückgegeben." }
$localNames = @($AssetPaths | ForEach-Object { [System.IO.Path]::GetFileName($_) })
foreach ($existing in @($release.assets | Where-Object { $_.name -in $localNames })) {
Send-GiteaJson -Method "DELETE" -Path "/repos/$Owner/$Repository/releases/$($release.id)/assets/$($existing.id)" -Body $null -Action "Altes Asset entfernen" | Out-Null
}
foreach ($assetPath in $AssetPaths) {
$assetName = [System.IO.Path]::GetFileName($assetPath)
Write-Host "Lade hoch: $assetName"
$uploadUrl = "$BaseUrl/api/v1/repos/$Owner/$Repository/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($assetName))"
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $uploadUrl)
$multipart = [System.Net.Http.MultipartFormDataContent]::new()
$stream = [System.IO.File]::OpenRead($assetPath)
$content = [System.Net.Http.StreamContent]::new($stream)
$content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new("application/octet-stream")
$multipart.Add($content, "attachment", $assetName)
$request.Content = $multipart
try {
$response = $client.SendAsync($request).GetAwaiter().GetResult()
try { Read-ResponseJson -Response $response -Action "Upload von $assetName" | Out-Null }
finally { $response.Dispose() }
}
finally {
$request.Dispose()
$multipart.Dispose()
$content.Dispose()
$stream.Dispose()
}
}
$published = Send-GiteaJson -Method "GET" -Path "/repos/$Owner/$Repository/releases/tags/$encodedTag" -Body $null -Action "Release verifizieren"
$publishedNames = @($published.assets | ForEach-Object { $_.name })
foreach ($required in $localNames) {
if ($required -notin $publishedNames) { throw "Veröffentlichtes Asset fehlt: $required" }
}
Write-Host "Gitea-Release erfolgreich veröffentlicht: $($published.html_url)"
Write-Host "SHA256: $actualHash"
}
finally {
if ($client) { $client.Dispose() }
if ($tokenPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer) }
$plainToken = $null
Remove-Variable plainToken -ErrorAction SilentlyContinue
}