Files
VitosUpdater/scripts/release.ps1
T

404 lines
18 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Baut, signiert und veröffentlicht eine neue VitosUpdater-Version auf Gitea.
.DESCRIPTION
1. Prüft Git-Zustand, cargo check, cargo clippy, cargo test und svelte-check
2. Erhöht die Version in tauri.conf.json, Cargo.toml und package.json
3. Baut den NSIS-Installer samt signiertem Updater-Artefakt (.sig)
4. Erzeugt latest.json
5. Committet die Version, setzt den Tag vX.Y.Z und pusht beides
6. Erstellt das Gitea-Release vX.Y.Z und lädt Installer, .sig und latest.json hoch
7. Aktualisiert das rollende Release "latest"
8. Prüft die öffentliche latest.json und die Installer-URL
Voraussetzungen: Umgebungsvariable GITEA_TOKEN (oder Datei .env.local mit GITEA_TOKEN=...),
Signierschlüssel unter %USERPROFILE%\.vitosupdater\updater.key (oder TAURI_SIGNING_PRIVATE_KEY[_PATH]).
.PARAMETER Bump
Art der Versionserhöhung: patch (Standard), minor oder major.
.PARAMETER Version
Explizite Version x.y.z (überschreibt -Bump).
.PARAMETER NoPublish
Nur bauen und latest.json erzeugen. Es wird nichts committet, getaggt, gepusht oder hochgeladen;
die Versionsänderung wird danach zurückgenommen. Die Ergebnisse liegen in release-out\.
.PARAMETER SkipChecks
Überspringt cargo check/clippy/test und svelte-check (nur für schnelle Wiederholungen).
#>
[CmdletBinding()]
param(
[ValidateSet('patch', 'minor', 'major')]
[string]$Bump = 'patch',
[string]$Version,
[switch]$NoPublish,
[switch]$SkipChecks
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.Encoding]::UTF8
$OutputEncoding = [Text.Encoding]::UTF8
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
$GiteaBase = 'https://gitea.davito.synology.me'
$Owner = 'Davito'
$Repo = 'VitosUpdater'
$Branch = 'main'
$Root = Split-Path -Parent $PSScriptRoot
$OutDir = Join-Path $Root 'release-out'
$Utf8NoBom = New-Object Text.UTF8Encoding($false)
$VersionFiles = @('src-tauri/tauri.conf.json', 'src-tauri/Cargo.toml', 'src-tauri/Cargo.lock', 'package.json', 'package-lock.json')
$script:Token = $null
$script:Committed = $false
$script:Bumped = $false
function Write-Step([string]$Text) { Write-Host ''; Write-Host "==> $Text" -ForegroundColor Cyan }
function Write-Ok([string]$Text) { Write-Host " [OK] $Text" -ForegroundColor Green }
function Write-Info([string]$Text) { Write-Host " $Text" -ForegroundColor Gray }
function Invoke-Native([string]$Description, [scriptblock]$Command) {
& $Command
if ($LASTEXITCODE -ne 0) { throw "$Description ist fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
}
function Get-GitOutput([string[]]$GitArgs) {
$result = & git @GitArgs
if ($LASTEXITCODE -ne 0) { throw "git $($GitArgs -join ' ') ist fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
return $result
}
function Read-Token {
if ($env:GITEA_TOKEN) { return $env:GITEA_TOKEN.Trim() }
$file = Join-Path $Root '.env.local'
if (Test-Path $file) {
foreach ($line in Get-Content $file -Encoding UTF8) {
if ($line -match '^\s*GITEA_TOKEN\s*=\s*(.+?)\s*$') { return $Matches[1].Trim('"', "'") }
}
}
return $null
}
function Show-TokenHelp {
Write-Host ''
Write-Host 'Es wurde kein Gitea-Zugriffstoken gefunden.' -ForegroundColor Yellow
Write-Host ' 1. In Gitea anmelden: Einstellungen -> Anwendungen -> Zugriffstoken'
Write-Host ' 2. Neues Token erzeugen, Berechtigung: Repository = Lesen und Schreiben (write:repository)'
Write-Host ' 3. Token hinterlegen, entweder'
Write-Host ' dauerhaft: setx GITEA_TOKEN "<token>" (danach neues Terminal öffnen)'
Write-Host " oder lokal: Datei .env.local im Projektordner mit der Zeile GITEA_TOKEN=<token>"
Write-Host ' (.env.local ist in .gitignore eingetragen und wird nie eingecheckt.)'
}
function Get-CurrentVersion {
$conf = Get-Content (Join-Path $Root 'src-tauri/tauri.conf.json') -Raw -Encoding UTF8 | ConvertFrom-Json
return [string]$conf.version
}
function Get-NextVersion([string]$Current) {
if ($Version) {
if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "Ungültige Version '$Version'. Erwartet wird x.y.z." }
return $Version
}
if ($Current -notmatch '^(\d+)\.(\d+)\.(\d+)$') { throw "Aktuelle Version '$Current' ist keine einfache SemVer-Version." }
$major = [int]$Matches[1]; $minor = [int]$Matches[2]; $patch = [int]$Matches[3]
switch ($Bump) {
'major' { return '{0}.0.0' -f ($major + 1) }
'minor' { return '{0}.{1}.0' -f $major, ($minor + 1) }
default { return '{0}.{1}.{2}' -f $major, $minor, ($patch + 1) }
}
}
function Set-FirstMatch([string]$Path, [string]$Pattern, [string]$Replacement) {
$full = Join-Path $Root $Path
$text = [IO.File]::ReadAllText($full, $Utf8NoBom)
$regex = New-Object Text.RegularExpressions.Regex($Pattern, [Text.RegularExpressions.RegexOptions]::Multiline)
if (-not $regex.IsMatch($text)) { throw "Versionszeile in $Path nicht gefunden." }
[IO.File]::WriteAllText($full, $regex.Replace($text, $Replacement, 1), $Utf8NoBom)
}
function Set-ProjectVersion([string]$New) {
Set-FirstMatch 'src-tauri/tauri.conf.json' '^( "version": ")[^"]+(")' ('${1}' + $New + '${2}')
Set-FirstMatch 'src-tauri/Cargo.toml' '^(version = ")[^"]+(")' ('${1}' + $New + '${2}')
Invoke-Native 'npm version' { npm version $New --no-git-tag-version --allow-same-version | Out-Null }
}
function Undo-VersionChange {
if ($script:Bumped -and -not $script:Committed) {
Write-Info 'Versionsänderung wird zurückgenommen ...'
& git checkout -- @VersionFiles 2>$null | Out-Null
$script:Bumped = $false
}
}
function Get-ReleaseNotes([string]$NewVersion) {
$lastTag = $null
$found = & git describe --tags --abbrev=0 --match 'v*' 2>$null
if ($LASTEXITCODE -eq 0 -and $found) { $lastTag = [string]$found }
$range = 'HEAD'
if ($lastTag) { $range = "$lastTag..HEAD" }
$lines = & git -c core.quotepath=off log $range --no-merges --pretty=format:'- %s (%h)' --grep='^Release v' --invert-grep
if ($LASTEXITCODE -ne 0) { throw 'git log ist fehlgeschlagen.' }
$text = ($lines | Where-Object { $_ }) -join "`n"
if (-not $text) { $text = '- Interne Verbesserungen und Fehlerbehebungen' }
return $text
}
# ---------- Gitea-API ----------
function Invoke-Gitea([string]$Method, [string]$Path, $Body = $null) {
$uri = "$GiteaBase/api/v1/repos/$Owner/$Repo$Path"
$headers = @{ Authorization = "token $script:Token"; Accept = 'application/json' }
if ($null -ne $Body) {
$bytes = [Text.Encoding]::UTF8.GetBytes(($Body | ConvertTo-Json -Depth 6))
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $bytes
}
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers
}
function Get-ReleaseByTag([string]$Tag) {
try {
return Invoke-Gitea 'GET' "/releases/tags/$Tag"
} catch {
$status = $null
if ($_.Exception.PSObject.Properties.Name -contains 'Response' -and $_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
if ($status -eq 404) { return $null }
throw
}
}
function Send-Asset([int]$ReleaseId, [string]$File, [string]$Name) {
$url = "$GiteaBase/api/v1/repos/$Owner/$Repo/releases/$ReleaseId/assets?name=" + [uri]::EscapeDataString($Name)
# Das Token wird über stdin an curl übergeben, damit es nicht in der Prozessliste erscheint.
$config = 'header = "Authorization: token ' + $script:Token + '"'
$response = $config | & curl.exe --silent --show-error --fail --config - -X POST -F "attachment=@$File" $url
if ($LASTEXITCODE -ne 0) { throw "Upload von '$Name' ist fehlgeschlagen (curl-Exit-Code $LASTEXITCODE)." }
Write-Ok "Hochgeladen: $Name ($([math]::Round((Get-Item $File).Length / 1MB, 1)) MB)"
}
function Clear-ReleaseAssets([int]$ReleaseId) {
$release = Invoke-Gitea 'GET' "/releases/$ReleaseId"
foreach ($asset in @($release.assets)) {
Invoke-Gitea 'DELETE' "/releases/$ReleaseId/assets/$($asset.id)" | Out-Null
Write-Info "Altes Asset entfernt: $($asset.name)"
}
}
function Get-HttpStatus([string]$Url) {
$code = & curl.exe --silent --location --head --output NUL --write-out '%{http_code}' $Url
if ($code -eq '405' -or $code -eq '000') {
$code = & curl.exe --silent --location --range 0-0 --output NUL --write-out '%{http_code}' $Url
}
return [string]$code
}
# ---------- Ablauf ----------
Set-Location $Root
$started = Get-Date
try {
Write-Host 'VitosUpdater – Release-Skript' -ForegroundColor White
# --- 1. Voraussetzungen und Prüfungen ---
Write-Step '1/8 Voraussetzungen und Qualitätsprüfungen'
if (-not $NoPublish) {
$script:Token = Read-Token
if (-not $script:Token) { Show-TokenHelp; throw 'GITEA_TOKEN fehlt.' }
Write-Ok 'Gitea-Token gefunden (wird nicht angezeigt)'
}
$keyPath = Join-Path $env:USERPROFILE '.vitosupdater\updater.key'
if (-not $env:TAURI_SIGNING_PRIVATE_KEY -and -not $env:TAURI_SIGNING_PRIVATE_KEY_PATH) {
if (-not (Test-Path $keyPath)) {
throw "Signierschlüssel nicht gefunden: $keyPath. Erzeugen mit: cargo tauri signer generate -w `"$keyPath`" (öffentlichen Schlüssel danach in tauri.conf.json eintragen)."
}
$env:TAURI_SIGNING_PRIVATE_KEY_PATH = $keyPath
}
Write-Ok 'Signierschlüssel gefunden'
$dirty = & git status --porcelain
if ($LASTEXITCODE -ne 0) { throw 'git status ist fehlgeschlagen. Ist dies ein Git-Repository?' }
if ($dirty) {
Write-Host ($dirty -join "`n") -ForegroundColor Yellow
throw 'Das Arbeitsverzeichnis ist nicht sauber. Bitte zuerst committen oder verwerfen.'
}
$branch = (Get-GitOutput @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -First 1).Trim()
if ($branch -ne $Branch) { throw "Es muss der Branch '$Branch' ausgecheckt sein (aktuell: '$branch')." }
if (-not $NoPublish) {
Invoke-Native 'git fetch' { git fetch origin --tags --force --quiet }
$remoteMain = & git rev-parse --verify --quiet "refs/remotes/origin/$Branch"
if ($LASTEXITCODE -eq 0 -and $remoteMain) {
$behind = [int](Get-GitOutput @('rev-list', '--count', "HEAD..origin/$Branch") | Select-Object -First 1)
if ($behind -gt 0) { throw "Der lokale Branch liegt $behind Commit(s) hinter origin/$Branch. Bitte zuerst git pull ausführen." }
}
}
Write-Ok 'Git-Zustand ist sauber'
if ($SkipChecks) {
Write-Info 'Prüfungen werden übersprungen (-SkipChecks).'
} else {
Push-Location (Join-Path $Root 'src-tauri')
try {
Invoke-Native 'cargo check' { cargo check --all-targets }
Write-Ok 'cargo check'
Invoke-Native 'cargo clippy' { cargo clippy --all-targets -- -D warnings }
Write-Ok 'cargo clippy (ohne Warnungen)'
Invoke-Native 'cargo test' { cargo test }
Write-Ok 'cargo test'
} finally { Pop-Location }
Invoke-Native 'svelte-check' { npm run check }
Write-Ok 'svelte-check'
}
# --- 2. Version erhöhen ---
Write-Step '2/8 Version erhöhen'
$current = Get-CurrentVersion
$new = Get-NextVersion $current
if (-not $NoPublish) {
$existingTag = & git tag --list "v$new"
if ($existingTag) { throw "Der Tag v$new existiert bereits. Bitte eine höhere Version wählen." }
if ([version]$new -le [version]$current) { throw "Die neue Version $new ist nicht größer als die aktuelle Version $current." }
}
$script:Bumped = $true
Set-ProjectVersion $new
Write-Ok "Version $current -> $new (tauri.conf.json, Cargo.toml, package.json)"
# --- 3. Bauen ---
Write-Step '3/8 Installer bauen und signieren (cargo tauri build)'
$env:CI = 'true'
Invoke-Native 'cargo tauri build' { cargo tauri build }
$nsisDir = Join-Path $Root 'src-tauri/target/release/bundle/nsis'
$installer = Get-ChildItem $nsisDir -Filter "*_${new}_*-setup.exe" | Select-Object -First 1
if (-not $installer) { throw "Installer für Version $new wurde nicht gefunden in $nsisDir." }
$sigFile = "$($installer.FullName).sig"
if (-not (Test-Path $sigFile)) { throw "Signaturdatei fehlt: $sigFile. Wurde createUpdaterArtifacts aktiviert und der Schlüssel gefunden?" }
Write-Ok "Installer: $($installer.Name) ($([math]::Round($installer.Length / 1MB, 1)) MB)"
Write-Ok 'Signatur (.sig) erzeugt'
# --- 4. latest.json ---
Write-Step '4/8 latest.json erzeugen'
$notes = Get-ReleaseNotes $new
$assetUrl = "$GiteaBase/$Owner/$Repo/releases/download/v$new/$($installer.Name)"
$manifest = [ordered]@{
version = $new
notes = $notes
pub_date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
platforms = [ordered]@{
'windows-x86_64' = [ordered]@{
signature = (Get-Content $sigFile -Raw -Encoding UTF8).Trim()
url = $assetUrl
size = [int64]$installer.Length
}
}
}
if (Test-Path $OutDir) { Remove-Item $OutDir -Recurse -Force }
New-Item -ItemType Directory -Path $OutDir | Out-Null
$latestFile = Join-Path $OutDir 'latest.json'
[IO.File]::WriteAllText($latestFile, ($manifest | ConvertTo-Json -Depth 6), $Utf8NoBom)
Copy-Item $installer.FullName $OutDir
Copy-Item $sigFile $OutDir
$outInstaller = Join-Path $OutDir $installer.Name
$outSig = Join-Path $OutDir "$($installer.Name).sig"
Write-Ok "latest.json -> $latestFile"
if ($NoPublish) {
Undo-VersionChange
Write-Step 'Fertig (-NoPublish)'
Write-Ok "Version $new gebaut. Dateien liegen in: $OutDir"
return
}
# --- 5. Commit, Tag, Push ---
Write-Step "5/8 Commit, Tag v$new und Push"
Invoke-Native 'git add' { git add -- @VersionFiles }
Invoke-Native 'git commit' { git commit --quiet -m "Release v$new" }
$script:Committed = $true
Invoke-Native 'git tag' { git tag -a "v$new" -m "Release v$new" }
Invoke-Native 'git push (Branch)' { git push origin $Branch }
Invoke-Native 'git push (Tag)' { git push origin "v$new" }
Write-Ok "Commit und Tag v$new gepusht"
# --- 6. Gitea-Release ---
Write-Step "6/8 Gitea-Release v$new erstellen"
$release = Get-ReleaseByTag "v$new"
if ($release) {
Write-Info 'Release existiert bereits, Assets werden ersetzt.'
Clear-ReleaseAssets $release.id
} else {
$release = Invoke-Gitea 'POST' '/releases' @{
tag_name = "v$new"
target_commitish = $Branch
name = "VitosUpdater $new"
body = $notes
draft = $false
prerelease = $false
}
}
Write-Ok "Release angelegt: $($release.html_url)"
Send-Asset $release.id $outInstaller $installer.Name
Send-Asset $release.id $outSig "$($installer.Name).sig"
Send-Asset $release.id $latestFile 'latest.json'
# --- 7. Rollendes Release "latest" ---
Write-Step '7/8 Rollendes Release "latest" aktualisieren'
Invoke-Native 'git tag latest' { git tag -f latest HEAD | Out-Null }
Invoke-Native 'git push latest' { git push --force origin refs/tags/latest }
$rolling = Get-ReleaseByTag 'latest'
$rollingBody = "Aktuelle Version: **$new**`n`n$notes"
if ($rolling) {
Invoke-Gitea 'PATCH' "/releases/$($rolling.id)" @{ name = "Aktuellste Version ($new)"; body = $rollingBody } | Out-Null
Clear-ReleaseAssets $rolling.id
} else {
$rolling = Invoke-Gitea 'POST' '/releases' @{
tag_name = 'latest'
target_commitish = $Branch
name = "Aktuellste Version ($new)"
body = $rollingBody
draft = $false
prerelease = $false
}
}
Send-Asset $rolling.id $outInstaller $installer.Name
Send-Asset $rolling.id $outSig "$($installer.Name).sig"
Send-Asset $rolling.id $latestFile 'latest.json'
Write-Ok 'Rollendes Release aktualisiert'
# --- 8. Prüfung ---
Write-Step '8/8 Veröffentlichung prüfen'
$latestUrl = "$GiteaBase/$Owner/$Repo/releases/download/latest/latest.json"
$raw = Invoke-WebRequest -Uri $latestUrl -UseBasicParsing
$content = $raw.Content
if ($content -is [byte[]]) { $content = [Text.Encoding]::UTF8.GetString($content) }
$remote = $content | ConvertFrom-Json
if ($remote.version -ne $new) { throw "Öffentliche latest.json enthält Version $($remote.version), erwartet wurde $new." }
Write-Ok "latest.json öffentlich erreichbar, Version $($remote.version)"
$platform = $remote.platforms.'windows-x86_64'
$status = Get-HttpStatus $platform.url
if ($status -ne '200' -and $status -ne '206') { throw "Installer-URL antwortet mit HTTP ${status}: $($platform.url)" }
Write-Ok "Installer-URL antwortet mit HTTP $status"
$rollingStatus = Get-HttpStatus "$GiteaBase/$Owner/$Repo/releases/download/latest/$($installer.Name)"
if ($rollingStatus -ne '200' -and $rollingStatus -ne '206') { throw "Installer im Release 'latest' antwortet mit HTTP $rollingStatus." }
Write-Ok "Installer im Release 'latest' antwortet mit HTTP $rollingStatus"
$elapsed = [math]::Round(((Get-Date) - $started).TotalMinutes, 1)
Write-Host ''
Write-Host '==================== Zusammenfassung ====================' -ForegroundColor Green
Write-Host " Version: $new (vorher $current)"
Write-Host " Release: $($release.html_url)"
Write-Host " Rollendes Release: $GiteaBase/$Owner/$Repo/releases/tag/latest"
Write-Host " Update-Endpunkt: $latestUrl"
Write-Host " Installer: $($platform.url)"
Write-Host " Dauer: $elapsed Minuten"
Write-Host '==========================================================' -ForegroundColor Green
}
catch {
Write-Host ''
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($script:Committed) {
Write-Host 'Der Versions-Commit wurde bereits lokal erstellt. Nach der Fehlerbehebung zurücknehmen mit: git tag -d v<Version>; git reset --hard HEAD~1 (bereits gepushte Teile ggf. auf Gitea prüfen), danach das Skript erneut starten.' -ForegroundColor Yellow
} else {
Undo-VersionChange
}
exit 1
}