param( [Parameter(Mandatory = $true)] [string]$CandidateInstaller, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Za-z0-9._-]{1,80}$')] [string]$Label ) $ErrorActionPreference = 'Stop' $repository = 'opensquilla/opensquilla' $oldTag = 'v0.5.0rc3' $oldAsset = 'OpenSquilla-0.5.0-rc3-win-x64.exe' $candidate = (Resolve-Path -LiteralPath $CandidateInstaller).Path $sandbox = Join-Path $env:RUNNER_TEMP "opensquilla-release-preservation-$Label" $oldDir = Join-Path $sandbox 'rc3' $installDir = Join-Path $sandbox 'OpenSquilla' $appData = Join-Path $sandbox 'appdata' $userData = Join-Path $appData 'OpenSquilla' $profile = Join-Path $userData 'opensquilla' $probe = Join-Path $PWD '.github\scripts\verify-release-profile-preservation.py' $env:APPDATA = $appData $env:OPENSQUILLA_DESKTOP_DISABLE_AUTO_UPDATE = '1' $env:OPENSQUILLA_RECOVERY_OFFLINE = '1' New-Item -ItemType Directory -Force -Path $oldDir, $appData | Out-Null gh release download $oldTag --repo $repository --pattern $oldAsset --dir $oldDir if ($LASTEXITCODE -ne 0) { throw 'Failed to download the RC3 Windows installer.' } $oldInstaller = Join-Path $oldDir $oldAsset function Stop-InstalledProcesses { Get-Process -Name 'OpenSquilla', 'opensquilla-gateway' -ErrorAction SilentlyContinue | ForEach-Object { try { $path = if ($_.Path) { [IO.Path]::GetFullPath($_.Path) } else { '' } $prefix = [IO.Path]::GetFullPath($installDir + [IO.Path]::DirectorySeparatorChar) if ($path.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { & taskkill.exe /PID $_.Id /T /F 2>$null | Out-Null } } catch { if ($_.Exception.Message -notmatch 'exited|cannot find|No process') { throw } } } } try { $old = Start-Process -FilePath $oldInstaller -ArgumentList @('/S', "/D=$installDir") ` -Wait -PassThru if ($old.ExitCode -ne 0) { throw "RC3 installer failed with exit code $($old.ExitCode)." } python $probe seed --home $profile --label $Label if ($LASTEXITCODE -ne 0) { throw 'Failed to seed the synthetic RC3 profile.' } $installed = Start-Process -FilePath $candidate -ArgumentList @('/S', "/D=$installDir") ` -Wait -PassThru if ($installed.ExitCode -ne 0) { throw "Candidate installer failed with exit code $($installed.ExitCode)." } python $probe verify --home $profile --label $Label if ($LASTEXITCODE -ne 0) { throw 'Candidate installation changed RC3 profile data.' } $app = Join-Path $installDir 'OpenSquilla.exe' if (-not (Test-Path -LiteralPath $app -PathType Leaf)) { throw 'Candidate installation did not publish OpenSquilla.exe.' } $launched = Start-Process -FilePath $app ` -ArgumentList @('--use-mock-keychain', "--user-data-dir=$userData") -PassThru Start-Sleep -Seconds 8 if ($launched.HasExited) { throw "Candidate Desktop exited during launch verification: $($launched.ExitCode)" } Stop-InstalledProcesses $gateway = Get-ChildItem -Path (Join-Path $installDir 'resources\runtime\gateway') ` -Filter 'opensquilla-gateway.exe' -File -Recurse | Select-Object -First 1 if (-not $gateway) { throw 'Packaged recovery CLI was not found.' } $inspectionRaw = & $gateway.FullName recovery inspect --home $profile --json if ($LASTEXITCODE -ne 0) { throw 'Packaged recovery inspection failed.' } $inspection = $inspectionRaw | ConvertFrom-Json if ($inspection.outcome -notin @('ready', 'attention')) { throw "Unsafe packaged profile inspection: $inspectionRaw" } if ([IO.Path]::GetFullPath($inspection.primary_home) -ne [IO.Path]::GetFullPath($profile)) { throw 'Candidate selected a different primary profile after upgrade.' } if ( [IO.Path]::GetFullPath($inspection.effective_workspace) -ne [IO.Path]::GetFullPath((Join-Path $profile 'workspace')) ) { throw 'Candidate selected a different workspace after upgrade.' } $configuredState = @($inspection.candidates | Where-Object { $_.kind -eq 'state' -and $_.configured -and $_.valid }) if ( $configuredState.Count -ne 1 -or [IO.Path]::GetFullPath($configuredState[0].path) -ne [IO.Path]::GetFullPath((Join-Path $profile 'state')) ) { throw 'Candidate selected a different state directory after upgrade.' } python $probe verify --home $profile --label $Label if ($LASTEXITCODE -ne 0) { throw 'Candidate launch changed RC3 profile data.' } $uninstaller = Get-ChildItem -LiteralPath $installDir -Filter 'Uninstall*.exe' -File | Select-Object -First 1 if (-not $uninstaller) { throw 'Candidate Windows uninstaller was not found.' } $uninstall = Start-Process -FilePath $uninstaller.FullName -ArgumentList @('/S') ` -Wait -PassThru if ($uninstall.ExitCode -ne 0) { throw "Candidate uninstaller failed with exit code $($uninstall.ExitCode)." } $deadline = [DateTime]::UtcNow.AddSeconds(30) while ( (Test-Path -LiteralPath $app -PathType Leaf) -and [DateTime]::UtcNow -lt $deadline ) { Start-Sleep -Seconds 1 } if (Test-Path -LiteralPath $app -PathType Leaf) { throw 'Candidate uninstaller did not remove OpenSquilla.exe.' } python $probe verify --home $profile --label $Label if ($LASTEXITCODE -ne 0) { throw 'Candidate uninstaller changed RC3 profile data.' } } finally { Stop-InstalledProcesses }