param( [string]$Version = "", [string]$Notes = "", [switch]$ValidateOnly ) # Veroeffentlicht den Installer aus dist\ als Gitea-Release (Tag v) samt SHA-256-Datei. # Token: Umgebungsvariable GITEA_TOKEN oder DPAPI-Datei %LOCALAPPDATA%\EuerChat\developer\gitea-token.dpapi # (ersatzweise die MusicBrain-Token-Datei). $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Net.Http $Root = Split-Path -Parent $MyInvocation.MyCommand.Path $BaseUrl = "https://gitea.davito.synology.me" $Owner = "Davito" $Repository = "EuerChat" $LocalAppData = [Environment]::GetFolderPath("LocalApplicationData") $TokenPaths = @( (Join-Path $LocalAppData "EuerChat\developer\gitea-token.dpapi"), (Join-Path $LocalAppData "MusicBrain\developer\gitea-token.dpapi") ) if ([string]::IsNullOrWhiteSpace($Version)) { $Version = (Get-Content -LiteralPath (Join-Path $Root "src-tauri\tauri.conf.json") -Raw -Encoding UTF8 | ConvertFrom-Json).version } if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "Ungueltige Release-Version: $Version" } $Tag = "v$Version" $InstallerName = "EuerChat_${Version}_x64-setup.exe" $ChecksumName = "$InstallerName.sha256" $InstallerPath = Join-Path $Root "dist\$InstallerName" $ChecksumPath = Join-Path $Root "dist\$ChecksumName" if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf)) { throw "Installer fehlt: $InstallerPath (zuerst build.bat ausfuehren)" } $sha256 = [System.Security.Cryptography.SHA256]::Create() $stream = [System.IO.File]::OpenRead($InstallerPath) try { $hash = -join ($sha256.ComputeHash($stream) | ForEach-Object { $_.ToString("x2") }) } finally { $stream.Dispose() $sha256.Dispose() } [System.IO.File]::WriteAllText($ChecksumPath, "$hash $InstallerName`n", [System.Text.UTF8Encoding]::new($false)) if ([string]::IsNullOrWhiteSpace($Notes)) { $notesFile = Join-Path $Root "RELEASE_NOTES.md" if (Test-Path -LiteralPath $notesFile) { $Notes = (Get-Content -LiteralPath $notesFile -Raw -Encoding UTF8).Trim() } } $plainToken = $env:GITEA_TOKEN if ([string]::IsNullOrWhiteSpace($plainToken)) { $tokenPath = $TokenPaths | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 if (-not $tokenPath) { throw "Gitea-Token fehlt. Erwartet: GITEA_TOKEN oder $($TokenPaths[0])" } $secure = (Get-Content -LiteralPath $tokenPath -Raw -Encoding UTF8).Trim() | ConvertTo-SecureString $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } } $client = [System.Net.Http.HttpClient]::new() $client.Timeout = [TimeSpan]::FromMinutes(20) $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new("token", $plainToken.Trim()) $client.DefaultRequestHeaders.UserAgent.ParseAdd("EuerChat-Release-Publisher/$Version") $client.DefaultRequestHeaders.Accept.ParseAdd("application/json") function Send-Gitea { param([string]$Method, [string]$Path, [object]$Body, [string]$Action, [switch]$AllowNotFound) $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), "$BaseUrl/api/v1$Path") try { if ($null -ne $Body) { $request.Content = [System.Net.Http.StringContent]::new( ($Body | ConvertTo-Json -Depth 8 -Compress), [System.Text.Encoding]::UTF8, "application/json") } $response = $client.SendAsync($request).GetAwaiter().GetResult() try { $text = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() if ($AllowNotFound -and [int]$response.StatusCode -eq 404) { return $null } if (-not $response.IsSuccessStatusCode) { throw "$Action fehlgeschlagen (HTTP $([int]$response.StatusCode)): $text" } if ([string]::IsNullOrWhiteSpace($text)) { return $null } return $text | ConvertFrom-Json } finally { $response.Dispose() } } finally { $request.Dispose() } } try { $repo = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository" -Action "Repository-Zugriff pruefen" if (-not $repo.permissions.push) { throw "Der Token besitzt keine Schreibrechte fuer $Owner/$Repository." } Write-Host "Repository: $($repo.full_name) Version: $Version SHA256: $hash" if ($ValidateOnly) { return } $encodedTag = [Uri]::EscapeDataString($Tag) $release = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/releases/tags/$encodedTag" -Action "Release lesen" -AllowNotFound if ($null -eq $release) { # Gitea datiert Releases nach dem Commit von main; ohne frischen Commit bleibt /releases/latest alt. $marker = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/contents/RELEASES.md?ref=main" -Action "Release-Marker lesen" -AllowNotFound $markerText = "# EuerChat Releases`n`nLetzter veroeffentlichter Release: v$Version ($([DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')) UTC)`n" $markerBody = @{ message = "Release v$Version" content = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($markerText)) branch = "main" } if ($marker) { $markerBody.sha = $marker.sha } $markerMethod = if ($marker) { "PUT" } else { "POST" } Send-Gitea -Method $markerMethod -Path "/repos/$Owner/$Repository/contents/RELEASES.md" -Body $markerBody -Action "Release-Marker-Commit anlegen" | Out-Null Write-Host "Lege Gitea-Release $Tag an..." $release = Send-Gitea -Method POST -Path "/repos/$Owner/$Repository/releases" -Action "Release anlegen" -Body @{ tag_name = $Tag target_commitish = "main" name = "EuerChat $Version" body = $Notes draft = $false prerelease = $false } } else { Write-Host "Setze vorhandenen Release $Tag fort." } $existing = @($release.assets | ForEach-Object { $_.name }) foreach ($assetPath in @($InstallerPath, $ChecksumPath)) { $assetName = [System.IO.Path]::GetFileName($assetPath) if ($assetName -in $existing) { Write-Host "Bereits vorhanden: $assetName" continue } Write-Host "Lade hoch: $assetName" $uploadUrl = "$BaseUrl/api/v1/repos/$Owner/$Repository/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($assetName))" $multipart = [System.Net.Http.MultipartFormDataContent]::new() $fileStream = [System.IO.File]::OpenRead($assetPath) $fileContent = [System.Net.Http.StreamContent]::new($fileStream) $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new("application/octet-stream") $multipart.Add($fileContent, "attachment", $assetName) $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $uploadUrl) $request.Content = $multipart try { $response = $client.SendAsync($request).GetAwaiter().GetResult() try { if (-not $response.IsSuccessStatusCode) { throw "Upload von $assetName fehlgeschlagen (HTTP $([int]$response.StatusCode)): $($response.Content.ReadAsStringAsync().GetAwaiter().GetResult())" } } finally { $response.Dispose() } } finally { $request.Dispose(); $multipart.Dispose(); $fileContent.Dispose(); $fileStream.Dispose() } } $published = Send-Gitea -Method GET -Path "/repos/$Owner/$Repository/releases/tags/$encodedTag" -Action "Veroeffentlichten Release pruefen" $names = @($published.assets | ForEach-Object { $_.name }) foreach ($required in @($InstallerName, $ChecksumName)) { if ($required -notin $names) { throw "Release wurde angelegt, aber das Asset fehlt: $required" } } Write-Host "Fertig: $BaseUrl/$Owner/$Repository/releases/tag/$Tag" } finally { $client.Dispose() }