Repository navigation
test(windows): cover hash argument quoting #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Windows v0.4.3 upgrade synthetic E2E | |
| on: | |
| push: | |
| branches: | |
| - fix/v043-unified-upgrade | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| e2e: | |
| runs-on: windows-2022 | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install NSIS | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| choco install nsis --yes --no-progress | |
| $roots = @( | |
| (Join-Path ${env:ProgramFiles(x86)} "NSIS\makensis.exe"), | |
| (Join-Path $env:ProgramFiles "NSIS\makensis.exe") | |
| ) | |
| $makensis = $roots | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 | |
| if (-not $makensis) { throw "makensis.exe not found" } | |
| "MAKENSIS=$makensis" >> $env:GITHUB_ENV | |
| - name: Build synthetic candidate CLI and inner installer | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| rustc -vV | |
| if ($LASTEXITCODE -ne 0) { throw "rustc is unavailable" } | |
| @' | |
| use std::{env, fs, path::{Path, PathBuf}, process}; | |
| fn log(text: &str) { | |
| let mut paths = vec![env::temp_dir().join("webcodex-stub.log")]; | |
| if let Ok(path) = env::var("STUB_LOG") { paths.push(path.into()); } | |
| for path in paths { | |
| let old = fs::read_to_string(&path).unwrap_or_default(); | |
| let _ = fs::write(path, format!("{old}candidate {text}\n")); | |
| } | |
| } | |
| fn option(args: &[String], name: &str) -> Option<String> { | |
| args.windows(2).find(|pair| pair[0] == name).map(|pair| pair[1].clone()) | |
| } | |
| fn same(a: impl AsRef<Path>, b: impl AsRef<Path>) -> bool { | |
| fs::read(a).ok().zip(fs::read(b).ok()).is_some_and(|(a, b)| a == b) | |
| } | |
| fn main() { | |
| let args: Vec<String> = env::args().skip(1).collect(); | |
| log(&format!("args={}", args.join(" "))); | |
| if args.as_slice().starts_with(&["environment".into(), "--help".into()]) { | |
| log("help=0"); | |
| return; | |
| } | |
| if args.len() >= 2 && args[0] == "environment" && args[1] == "installer-verify-same" { | |
| let Some(runtime) = option(&args, "--expected-runtime-dir").map(PathBuf::from) else { process::exit(20) }; | |
| let Some(source) = env::var_os("STUB_CANDIDATE_BIN").map(PathBuf::from) else { process::exit(20) }; | |
| let Some(root) = runtime.parent() else { process::exit(20) }; | |
| let pairs = [ | |
| (source.join("webcodex.exe"), runtime.join("webcodex.exe")), | |
| (source.join("webcodex-server.exe"), runtime.join("webcodex-server.exe")), | |
| (source.join("webcodex-runner.exe"), runtime.join("webcodex-runner.exe")), | |
| (source.join("webcodex-desktop.exe"), root.join("WebCodex.exe")), | |
| ]; | |
| for (expected, installed) in pairs { | |
| if !same(&expected, &installed) { | |
| log(&format!("verify=21 {}", installed.display())); | |
| process::exit(21); | |
| } | |
| } | |
| log("verify=0"); | |
| return; | |
| } | |
| log("other=0"); | |
| } | |
| '@ | Set-Content -LiteralPath candidate.rs -Encoding utf8 | |
| @' | |
| use std::{env, fs, path::PathBuf, process}; | |
| fn log(text: &str) { | |
| let mut paths = vec![env::temp_dir().join("webcodex-stub.log")]; | |
| if let Ok(path) = env::var("STUB_LOG") { paths.push(path.into()); } | |
| for path in paths { | |
| let old = fs::read_to_string(&path).unwrap_or_default(); | |
| let _ = fs::write(path, format!("{old}inner {text}\n")); | |
| } | |
| } | |
| fn main() { | |
| log("start"); | |
| let Some(source) = env::var_os("STUB_CANDIDATE_BIN").map(PathBuf::from) else { process::exit(30) }; | |
| let Some(root) = env::var_os("STUB_INSTALL_ROOT").map(PathBuf::from) else { process::exit(31) }; | |
| let runtime = root.join("webcodex-runtime"); | |
| if fs::create_dir_all(&runtime).is_err() { process::exit(32) } | |
| for (src, dst) in [ | |
| (source.join("webcodex.exe"), runtime.join("webcodex.exe")), | |
| (source.join("webcodex-server.exe"), runtime.join("webcodex-server.exe")), | |
| (source.join("webcodex-runner.exe"), runtime.join("webcodex-runner.exe")), | |
| (source.join("webcodex-desktop.exe"), root.join("WebCodex.exe")), | |
| ] { | |
| if fs::copy(src, dst).is_err() { process::exit(33) } | |
| } | |
| log("copies=done"); | |
| } | |
| '@ | Set-Content -LiteralPath inner.rs -Encoding utf8 | |
| rustc -O candidate.rs -o candidate-stub.exe | |
| if ($LASTEXITCODE -ne 0) { throw "candidate stub compile failed" } | |
| rustc -O inner.rs -o inner-stub.exe | |
| if ($LASTEXITCODE -ne 0) { throw "inner stub compile failed" } | |
| - name: Build synthetic unified outer installer from production bootstrap | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| @' | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| from unittest import mock | |
| sys.path.insert(0, str(Path("scripts/tests").resolve())) | |
| import test_prepare_windows_unified_installer as tests | |
| case = tests.WindowsUnifiedNsisTests(methodName="test_outer_bootstrap_extracts_candidate_in_section_before_inner_install") | |
| case.setUp() | |
| try: | |
| root = Path("synthetic-candidate").resolve() | |
| root.mkdir() | |
| infos = case.fixture(root) | |
| bin_dir = root / "artifacts" / "bin" | |
| stub = Path("candidate-stub.exe").resolve() | |
| for name in tests.nsis.BINARIES: | |
| shutil.copyfile(stub, bin_dir / f"{name}.exe") | |
| manifest_path = root / "source-manifest.json" | |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| sums = {} | |
| for name in tests.nsis.BINARIES: | |
| path = bin_dir / f"{name}.exe" | |
| rel = path.relative_to(root).as_posix() | |
| digest = hashlib.sha256(path.read_bytes()).hexdigest() | |
| manifest["artifacts"][name]["sha256"] = digest | |
| sums[rel] = digest | |
| manifest["desktop_payload"]["sha256"] = tests.nsis.collector.package.tree_digest(bin_dir / "webcodex-desktop.exe") | |
| manifest["desktop_payload"]["managed_files"] = [ | |
| {"path": path, "sha256": manifest["artifacts"][name]["sha256"]} | |
| for path, name in sorted(tests.nsis.MANAGED_INSTALL_FILES.items()) | |
| ] | |
| raw = json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n" | |
| manifest_path.write_bytes(raw) | |
| sums["source-manifest.json"] = hashlib.sha256(raw).hexdigest() | |
| (root / "SHA256SUMS").write_text( | |
| "".join(f"{digest} {name}\n" for name, digest in sorted(sums.items())), | |
| encoding="ascii", | |
| ) | |
| output = Path("synthetic-unified.exe").resolve() | |
| with mock.patch.object(tests.nsis.collector, "probe", side_effect=lambda path, name: infos[name]): | |
| tests.bootstrap.build(argparse.Namespace( | |
| candidate_dir=root, | |
| inner_installer=Path("inner-stub.exe").resolve(), | |
| platform="win32-x64", | |
| output=output, | |
| makensis=os.environ["MAKENSIS"], | |
| )) | |
| finally: | |
| case.doCleanups() | |
| '@ | python - | |
| if ($LASTEXITCODE -ne 0) { throw "synthetic unified installer build failed" } | |
| - name: Install official v0.4.3 | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| New-Item -ItemType Directory -Force -Path legacy | Out-Null | |
| $base = "https://github.com/yyjeqhc/webcodex/releases/download/v0.4.3" | |
| $installer = Join-Path $PWD "legacy\webcodex-desktop-v0.4.3-win32-x64-setup.exe" | |
| $sums = Join-Path $PWD "legacy\SHA256SUMS" | |
| Invoke-WebRequest -UseBasicParsing "$base/webcodex-desktop-v0.4.3-win32-x64-setup.exe" -OutFile $installer | |
| Invoke-WebRequest -UseBasicParsing "$base/SHA256SUMS" -OutFile $sums | |
| $line = Get-Content -LiteralPath $sums | Where-Object { $_ -match 'webcodex-desktop-v0\.4\.3-win32-x64-setup\.exe$' } | Select-Object -First 1 | |
| $expected = ($line -split '\s+')[0].ToLowerInvariant() | |
| $actual = (Get-FileHash -LiteralPath $installer -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actual -ne $expected) { throw "official v0.4.3 checksum mismatch" } | |
| $p = Start-Process -FilePath $installer -ArgumentList "/S" -Wait -PassThru | |
| if ($p.ExitCode -ne 0) { throw "official v0.4.3 installation failed" } | |
| $key = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\WebCodex Desktop" | |
| $root = (Get-ItemProperty -LiteralPath $key).InstallLocation.Trim('"') | |
| Set-Content -LiteralPath (Join-Path $root "legacy-upgrade-sentinel.txt") -Value "preserve-me" -Encoding ascii | |
| "LEGACY_INSTALL_ROOT=$root" >> $env:GITHUB_ENV | |
| - name: Upgrade v0.4.3 through patched outer bootstrap | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $env:STUB_CANDIDATE_BIN = (Resolve-Path "synthetic-candidate\artifacts\bin").Path | |
| $env:STUB_INSTALL_ROOT = $env:LEGACY_INSTALL_ROOT | |
| $env:STUB_LOG = Join-Path $PWD "stub-invocations.log" | |
| Remove-Item -LiteralPath (Join-Path $env:TEMP "webcodex-stub.log") -Force -ErrorAction SilentlyContinue | |
| $p = Start-Process -FilePath (Resolve-Path "synthetic-unified.exe") -ArgumentList "/S" -PassThru | |
| if (-not $p.WaitForExit(15000)) { | |
| "===== process snapshot after 15s =====" | |
| Get-CimInstance Win32_Process | | |
| Where-Object { $_.Name -match 'synthetic-unified|webcodex|powershell|inner-stub|candidate-stub' } | | |
| Select-Object ProcessId,ParentProcessId,Name,CommandLine | | |
| Format-List | Out-String | Write-Host | |
| "===== stub log =====" | |
| foreach ($path in @($env:STUB_LOG, (Join-Path $env:TEMP "webcodex-stub.log"))) { | |
| if (Test-Path -LiteralPath $path) { Get-Content -LiteralPath $path } | |
| } | |
| } | |
| if (-not $p.WaitForExit(45000)) { | |
| Get-CimInstance Win32_Process | | |
| Where-Object { $_.Name -match 'synthetic-unified|webcodex|powershell|inner-stub|candidate-stub' } | | |
| Select-Object ProcessId,ParentProcessId,Name,CommandLine | | |
| Format-List | Out-String | Write-Host | |
| Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue | |
| foreach ($path in @($env:STUB_LOG, (Join-Path $env:TEMP "webcodex-stub.log"))) { | |
| if (Test-Path -LiteralPath $path) { Get-Content -LiteralPath $path } | |
| } | |
| throw "patched synthetic unified upgrade did not terminate within 60 seconds" | |
| } | |
| if ($p.ExitCode -ne 0) { | |
| if (Test-Path -LiteralPath $env:STUB_LOG) { Get-Content -LiteralPath $env:STUB_LOG } | |
| throw "patched synthetic unified upgrade failed with exit code $($p.ExitCode)" | |
| } | |
| $root = $env:LEGACY_INSTALL_ROOT | |
| $runtime = Join-Path $root "webcodex-runtime" | |
| $pairs = @( | |
| @((Join-Path $env:STUB_CANDIDATE_BIN "webcodex.exe"), (Join-Path $runtime "webcodex.exe")), | |
| @((Join-Path $env:STUB_CANDIDATE_BIN "webcodex-server.exe"), (Join-Path $runtime "webcodex-server.exe")), | |
| @((Join-Path $env:STUB_CANDIDATE_BIN "webcodex-runner.exe"), (Join-Path $runtime "webcodex-runner.exe")), | |
| @((Join-Path $env:STUB_CANDIDATE_BIN "webcodex-desktop.exe"), (Join-Path $root "WebCodex.exe")) | |
| ) | |
| foreach ($pair in $pairs) { | |
| $want = (Get-FileHash -LiteralPath $pair[0] -Algorithm SHA256).Hash | |
| $got = (Get-FileHash -LiteralPath $pair[1] -Algorithm SHA256).Hash | |
| if ($want -ne $got) { throw "installed managed executable does not match synthetic candidate: $($pair[1])" } | |
| } | |
| if ((Get-Content -LiteralPath (Join-Path $root "legacy-upgrade-sentinel.txt") -Raw).Trim() -ne "preserve-me") { | |
| throw "legacy non-managed sentinel was not preserved" | |
| } | |
| $environmentRecord = Join-Path $env:APPDATA "webcodex\environment\environment.json" | |
| if (Test-Path -LiteralPath $environmentRecord) { | |
| throw "package-only legacy upgrade unexpectedly fabricated an EnvironmentStore" | |
| } | |
| [ordered]@{ | |
| official_v043_installed = $true | |
| patched_outer_exit = $p.ExitCode | |
| all_four_managed_executables_replaced = $true | |
| unmanaged_sentinel_preserved = $true | |
| environment_store_fabricated = $false | |
| } | ConvertTo-Json | Set-Content -LiteralPath synthetic-e2e-result.json -Encoding utf8 | |
| - name: Upload E2E evidence | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: windows-v043-upgrade-synthetic-e2e | |
| path: | | |
| synthetic-unified.nsi | |
| synthetic-unified.exe | |
| synthetic-e2e-result.json | |
| stub-invocations.log | |
| if-no-files-found: warn | |
| retention-days: 14 |