Files
hermes-agent/tests/install/windows-bundle-smoke.ps1
ethernet b37acb8389 feat(release): dynamic R2-owned channels and preview retirement (rounds 1-2)
Checkpoint before round-3 reduction (two-tier retirement derived from
product identity). Includes:

- R2 channel protocol (release_channels.py, channel-protocol.ts): records,
  builds, manifests, retired channels with pinned destinationHead and
  receiverProtocol; fail-closed readers in both languages
- One shared native manifest/feed writer (scripts/bundles/channel_artifacts.py)
- Scoped/disposable R2 publication, fork isolation before credential
  access, canary bootstrap verification of its own promoted outputs
- Channel source CLI: typed SourceTarget, source-channel resolution,
  retirement downgrade refusal
- Desktop channel resolver/strategy, install-stamp/build-stamp receiver
  ownership (stable-owned S/T candidates), single-flight updater operation
- Cross-package retirement machinery (receiver/host/preservation/
  compatibility/connections/dialog/discovery, backup_migration strict
  snapshots with retained-link inventory, empty-dir preservation,
  connection-collision resolution, URL-credential rejection)
- Native install harness (tests/install/channel-retirement-*) and
  install-e2e retirement jobs
- checkout-source.test.ts transport shim now covers build_opener().open
  (was silently hitting the real network in CI)

Removed secondary certification protocol (channel_qualification.py) per
approved round-2 plan. All focused suites green at checkpoint; native
cross-package journeys unverified (to be deleted in round 3).
2026-09-14 10:26:36 -04:00

121 lines
7.5 KiB
PowerShell

# Real deployment of receipt-verified bytes. Never run on a developer desktop.
param(
[Parameter(Mandatory=$true)][string]$Artifact,
[Parameter(Mandatory=$true)][ValidateSet('x64','arm64')][string]$Arch,
[Parameter(Mandatory=$true)][string]$Commit,
[string]$Tag = '',
[string]$ChannelRequest = '',
[Parameter(Mandatory=$true)][string]$Work,
[Parameter(Mandatory=$true)][string]$Out
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
if ($env:GITHUB_ACTIONS -cne 'true' -or $env:RUNNER_ENVIRONMENT -cne 'github-hosted' -or $env:OS -cne 'Windows_NT') {
throw 'Disposable native GitHub-hosted Windows runner required'
}
if ($Commit -cnotmatch '^[a-f0-9]{40}$') { throw 'Expected exact lowercase full commit SHA' }
if (-not [IO.Path]::IsPathRooted($Artifact) -or -not (Test-Path -LiteralPath $Artifact -PathType Leaf)) { throw 'Absolute artifact path required' }
$Artifact = (Resolve-Path -LiteralPath $Artifact).Path
$processors = @(Get-CimInstance Win32_Processor)
$nativeCode = @{ x64=9; arm64=12 }[$Arch]
if (-not $processors.Count -or @($processors | Where-Object { $_.Architecture -ne $nativeCode }).Count) {
throw "Host is not native $Arch; emulated x64 on ARM is not coverage"
}
$Assets = Join-Path $PSScriptRoot 'e2e-assets'
$Node = (Get-Command node.exe -ErrorAction Stop).Source
$Metadata = Join-Path $Assets 'bundle-smoke-metadata.mjs'
function Run-Node([string[]]$Argv) {
& $Node @Argv
if ($LASTEXITCODE -ne 0) { throw "Node exited $LASTEXITCODE ($($Argv[0]))" }
}
$nodeArch = Run-Node @('-p', 'process.arch')
if ($nodeArch -cne $Arch) { throw 'Driver Node must also use the native architecture' }
$identityArgs = @('--commit', $Commit)
if ($Tag) { $identityArgs += @('--tag', $Tag) }
if ($ChannelRequest) { $identityArgs += @('--channel-request', $ChannelRequest) }
$Expected = (Run-Node (@($Metadata, 'identity') + $identityArgs)) | ConvertFrom-Json
Run-Node @($Metadata, 'prepare', '--work', $Work, '--out', $Out)
. (Join-Path $Assets 'windows-bundle-metadata.ps1')
Add-Type -AssemblyName System.IO.Compression.FileSystem
$HomeDir = Join-Path $Work 'home'
$UserData = Join-Path $Work 'user-data'
New-Item -ItemType Directory -Path $HomeDir, $UserData | Out-Null
Start-Transcript -Path (Join-Path $Out 'native-install.log') | Out-Null
$attempted = $false
$installedFullName = $null
$failed = $false
try {
$beforeHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Artifact).Hash.ToLowerInvariant()
@{ path=$Artifact; sha256=$beforeHash } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Out 'artifact.json')
$signature = Get-AuthenticodeSignature -LiteralPath $Artifact
if ($signature.Status -ne 'Valid' -or -not $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -cne $Expected.publisher) { throw 'Artifact signature/publisher invalid' }
$inputMetadata = Read-BundleSmokeMetadata $Artifact $Arch $Expected
$inputMetadata | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Out 'artifact-metadata.json')
if (@(Get-AppxPackage -AllUsers -Name $Expected.msixIdentity).Count) { throw 'Package identity already exists; refusing to replace it' }
$attempted = $true
Add-AppxPackage -Path $Artifact -ErrorAction Stop
$packages = @(Get-AppxPackage -Name $Expected.msixIdentity)
if ($packages.Count -ne 1) { throw 'Expected exactly one deployed package' }
$pkg = $packages[0]
$installedFullName = $pkg.PackageFullName
if ($pkg.Publisher -cne $Expected.publisher -or $pkg.Architecture.ToString() -ine $Arch -or
$pkg.Version.ToString() -cne $inputMetadata.Version -or $pkg.Status.ToString() -cne 'Ok') {
throw 'Deployed package identity/version/native architecture/status mismatch'
}
$manifest = Get-AppxPackageManifest -Package $pkg.PackageFullName
$installed = Read-SmokePackageManifest $manifest $Arch $Expected
if ($installed.Version -cne $inputMetadata.Version) { throw 'Installed manifest version mismatch' }
if ($inputMetadata.Executable -and $installed.Executable -cne $inputMetadata.Executable) { throw 'Installed executable differs from input manifest' }
$exe = [IO.Path]::GetFullPath((Join-Path $pkg.InstallLocation $installed.Executable))
if (-not $exe.StartsWith($pkg.InstallLocation.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase) -or
-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw 'Installed manifest executable is absent or outside package' }
$resources = Join-Path (Split-Path -Parent $exe) 'resources'
$root = Join-Path $resources 'agent-payload'
if (-not (Test-Path -LiteralPath $root -PathType Container)) { throw 'Installed agent-payload missing' }
$stampPath = Join-Path $resources 'install-stamp.json'
$stamp = Get-Content -Raw -LiteralPath $stampPath | ConvertFrom-Json
$displayVersion = (Run-Node (@($Metadata, 'stamp', '--platform', 'win32', '--stamp', $stampPath) + $identityArgs)) | ConvertFrom-Json
if ($ChannelRequest) {
if ($pkg.Version.ToString() -cne $displayVersion) { throw 'Package version disagrees with channel request' }
} else {
$semverBase = ($displayVersion -split '-')[0]
if (-not $pkg.Version.ToString().StartsWith($semverBase + '.', [StringComparison]::Ordinal)) { throw 'Package version disagrees with stamped semver' }
if (-not $Tag -and $pkg.Version.ToString() -cne ($displayVersion + '.0')) { throw 'Commit package must use stamped semver with zero revision' }
}
@{ packageFullName=$pkg.PackageFullName; publisher=$pkg.Publisher; architecture=$pkg.Architecture.ToString();
version=$pkg.Version.ToString(); exe=$exe; root=$root; stamp=$stamp } |
ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $Out 'installed-identity.json')
if ((Get-FileHash -Algorithm SHA256 -LiteralPath $Artifact).Hash.ToLowerInvariant() -cne $beforeHash) { throw 'Input artifact changed during installation' }
Push-Location $Work
try {
Run-Node @((Join-Path $Assets 'desktop-smoke.ts'), '--exe', $exe, '--root', $root, '--origin', 'bundled',
'--home', $HomeDir, '--user-data', $UserData, '--out', $Out, '--phase', 'installed', '--expect-commit', $Commit)
} finally { Pop-Location }
} catch {
$failed = $true
$_ | Out-String | Set-Content -LiteralPath (Join-Path $Out 'native-install-error.txt')
Write-Error $_ -ErrorAction Continue
} finally {
try {
# A failed deployment can still have registered the package. Admission
# ruled out prior identities, so only this invocation's exact package is eligible.
if ($attempted) {
$remaining = @(Get-AppxPackage -Name $Expected.msixIdentity)
foreach ($pkg in $remaining) {
if ($pkg.Publisher -cne $Expected.publisher -or $pkg.Version.ToString() -cne $inputMetadata.Version -or
($installedFullName -and $pkg.PackageFullName -cne $installedFullName)) { throw 'Refusing cleanup of an unowned package' }
Remove-AppxPackage -Package $pkg.PackageFullName -ErrorAction Stop
if (Get-AppxPackage -Name $Expected.msixIdentity) { throw 'Package removal did not complete' }
}
}
} catch {
$failed = $true
$_ | Out-String | Set-Content -LiteralPath (Join-Path $Out 'native-cleanup-error.txt')
Write-Error $_ -ErrorAction Continue
} finally {
@{ exitCode=[int]$failed } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Out 'native-install-exit.json')
Stop-Transcript | Out-Null
}
}
if ($failed) { exit 1 }