refactor: retire production updater 3.3 worker

This commit is contained in:
Masterluke77
2026-07-10 14:43:04 +02:00
parent 3818cee11f
commit 14f1a0fa20
-334
View File
@@ -1,334 +0,0 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$StatusFile,
[Parameter(Mandatory=$true)][string]$EventFile,
[Parameter(Mandatory=$true)][string]$InputFile,
[Parameter(Mandatory=$true)][string]$CancelFile,
[Parameter(Mandatory=$true)][string]$SessionFile,
[Parameter(Mandatory=$true)][string]$LogFile,
[switch]$SkipProductionDeploy,
[switch]$SelfTest,
[string]$SelfTestFailAt = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
Add-Type -AssemblyName System.Net.Http
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.3.0';$PublicBaseUrl='https://vendoo.flatlined.de'
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion";New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$Branch="release/$TargetVersion-production-update-v3";$CloneDir=Join-Path $WorkRoot 'repository'
$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date
$script:Steps=@(
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},[ordered]@{id='merge';title='Sicher mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
function Write-Event([string]$Level,[string]$Message){
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
}
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 8
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
}
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
function Save-Session($Data){if(-not($Data.PSObject.Properties.Name -contains 'updaterVersion')){$Data|Add-Member -NotePropertyName updaterVersion -NotePropertyValue $UpdaterVersion};$Data.updaterVersion=$UpdaterVersion;$Data.updatedAt=(Get-Date).ToString('o');$tmp="$SessionFile.tmp";$Data|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $tmp -Encoding UTF8;Move-Item $tmp $SessionFile -Force}
function Load-Session{if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.version-eq$TargetVersion -and [string]$saved.updaterVersion-eq$UpdaterVersion){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}};return [pscustomobject]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;branch=$Branch;prUrl='';merged=$false;runId='';stage='new';updatedAt=''}}
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
function Stop-Tree([int]$ProcessId){
try { & taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null }
catch { try { Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue } catch {} }
}
function Get-TailText([string]$Text,[int]$MaxLength){
if([string]::IsNullOrWhiteSpace($Text)){return ''}
$value=$Text.Trim()
if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)}
return $value
}
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){
$parts=New-Object Collections.Generic.List[string]
$errorTail=Get-TailText $StdErr 1800
$outputTail=Get-TailText $StdOut 900
if($errorTail){$parts.Add("Fehlerausgabe:`n$errorTail")}
if($outputTail){$parts.Add("Letzte Standardausgabe:`n$outputTail")}
if($parts.Count -eq 0){return 'Der Prozess lieferte keine Diagnoseausgabe.'}
return ($parts -join "`n`n")
}
function Invoke-External{
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
Assert-NotCancelled
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg ([string]$_)}) -join ' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
# Node.js, npm and Git emit UTF-8 when redirected. Windows PowerShell otherwise decodes
# redirected streams with the active OEM code page, which corrupts German umlauts.
$utf8NoBom=New-Object Text.UTF8Encoding($false)
if($psi.PSObject.Properties.Name -contains 'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8NoBom}
if($psi.PSObject.Properties.Name -contains 'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8NoBom}
$psi.EnvironmentVariables['NO_COLOR']='1'
$psi.EnvironmentVariables['FORCE_COLOR']='0'
$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false'
$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi
if(-not $Quiet){Write-Event 'INFO' "$Label gestartet."}
if(-not $p.Start()){throw "$Label konnte nicht gestartet werden."}
$outTask = $p.StandardOutput.ReadToEndAsync()
$errTask = $p.StandardError.ReadToEndAsync()
if ($InputText) { $p.StandardInput.Write($InputText) }
$p.StandardInput.Close()
$sw=[Diagnostics.Stopwatch]::StartNew()
while(-not $p.WaitForExit(500)){
if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}
if($sw.Elapsed.TotalSeconds -gt $TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."}
if(-not $Quiet -and ((Get-Date)-$script:LastHeartbeat).TotalSeconds -ge 8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}." -f $sw.Elapsed);$script:LastHeartbeat=Get-Date}
}
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
if(-not $Quiet){
foreach($line in @($stdout -split "`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}}
foreach($line in @($stderr -split "`r?`n")){
$clean=$line.Trim()
if(-not $clean){continue}
if($code -ne 0){Write-Event 'ERROR' $clean}
elseif($clean -match '(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}
else{Write-Event 'OUTPUT' $clean}
}
}
if(-not $AllowFailure -and $code -ne 0){$summary=Get-ProcessFailureSummary $stderr $stdout;throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$summary"}
return [pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
}
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){
if($script:Tools.ContainsKey($Name) -and (Test-Path -LiteralPath $script:Tools[$Name])){return $script:Tools[$Name]}
foreach($c in @($Candidates)){if($c -and (Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return $script:Tools[$Name]}}
$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd -and $cmd.Source -and (Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return $cmd.Source};return $null
}
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds (2*$i)}}}
function Download-File([string]$Uri,[string]$Destination,[string]$Label){
for($attempt=1;$attempt-le3;$attempt++){try{
$handler = New-Object Net.Http.HttpClientHandler
$client = [Net.Http.HttpClient]::new($handler)
$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion")
$resp = $client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
$null = $resp.EnsureSuccessStatusCode()
$total=$resp.Content.Headers.ContentLength;$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None)
try{
$buf=New-Object byte[] 65536;$readTotal=0L;$lastReported=-10
while(($read=$input.Read($buf,0,$buf.Length))-gt0){
Assert-NotCancelled;$output.Write($buf,0,$read);$readTotal+=$read
if($total -and $total -gt 0){
$percent=[Math]::Min(100,[int][Math]::Floor(($readTotal*100.0)/$total))
if($percent -ge ($lastReported+10) -or $readTotal -ge $total){
$bucket=[Math]::Min(100,[int]([Math]::Floor($percent/10)*10))
if($bucket -gt $lastReported){
$lastReported=$bucket
Write-Event 'INFO' ("${Label}: {0}% ({1:N1} von {2:N1} MB)" -f $bucket,($readTotal/1048576),($total/1048576))
Write-State 18 'GitHub CLI' ("${Label} wird geladen: {0}%" -f $bucket)
}
}
}
}
}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()}
if(-not $total){Write-Event 'INFO' "${Label}: Download abgeschlossen."}
return
}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Write-Event 'WARN' "${Label}: Downloadversuch $attempt fehlgeschlagen; erneuter Versuch.";Start-Sleep -Seconds (3*$attempt)}}
}
function Ensure-GitHubCli{
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen';if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return $managed}}
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.'
$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name -like "gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name -like '*_checksums.txt'}|Select-Object -First 1)
if(-not $asset -or -not $checks){throw 'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_ -match [regex]::Escape($asset.name)}|Select-Object -First 1;if(-not $line){throw 'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line -split '\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw 'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not $source){throw 'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent (Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path (Split-Path -Parent $managed) -Force|Out-Null;Copy-Item $source.FullName $managed -Force;$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return $managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
}
function Test-GitHubAuthentication([string]$GhPath){
$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet
return ($status.Code -eq 0)
}
function Invoke-VisibleGitHubLogin([string]$GhPath){
$runtimeDirectory=Split-Path -Parent $StatusFile
$loginScript=Join-Path $runtimeDirectory 'github-login.cmd'
$escapedGh=$GhPath.Replace('"','""')
$lines=@(
'@echo off',
'chcp 65001 >nul',
'title Vendoo - GitHub Anmeldung',
'echo ============================================================',
'echo Vendoo - Sichere GitHub Anmeldung',
'echo ============================================================',
'echo.',
'echo Der einmalige GitHub-Code wird angezeigt und in die',
'echo Zwischenablage kopiert. Folge danach dem Browserdialog.',
'echo Dieses Fenster schliesst sich nach erfolgreicher Anmeldung.',
'echo.',
('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard' -f $escapedGh),
'set "VENDOO_GH_LOGIN_EXIT=%ERRORLEVEL%"',
'echo.',
'if "%VENDOO_GH_LOGIN_EXIT%"=="0" (',
' echo GitHub-Anmeldung erfolgreich. Vendoo faehrt automatisch fort.',
') else (',
' echo GitHub-Anmeldung wurde nicht abgeschlossen. Fehlercode: %VENDOO_GH_LOGIN_EXIT%',
' echo Das Fenster kann geschlossen werden; der Vendoo Updater bleibt geoeffnet.',
' pause',
')',
'exit /b %VENDOO_GH_LOGIN_EXIT%'
)
[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)))
Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Anmeldefenster den Gerätecode bestätigen und den Browserdialog abschließen. Danach geht es automatisch weiter.' 'waiting'
Write-Event 'WAIT' 'Sichtbares GitHub-Anmeldefenster wird geöffnet.'
try{
$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"' -f $loginScript)) -WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru
}catch{
throw "Das sichtbare GitHub-Anmeldefenster konnte nicht gestartet werden: $($_.Exception.Message)"
}
$deadline=(Get-Date).AddMinutes(10);$lastStatus=Get-Date
while((Get-Date)-lt$deadline){
Assert-NotCancelled
if(Test-GitHubAuthentication $GhPath){
Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.'
try{if(-not $loginProcess.HasExited){$loginProcess.CloseMainWindow()|Out-Null}}catch{}
return
}
if($loginProcess.HasExited){
Start-Sleep -Seconds 1
if(Test-GitHubAuthentication $GhPath){Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.';return}
throw "Die GitHub-Anmeldung wurde nicht abgeschlossen. Das Anmeldefenster endete mit Exit-Code $($loginProcess.ExitCode)."
}
if(((Get-Date)-$lastStatus).TotalSeconds -ge 15){
Write-Event 'INFO' 'GitHub-Anmeldung wartet auf Abschluss im Browser.'
$lastStatus=Get-Date
}
Start-Sleep -Seconds 2
}
try{if(-not $loginProcess.HasExited){Stop-Tree $loginProcess.Id}}catch{}
throw 'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
}
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return [pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return [pscustomobject]@{Ready=$false;Version='unbekannt'}}}
function Assert-Package{foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw "Paketdatei fehlt: $f"}};$p=Get-Content (Join-Path $PackageRoot 'package.json') -Raw -Encoding UTF8|ConvertFrom-Json;$m=Get-Content (Join-Path $PackageRoot 'update-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;$r=Get-Content (Join-Path $PackageRoot 'release-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$p.version-ne$TargetVersion -or [string]$m.version-ne$TargetVersion -or [string]$r.version-ne$TargetVersion){throw "Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.updaterVersion-ne$UpdaterVersion){throw "Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};if([string]$r.mode-ne'overlay-with-explicit-removals'){throw "Unzulässiger Release-Modus: $($r.mode)"}}
try{
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
$session=Load-Session
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Manifest und sicherheitsrelevante Paketdateien werden geprüft.';Assert-Package;if($SelfTestFailAt-eq'package'){throw 'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' 'Paketprüfung erfolgreich.'
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js und npm werden mit festen Zeitlimits geprüft.'
$git=Get-GitPath;if(-not$git){throw 'Git für Windows wurde nicht gefunden. Bitte Git für Windows installieren; danach kann derselbe Updater erneut gestartet werden.'};$node=Get-NodePath;$npm=Get-NpmPath;if(-not$node -or -not$npm){throw 'Node.js 22 LTS oder npm wurde nicht gefunden.'};$gv=Invoke-External $git @('--version') -TimeoutSeconds 20 -Label 'Git prüfen';$nv=Invoke-External $node @('--version') -TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output -notmatch 'v?(\d+)\.' -or [int]$Matches[1]-lt22){throw "Node.js 22 oder neuer ist erforderlich. Gefunden: $($nv.Output.Trim())"};$gh=Ensure-GitHubCli;Complete-Step 'tools';Write-Event 'OK' 'Systemprüfung erfolgreich.'
Set-Step 'github' 'active';Write-State 24 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.'
if(-not (Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh}
Invoke-External $gh @('auth','setup-git','--hostname','github.com') -TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null
$repo=Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate') -TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen';$repoObj=$repo.Output|ConvertFrom-Json;if([string]$repoObj.viewerPermission -notin @('ADMIN','MAINTAIN','WRITE')){throw "Für $Repository fehlt Schreibzugriff. GitHub meldet $($repoObj.viewerPermission)."};Complete-Step 'github';Write-Event 'OK' "GitHub-Zugriff bestätigt: $($repoObj.viewerPermission)."
Set-Step 'production' 'active';Write-State 32 'Produktionsvorbereitung' 'Aktueller Produktionsstand und Coolify Auto Deploy werden geprüft.'
$prod=Check-ProductionVersion;Write-Event 'INFO' "Produktion meldet ready=$($prod.Ready), version=$($prod.Version)."
if(-not$SkipProductionDeploy){Write-Event 'OK' 'Kein Coolify-Webhook erforderlich: Nach dem Merge wird das bestehende Coolify Auto Deploy der GitHub-Verknüpfung verwendet.'}
Complete-Step 'production'
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Write-State 50 "Selbsttest: $id" 'Darstellung, Statuswechsel und Protokoll werden simuliert.';Start-Sleep -Milliseconds 350;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Die Updater-Oberfläche und der Worker-Vertrag funktionieren.' 'success';return}
if([bool]$session.merged){Write-Event 'INFO' 'Gespeicherter Wiederaufnahmepunkt: main wurde bereits aktualisiert.';foreach($id in @('workspace','tests','pullrequest','merge')){Complete-Step $id}}
else {
Set-Step 'workspace' 'active'
Write-State 40 'Arbeitskopie' 'main wird isoliert geklont; das vollständige Release-Manifest wird ohne Leeren der Baseline angewendet.'
if (Test-Path -LiteralPath $WorkRoot) { Remove-Item -LiteralPath $WorkRoot -Recurse -Force }
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch') -WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen' | Out-Null
Invoke-External $git @('switch','-c',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Release-Branch erstellen' | Out-Null
Invoke-External $node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir) -TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden' | Out-Null
Complete-Step 'workspace'
Set-Step 'tests' 'active'
Write-State 52 'Abnahmetests' 'Abhängigkeiten werden reproduzierbar installiert; danach läuft der vollständige verify:all-Vertrag.'
Invoke-External $npm @('ci','--no-audit','--no-fund') -WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci' | Out-Null
Write-State 61 'Abnahmetests' 'Alle Vendoo-Sicherheits-, Datenbank-, Modul- und Release-Gates laufen.'
Invoke-External $npm @('run','verify:all') -WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all' | Out-Null
Complete-Step 'tests'
Write-Event 'OK' 'Alle lokalen Abnahmetests sind grün.'
Set-Step 'pullrequest' 'active'
Write-State 70 'Pull Request' 'Der geprüfte Stand wird committed und in einen kontrollierten Pull Request übertragen.'
Invoke-External $git @('config','user.name','Vendoo Production Updater') -WorkingDirectory $CloneDir -Label 'Git-Name setzen' | Out-Null
$uid = (Invoke-External $gh @('api','user','--jq','.id') -Label 'GitHub-Benutzer-ID').Output.Trim()
$ulogin = (Invoke-External $gh @('api','user','--jq','.login') -Label 'GitHub-Benutzername').Output.Trim()
Invoke-External $git @('config','user.email',"$uid+$ulogin@users.noreply.github.com") -WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen' | Out-Null
Invoke-External $git @('add','--all') -WorkingDirectory $CloneDir -Label 'Änderungen vormerken' | Out-Null
$diff = Invoke-External $git @('diff','--cached','--quiet') -WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen'
if ($diff.Code -eq 0) {
Write-Event 'INFO' 'main enthält bereits denselben Source-Stand.'
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'pullrequest'
Complete-Step 'merge'
}
elseif ($diff.Code -eq 1) {
Invoke-External $git @('commit','-m',"release: Vendoo $TargetVersion production update") -WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen' | Out-Null
Invoke-External $git @('push','--force','--set-upstream','origin',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen' | Out-Null
$existing = Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','url','--jq','.[0].url') -AllowFailure -Label 'Vorhandenen Pull Request suchen'
$prUrl = $existing.Output.Trim()
if (-not $prUrl) {
$body = Join-Path $WorkRoot 'pr-body.md'
$bodyText = @"
## Vendoo $TargetVersion Production Update
Erstellt durch Vendoo Production Updater $UpdaterVersion.
- vollständiges `npm run verify:all` erfolgreich
- keine Datenbanken, Uploads, Secrets oder `/app/data`-Inhalte
- Coolify Auto Deploy mit exakter Versionsprüfung
"@
$bodyText | Set-Content -LiteralPath $body -Encoding UTF8
$prUrl = (Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion Production Update",'--body-file',$body) -WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim()
}
$session.prUrl = $prUrl
$session.stage = 'pr'
Save-Session $session
Complete-Step 'pullrequest'
Write-Event 'OK' "Pull Request: $prUrl"
Set-Step 'merge' 'active'
Write-State 78 'GitHub-Prüfungen' 'Alle gemeldeten GitHub-Checks werden abgewartet.'
$rollup = Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','statusCheckRollup') -Label 'GitHub-Checks ermitteln'
$rollupObj = $rollup.Output | ConvertFrom-Json
if (@($rollupObj.statusCheckRollup).Count -gt 0) {
Invoke-External $gh @('pr','checks',$prUrl,'--repo',$Repository,'--watch','--fail-fast') -WorkingDirectory $CloneDir -TimeoutSeconds 2700 -Label 'GitHub-Checks abwarten' | Out-Null
}
Write-State 82 'Merge' 'Der vollständig geprüfte Pull Request wird per Squash nach main übernommen.'
Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion Production Update") -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen' | Out-Null
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'merge'
Write-Event 'OK' 'main wurde kontrolliert aktualisiert.'
}
else {
throw "Git-Diff-Prüfung lieferte unerwarteten Exit-Code $($diff.Code)."
}
}
if($SkipProductionDeploy){foreach($id in @('deploy','verify')){foreach($s in $script:Steps){if($s.id-eq$id){$s.state='skipped'}}};Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return}
Set-Step 'deploy' 'active';Write-State 87 'Coolify Auto Deploy' 'main wurde aktualisiert. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.'
$session.stage='deploy';Save-Session $session;Write-Event 'INFO' 'Warte auf Coolify Auto Deploy. Es wird kein Webhook und kein GitHub Secret benötigt.'
$deadline=(Get-Date).AddMinutes(15);$last='';$nextLog=Get-Date
do{
Assert-NotCancelled
$prod=Check-ProductionVersion
$last="ready=$($prod.Ready), version=$($prod.Version)"
if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)}
if($prod.Ready -and $prod.Version-eq$TargetVersion){break}
Start-Sleep -Seconds 10
}while((Get-Date)-lt$deadline)
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last. In Coolify bei der Vendoo-Anwendung unter Advanced prüfen, ob Auto Deploy aktiviert ist."}
Complete-Step 'deploy'
Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentlich erreichbare Readiness und die exakte Zielversion werden abschließend bestätigt.'
$prod=Check-ProductionVersion
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Die abschließende Produktionsprüfung ist fehlgeschlagen. Stand: ready=$($prod.Ready), version=$($prod.Version)"}
Complete-Step 'verify';$session.stage='complete';Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig aktualisiert.";return
}catch [System.OperationCanceledException]{Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten. Mit „Erneut versuchen“ kann der sichere Ablauf fortgesetzt werden.';Write-Event 'WARN' $_.Exception.Message;return
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if ($script:CurrentStep -eq 'tools'){'Systemvoraussetzung korrigieren und „Erneut versuchen“ wählen. Es wurden noch keine GitHub- oder Produktionsänderungen vorgenommen.'}elseif ($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. „Erneut versuchen“ setzt beim Deployment beziehungsweise der Produktionsprüfung fort.'}else{'„Erneut versuchen“ verwendet den gespeicherten Stand. Vor einem erfolgreichen Merge bleibt main unverändert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg;return
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}