# Neue Version automatisch auf den Keymaster hochladen (msco.mbm.wtf -> /download, Launcher-Updates). # Läuft nach dem Release-Build von selbst (MSCO.csproj bzw. MSCO.Launcher.csproj) - oder von Hand: # powershell -ExecutionPolicy Bypass -File release.ps1 -Product mod -File bin\Release\MSCO.dll # # Braucht den Release-Token des Keymasters (RELEASE_TOKEN in dessen .env), lokal verschlüsselt in # %AppData%\MSCO\release-token.dpapi (Windows DPAPI, nur dein Windows-Konto kann ihn lesen - anlegen mit # "release.ps1 -SaveToken"), sonst als Umgebungsvariable MSCO_RELEASE_TOKEN. Ohne Token wird nichts hochgeladen. # MSCO_NO_RELEASE=1 schaltet das Hochladen ab (Test-Builds). # Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen. param( [ValidateSet("mod", "launcher", "installer")][string]$Product, [string]$File, [switch]$SaveToken, [string]$Version = "", [string]$Changelog = "" ) $ErrorActionPreference = "Stop" $here = Split-Path -Parent $MyInvocation.MyCommand.Path Add-Type -AssemblyName System.Security $entropy = [Text.Encoding]::UTF8.GetBytes("MSCO release token v1") $scope = [Security.Cryptography.DataProtectionScope]::CurrentUser $tokenDpapi = Join-Path $env:APPDATA "MSCO\release-token.dpapi" # Token einmal verschlüsselt ablegen: fragt verdeckt danach (oder nimmt MSCO_RELEASE_TOKEN) if ($SaveToken) { $plain = $env:MSCO_RELEASE_TOKEN if (-not $plain) { $secure = Read-Host "Release token" -AsSecureString $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)) } if (-not $plain -or $plain.Length -lt 24) { Write-Host "Token too short - nothing saved"; exit 1 } New-Item -ItemType Directory -Force (Split-Path $tokenDpapi) | Out-Null $enc = [Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($plain), $entropy, $scope) [IO.File]::WriteAllBytes($tokenDpapi, $enc) Write-Host "Release token saved encrypted in $tokenDpapi" exit 0 } if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File | release.ps1 -SaveToken"; exit 1 } if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 } $token = $env:MSCO_RELEASE_TOKEN if (-not $token -and (Test-Path $tokenDpapi)) { try { $token = [Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($tokenDpapi), $entropy, $scope)) } catch { Write-Host "MSCO release: $tokenDpapi cannot be decrypted (other Windows user/PC?) - not uploading."; exit 0 } } $tokenFile = Join-Path $env:APPDATA "MSCO\release-token.txt" if (-not $token -and (Test-Path $tokenFile)) { $token = (Get-Content $tokenFile -Raw).Trim() } if (-not $token) { Write-Host "MSCO release: no release token ($tokenDpapi or MSCO_RELEASE_TOKEN) - not uploading." exit 0 } $keymaster = if ($env:MSCO_KEYMASTER) { $env:MSCO_KEYMASTER.TrimEnd("/") } else { "https://msco.mbm.wtf" } if (-not (Test-Path $File)) { Write-Host "MSCO release: $File not found"; exit 1 } # Version: aus MSCO.cs (Mod) bzw. aus der Datei (Produktversion) if (-not $Version) { $source = Join-Path $here "MSCO.cs" if ($Product -eq "mod" -and (Test-Path $source)) { $m = Select-String -Path $source -Pattern 'ModVersion\s*=\s*"([^"]+)"' | Select-Object -First 1 if ($m) { $Version = $m.Matches[0].Groups[1].Value } } if (-not $Version) { $Version = ((Get-Item $File).VersionInfo.ProductVersion -split '\+')[0] } } # Changelog: Abschnitt "## " aus CHANGELOG.md (Text ohne Markdown, gekürzt) if (-not $Changelog) { $log = Join-Path $here "CHANGELOG.md" if (Test-Path $log) { $lines = Get-Content $log -Encoding UTF8 $start = [Array]::FindIndex($lines, [Predicate[string]] { param($l) $l -match "^##\s+$([regex]::Escape($Version))\b" }) if ($start -ge 0) { $text = @() for ($i = $start + 1; $i -lt $lines.Count -and $lines[$i] -notmatch '^##\s'; $i++) { $l = $lines[$i].Trim() -replace '\*\*', '' -replace '`', '' if ($l) { $text += $l } } $Changelog = ($text -join " ") if ($Changelog.Length -gt 3500) { $Changelog = $Changelog.Substring(0, 3500) + " …" } } } } Add-Type -AssemblyName System.Net.Http $client = New-Object System.Net.Http.HttpClient $client.Timeout = [TimeSpan]::FromMinutes(10) $client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $token) # Multipart von Hand: Windows PowerShell (.NET Framework) schreibt name=... ohne Anführungszeichen und die Boundary in # Anführungszeichen - das lehnt der Parser des Keymasters (Node/undici) ab. $boundary = "----MSCO" + [Guid]::NewGuid().ToString("N") $utf8 = New-Object System.Text.UTF8Encoding($false) $body = New-Object System.IO.MemoryStream function Write-Text([string]$text) { $b = $utf8.GetBytes($text); $body.Write($b, 0, $b.Length) } foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog))) { Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"$($field[0])`"`r`nContent-Type: text/plain; charset=utf-8`r`n`r`n$($field[1])`r`n" } $bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File)) Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"$([System.IO.Path]::GetFileName($File))`"`r`nContent-Type: application/octet-stream`r`n`r`n" $body.Write($bytes, 0, $bytes.Length) Write-Text "`r`n--$boundary--`r`n" $form = New-Object System.Net.Http.ByteArrayContent(,$body.ToArray()) $form.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=$boundary") | Out-Null try { $res = $client.PostAsync("$keymaster/api/releases/upload", $form).GetAwaiter().GetResult() $body = $res.Content.ReadAsStringAsync().GetAwaiter().GetResult() } catch { Write-Host "MSCO release: keymaster not reachable ($($_.Exception.Message)) - not uploaded" exit 0 } switch ([int]$res.StatusCode) { 200 { Write-Host "MSCO release: $Product $Version uploaded to $keymaster" } 409 { Write-Host "MSCO release: $Product $Version is already on the keymaster - raise the version to publish a new one" } default { Write-Host "MSCO release: upload failed (HTTP $([int]$res.StatusCode)): $body" } } exit 0