Erste Version: Tauri-Grundgerüst, WinGet-Anbindung, Selbst-Updater und Release-Skript

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
DavitoandCopilot committed 2026-10-30 15:58:58 +01:00
commit e0773ba299
84 files changed
+12688

No files matched your search

+403
View File
@@ -0,0 +1,403 @@
<#
.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
}