[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}}