From f54cd9b929999b11f5d83bd574814c21251be235 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Mon, 1 Jun 2026 13:16:08 -0700 Subject: [PATCH 01/38] wip - msix --- .github/workflows/ci.yml | 4 +- README.md | 13 +- build.ps1 | 77 +++++++- openclaw-windows-node.slnx | 1 + run-app-local.ps1 | 65 ++---- scripts/build-inno-local.ps1 | 3 +- scripts/setup-dev-msix-cert.ps1 | 186 ++++++++++++++++++ .../OpenClaw.Tray.WinUI.csproj | 64 ++++-- src/OpenClaw.Tray.WinUI/Package.appxmanifest | 12 +- .../Properties/launchSettings.json | 9 + 10 files changed, 346 insertions(+), 88 deletions(-) create mode 100644 scripts/setup-dev-msix-cert.ps1 create mode 100644 src/OpenClaw.Tray.WinUI/Properties/launchSettings.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff4ce3087..0940d51b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -383,10 +383,10 @@ jobs: run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} - name: Build WinUI Tray App (Release) - run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} + run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} -p:Unpackaged=true - name: Publish WinUI Tray App - run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish + run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish -p:Unpackaged=true - name: Verify x64 Native Runtime Payload if: matrix.rid == 'win-x64' diff --git a/README.md b/README.md index a2d4870d7..929bcd525 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,12 @@ dotnet build src/OpenClaw.Tray.WinUI -r win-x64 -p:PackageMsix=true # x64 MSI ### Run Tray App +The tray always runs as a packaged WinUI app in development. `run-app-local.ps1` +uses Microsoft WinAppCLI (`winget install Microsoft.WinAppCLI`) to activate the +build output as a packaged loose layout — no `.msix` file is required. + ```powershell -# Build and launch the unpackaged WinUI tray app +# Build and launch the tray app (packaged loose layout via winapp) .\run-app-local.ps1 # If you already built, skip rebuild and launch the existing Debug output @@ -86,15 +90,8 @@ dotnet build src/OpenClaw.Tray.WinUI -r win-x64 -p:PackageMsix=true # x64 MSI # Alpha update testing from a Release build .\run-app-local.ps1 -Configuration Release -Isolated -UpdateChannel alpha - -# Optional: launch through WinAppCLI with Package.appxmanifest -.\run-app-local.ps1 -UseWinApp -NoBuild ``` -The default path starts the unpackaged executable directly. `-UseWinApp` requires -Microsoft WinAppCLI (`winget install Microsoft.WinAppCLI`) and is only needed when -you want manifest/MSIX-adjacent launch validation. - ### Run CLI WebSocket Validator Use the CLI to validate gateway connectivity and `chat.send` outside the tray UI. diff --git a/build.ps1 b/build.ps1 index 9426ebeda..a8254c1a6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -21,9 +21,18 @@ cannot read a repo owned by a different Windows account/group. The script will print the manual command instead. +.PARAMETER PackageMsix + In addition to the always-packaged loose-layout build, produce a .msix + package file in src/OpenClaw.Tray.WinUI/AppPackages/. Requires the + OpenClaw.Tray.WinUI project to be in the build set (Project=All, Tray, or + WinUI). If %LOCALAPPDATA%\OpenClawTray\dev-msix.pfx exists the .msix is + signed with that cert (run scripts\setup-dev-msix-cert.ps1 once to create + it); otherwise the .msix is unsigned. + .EXAMPLE .\build.ps1 .\build.ps1 -Project WinUI -Configuration Release + .\build.ps1 -Project WinUI -PackageMsix .\build.ps1 -CheckOnly #> @@ -36,7 +45,9 @@ param( [switch]$CheckOnly, - [switch]$NoTrustRepository + [switch]$NoTrustRepository, + + [switch]$PackageMsix ) $ErrorActionPreference = "Stop" @@ -297,7 +308,7 @@ function Invoke-DotNetCaptured($arguments) { } } -function Build-Project($name, $path, $useRid = $false) { +function Build-Project($name, $path, $useRid = $false, $publishMsix = $false) { Write-Host "`nBuilding $name..." -ForegroundColor White if (-not (Test-Path $path)) { @@ -305,10 +316,16 @@ function Build-Project($name, $path, $useRid = $false) { return $false } - $dotnetArgs = @("build", $path, "-c", $Configuration) - # WinUI requires runtime identifier for self-contained WebView2 support - if ($useRid) { - $dotnetArgs += @("-r", $rid) + if ($publishMsix) { + # MSIX file production: dotnet publish so the self-contained layout MSIX + # tooling packages matches what end-users install. -p:PackageMsix=true + # turns on GenerateAppxPackageOnBuild in the csproj. + $dotnetArgs = @("publish", $path, "-c", $Configuration, "-r", $rid, "--self-contained", "-p:PackageMsix=true") + } elseif ($useRid) { + # WinUI requires runtime identifier for self-contained WebView2 support. + $dotnetArgs = @("build", $path, "-c", $Configuration, "-r", $rid) + } else { + $dotnetArgs = @("build", $path, "-c", $Configuration) } $result = Invoke-DotNetCaptured $dotnetArgs $exitCode = $LASTEXITCODE @@ -369,11 +386,32 @@ if ($Project -ne "Shared" -and $Project -ne "All" -and $toBuild -notcontains "Sh $toBuild = @("Shared") + $toBuild } +# -PackageMsix preflight: must include WinUI/Tray and (warn only) check PFX. +if ($PackageMsix) { + $winUITargetIncluded = ($toBuild -contains "WinUI") -or ($toBuild -contains "Tray") + if (-not $winUITargetIncluded) { + Write-Error "-PackageMsix requires -Project All, Tray, or WinUI (current: $Project)" + exit 1 + } + + $devPfx = Join-Path $env:LOCALAPPDATA "OpenClawTray\dev-msix.pfx" + if (Test-Path $devPfx) { + Write-Success "Dev MSIX signing cert found: $devPfx" + } else { + Write-Warning "Dev MSIX signing cert not found at $devPfx" + Write-Info "The .msix will be unsigned and must be installed with: Add-AppxPackage -AllowUnsigned -Path " + Write-Info "To produce a signed .msix instead, run (elevated):" + Write-Info " .\scripts\setup-dev-msix-cert.ps1" + } +} + for ($i = 0; $i -lt $toBuild.Count; $i++) { $proj = $toBuild[$i] if ($projects.ContainsKey($proj)) { $projInfo = $projects[$proj] - $buildResults[$proj] = Build-Project $proj $projInfo.Path $projInfo.UseRid + $isWinUI = ($proj -eq "WinUI" -or $proj -eq "Tray") + $shouldPackageMsix = $PackageMsix -and $isWinUI + $buildResults[$proj] = Build-Project $proj $projInfo.Path $projInfo.UseRid $shouldPackageMsix if ($proj -eq "Shared" -and -not $buildResults[$proj] -and $i -lt ($toBuild.Count - 1)) { Write-Warning "Skipping remaining projects because Shared failed." break @@ -409,11 +447,32 @@ if ($failCount -eq 0) { $winUIManifestPath = ".\$winUIProjectDirectory\Package.appxmanifest" Write-Host " WinUI: .\run-app-local.ps1 -NoBuild" -ForegroundColor White Write-Host " Isolated: .\run-app-local.ps1 -NoBuild -Isolated" -ForegroundColor White - Write-Host " WinApp: .\run-app-local.ps1 -NoBuild -UseWinApp" -ForegroundColor White - Write-Host " Direct launch is default. -UseWinApp runs: winapp run `"$winUIOutputDirectory`" --manifest `"$winUIManifestPath`" --executable `"OpenClaw.Tray.WinUI.exe`" --debug-output" -ForegroundColor DarkGray + Write-Host " Runs: winapp run `"$winUIOutputDirectory`" --manifest `"$winUIManifestPath`" --executable `"OpenClaw.Tray.WinUI.exe`" --debug-output" -ForegroundColor DarkGray } else { Write-Warning "Unable to determine WinUI target framework from $winUIProjectPath" } + + if ($PackageMsix) { + $appPackagesDir = Join-Path $winUIProjectDirectory "AppPackages" + $producedMsix = $null + if (Test-Path $appPackagesDir) { + $producedMsix = Get-ChildItem -Path $appPackagesDir -Recurse -Filter "*.msix" -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + } + + Write-Host "`nMSIX:" -ForegroundColor Cyan + if ($producedMsix) { + Write-Host " Path: $($producedMsix.FullName)" -ForegroundColor White + $devPfx = Join-Path $env:LOCALAPPDATA "OpenClawTray\dev-msix.pfx" + if (Test-Path $devPfx) { + Write-Host " Install: Add-AppxPackage -Path `"$($producedMsix.FullName)`"" -ForegroundColor White + } else { + Write-Host " Install: Add-AppxPackage -AllowUnsigned -Path `"$($producedMsix.FullName)`" (elevated)" -ForegroundColor White + } + } else { + Write-Warning "Could not locate produced .msix under $appPackagesDir" + } + } } } else { Write-Host "❌ $failCount build(s) failed" -ForegroundColor Red diff --git a/openclaw-windows-node.slnx b/openclaw-windows-node.slnx index 0fb4c6ecd..6e821a514 100644 --- a/openclaw-windows-node.slnx +++ b/openclaw-windows-node.slnx @@ -26,6 +26,7 @@ + diff --git a/run-app-local.ps1 b/run-app-local.ps1 index abbffbc92..5d048d1d8 100644 --- a/run-app-local.ps1 +++ b/run-app-local.ps1 @@ -3,11 +3,10 @@ Builds and launches the WinUI tray app for local development. .DESCRIPTION - Builds the tray app, then launches the unpackaged WinUI executable directly - for the common local-development path. - - Use -UseWinApp when you specifically want Microsoft WinAppCLI (`winapp run`) - to launch with Package.appxmanifest for packaged/MSIX-adjacent validation. + Builds the tray app, then launches it as a packaged WinUI app via + Microsoft WinAppCLI (`winapp run`) using Package.appxmanifest from the + build output. The tray always runs with MSIX package identity in dev (no + .msix file required); direct unpackaged launch is no longer supported. Use -Isolated (or -DataDir) to run multiple worktrees side-by-side without sharing settings, logs, run markers, device identities, or mutex names. @@ -36,16 +35,8 @@ Set OPENCLAW_UPDATE_CHANNEL for this launch. Use alpha for prerelease update testing after building a lower-version Release baseline. -.PARAMETER UseWinApp - Launch through Microsoft WinAppCLI (`winapp run`) with Package.appxmanifest - instead of directly starting the unpackaged executable. - .PARAMETER NoDebugOutput - With -UseWinApp, launch without winapp --debug-output. - -.PARAMETER Wait - Wait for the launched process to exit. Direct launches return immediately by - default after printing the PID. + Launch winapp without --debug-output. .PARAMETER DryRun Print the launch command and environment without starting the app. @@ -81,12 +72,8 @@ param( [ValidateSet("stable", "alpha", "prerelease")] [string]$UpdateChannel, - [switch]$UseWinApp, - [switch]$NoDebugOutput, - [switch]$Wait, - [switch]$DryRun ) @@ -158,15 +145,12 @@ if (-not (Test-Path $exePath)) { throw "Tray executable not found: $exePath. Run without -NoBuild first." } -$winapp = $null -if ($UseWinApp) { - $winapp = Get-Command winapp -ErrorAction SilentlyContinue - if (-not $winapp) { - throw "winapp CLI was not found. Install Microsoft WinAppCLI (winget install Microsoft.WinAppCLI) or run /winui-setup." - } +$winapp = Get-Command winapp -ErrorAction SilentlyContinue +if (-not $winapp) { + throw "winapp CLI was not found. Install Microsoft WinAppCLI (winget install Microsoft.WinAppCLI) or run /winui-setup." } -if ($UseWinApp -and -not (Test-Path $manifestPath)) { +if (-not (Test-Path $manifestPath)) { throw "Manifest not found: $manifestPath." } @@ -194,11 +178,9 @@ try { $env:OPENCLAW_UPDATE_CHANNEL = $UpdateChannel } - if ($UseWinApp) { - $winappArgs = @("run", $outputDir, "--manifest", $manifestPath, "--executable", "OpenClaw.Tray.WinUI.exe") - if (-not $NoDebugOutput) { - $winappArgs += "--debug-output" - } + $winappArgs = @("run", $outputDir, "--manifest", $manifestPath, "--executable", "OpenClaw.Tray.WinUI.exe") + if (-not $NoDebugOutput) { + $winappArgs += "--debug-output" } Write-Host "Launching OpenClaw Tray" -ForegroundColor Cyan @@ -206,35 +188,22 @@ try { Write-Host " Configuration: $Configuration" Write-Host " Runtime: $runtimeIdentifier" Write-Host " Output: $outputDir" - Write-Host " Mode: $(if ($UseWinApp) { 'WinAppCLI manifest activation' } else { 'Direct unpackaged executable' })" + Write-Host " Mode: WinAppCLI manifest activation (packaged loose layout)" if ($env:OPENCLAW_TRAY_DATA_DIR) { Write-Host " Data dir: $env:OPENCLAW_TRAY_DATA_DIR" } if ($env:OPENCLAW_UPDATE_CHANNEL) { Write-Host " Update channel: $env:OPENCLAW_UPDATE_CHANNEL" } - if ($UseWinApp) { - Write-Host " Launcher: $($winapp.Source)" - Write-Host " Command: winapp $($winappArgs -join ' ')" - } else { - Write-Host " Launcher: $exePath" - } + Write-Host " Launcher: $($winapp.Source)" + Write-Host " Command: winapp $($winappArgs -join ' ')" if ($DryRun) { return } - if ($UseWinApp) { - & $winapp.Source @winappArgs - $exitCode = $LASTEXITCODE - } elseif ($Wait) { - $process = Start-Process -FilePath $exePath -WorkingDirectory $outputDir -Wait -PassThru - $exitCode = $process.ExitCode - } else { - $process = Start-Process -FilePath $exePath -WorkingDirectory $outputDir -PassThru - Write-Host "Started OpenClaw Tray (PID: $($process.Id))" -ForegroundColor Green - $exitCode = 0 - } + & $winapp.Source @winappArgs + $exitCode = $LASTEXITCODE } finally { $env:OPENCLAW_TRAY_DATA_DIR = $previousDataDir $env:OPENCLAW_UPDATE_CHANNEL = $previousUpdateChannel diff --git a/scripts/build-inno-local.ps1 b/scripts/build-inno-local.ps1 index d882fd0bd..c928aae13 100644 --- a/scripts/build-inno-local.ps1 +++ b/scripts/build-inno-local.ps1 @@ -100,7 +100,8 @@ function Publish-ArchitecturePayload { "-r", $RuntimeIdentifier, "--self-contained", "-o", $publishDir, - "-v:minimal" + "-v:minimal", + "-p:Unpackaged=true" ) if ($PublishVersion) { $trayPublishArgs += "-p:Version=$PublishVersion" diff --git a/scripts/setup-dev-msix-cert.ps1 b/scripts/setup-dev-msix-cert.ps1 new file mode 100644 index 000000000..c07cf653d --- /dev/null +++ b/scripts/setup-dev-msix-cert.ps1 @@ -0,0 +1,186 @@ +<# +.SYNOPSIS + Provisions a self-signed certificate and PFX for locally building and + installing a signed OpenClaw Companion MSIX. + +.DESCRIPTION + Reads the Publisher subject from src\OpenClaw.Tray.WinUI\Package.appxmanifest, + creates a code-signing certificate with that exact subject (if one doesn't + already exist), adds the public cert to LocalMachine\TrustedPeople so AppX + deployment will accept packages signed with it, and exports the cert + + private key to %LOCALAPPDATA%\OpenClawTray\dev-msix.pfx. + + The OpenClaw.Tray.WinUI project auto-detects the PFX at that well-known + path: when present, the MSIX build signs with it; when absent, the MSIX + is unsigned. So after running this script once, a normal build/publish + of the tray produces a signed .msix that installs with plain + Add-AppxPackage -- no -AllowUnsigned, no env-var plumbing. + + The script is idempotent: re-running reuses the existing cert and just + re-exports the PFX. Pass -Force to discard and recreate. Requires an + elevated PowerShell because writing to LocalMachine\TrustedPeople + requires admin rights. + +.PARAMETER Force + Delete any existing matching cert (both stores) and PFX, then create fresh. + +.PARAMETER SkipTrust + Create the cert and PFX but skip the LocalMachine\TrustedPeople step. + Useful when running non-elevated to inspect what will be created. A + package signed with this cert will not be installable until the public + cert is separately imported into LocalMachine\TrustedPeople. + +.EXAMPLE + # In an elevated PowerShell: + .\scripts\setup-dev-msix-cert.ps1 + +.EXAMPLE + .\scripts\setup-dev-msix-cert.ps1 -Force +#> +[CmdletBinding()] +param( + [switch]$Force, + [switch]$SkipTrust +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') +$manifestPath = Join-Path $repoRoot 'src\OpenClaw.Tray.WinUI\Package.appxmanifest' +$pfxDir = Join-Path $env:LOCALAPPDATA 'OpenClawTray' +$pfxPath = Join-Path $pfxDir 'dev-msix.pfx' +# Password is also hardcoded in OpenClaw.Tray.WinUI.csproj's +# PackageCertificatePassword. If you change one, change both. +$pfxPassword = 'openclaw-dev' + +function Write-Step([string]$Message) { + Write-Host "`n=== $Message ===" -ForegroundColor Cyan +} + +if (-not (Test-Path $manifestPath)) { + throw "Manifest not found at $manifestPath" +} + +# Pull Publisher from the manifest so this stays in sync if Publisher ever changes. +[xml]$manifest = Get-Content -LiteralPath $manifestPath +$ns = New-Object System.Xml.XmlNamespaceManager $manifest.NameTable +$ns.AddNamespace('p', 'http://schemas.microsoft.com/appx/manifest/foundation/windows10') +$identity = $manifest.SelectSingleNode('/p:Package/p:Identity', $ns) +if (-not $identity) { throw "Could not find in $manifestPath" } +$subject = $identity.Publisher +if ([string]::IsNullOrWhiteSpace($subject)) { throw "Identity/@Publisher is empty in $manifestPath" } + +Write-Step 'Manifest Publisher' +Write-Host $subject + +# Admin check is only needed when we're going to write to LocalMachine. +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin -and -not $SkipTrust) { + throw "This script must run as Administrator to add the certificate to LocalMachine\TrustedPeople. " + + "Re-run from an elevated PowerShell, or pass -SkipTrust to only create the cert in CurrentUser\My." +} + +# Match on Subject + Code Signing EKU (1.3.6.1.5.5.7.3.3). +$codeSigningOid = '1.3.6.1.5.5.7.3.3' +$existing = Get-ChildItem Cert:\CurrentUser\My | Where-Object { + $_.Subject -eq $subject -and + ($_.EnhancedKeyUsageList | ForEach-Object { $_.ObjectId }) -contains $codeSigningOid +} + +if ($existing -and $Force) { + Write-Step 'Removing existing certificate(s) (-Force)' + foreach ($c in $existing) { + Write-Host "Removing CurrentUser\My\$($c.Thumbprint)" + Remove-Item "Cert:\CurrentUser\My\$($c.Thumbprint)" -Force + $trusted = Get-ChildItem Cert:\LocalMachine\TrustedPeople -ErrorAction SilentlyContinue | + Where-Object { $_.Thumbprint -eq $c.Thumbprint } + if ($trusted) { + Write-Host "Removing LocalMachine\TrustedPeople\$($c.Thumbprint)" + Remove-Item "Cert:\LocalMachine\TrustedPeople\$($c.Thumbprint)" -Force + } + } + if (Test-Path $pfxPath) { + Write-Host "Removing $pfxPath" + Remove-Item $pfxPath -Force + } + $existing = $null +} + +if ($existing) { + $cert = $existing | Sort-Object NotAfter -Descending | Select-Object -First 1 + Write-Step 'Reusing existing certificate' +} else { + Write-Step 'Creating new self-signed certificate' + # Explicit TextExtension entries (Code Signing EKU + empty Basic Constraints) + # guard against quirks in older Windows PowerShell where -Type CodeSigningCert + # alone has occasionally produced certs AppX deployment rejects. + $cert = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $subject ` + -KeyUsage DigitalSignature ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -HashAlgorithm SHA256 ` + -NotAfter (Get-Date).AddYears(3) ` + -FriendlyName 'OpenClaw Dev MSIX Signing' ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}') +} + +Write-Host "Thumbprint: $($cert.Thumbprint)" +Write-Host "NotAfter: $($cert.NotAfter)" + +if (-not $SkipTrust) { + $alreadyTrusted = Get-ChildItem Cert:\LocalMachine\TrustedPeople -ErrorAction SilentlyContinue | + Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + if ($alreadyTrusted) { + Write-Step 'LocalMachine\TrustedPeople already contains this cert' + } else { + Write-Step 'Trusting certificate in LocalMachine\TrustedPeople' + # Public-only (.cer) import. The private key lives in CurrentUser\My + # (and the exported PFX); TrustedPeople only needs the public cert + # for AppX to validate package signatures. + $tempCer = Join-Path $env:TEMP "openclaw-dev-msix-$($cert.Thumbprint).cer" + try { + Export-Certificate -Cert $cert -FilePath $tempCer | Out-Null + Import-Certificate -FilePath $tempCer -CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' | Out-Null + } finally { + if (Test-Path $tempCer) { Remove-Item $tempCer -Force } + } + } +} + +Write-Step "Exporting PFX to $pfxPath" +if (-not (Test-Path $pfxDir)) { + New-Item -ItemType Directory -Path $pfxDir -Force | Out-Null +} +$securePwd = ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText +# Always re-export so the PFX matches the currently-active cert. Export-PfxCertificate +# overwrites without prompting. +Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $securePwd -Force | Out-Null +Write-Host "PFX written. The OpenClaw.Tray.WinUI MSBuild project auto-detects this file." + +Write-Step 'Next steps' +Write-Host @" +Build the MSIX (PackageMsix is already true in the project): + + dotnet publish src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj -c Debug -r win-x64 --nologo + +Install: + + Add-AppxPackage -Path .\src\OpenClaw.Tray.WinUI\AppPackages\<...>\<...>.msix + +Disable dev signing (revert to unsigned output) without removing the cert: + + Remove-Item '$pfxPath' + +Fully clean up dev signing artifacts: + + .\scripts\setup-dev-msix-cert.ps1 -Force # then delete the PFX, or: + Remove-Item '$pfxPath' -Force + Get-ChildItem Cert:\CurrentUser\My\$($cert.Thumbprint) | Remove-Item + Get-ChildItem Cert:\LocalMachine\TrustedPeople\$($cert.Thumbprint) | Remove-Item +"@ + diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 64f70ccf6..4cf164d1a 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -22,21 +22,52 @@ win-arm64 - - + + + MSIX + true + + + None true app.manifest - - MSIX - true - false true Never SideloadOnly + + $(LOCALAPPDATA)\OpenClawTray\dev-msix.pfx + false + true + $(OpenClawDevMsixPfx) + openclaw-dev @@ -93,11 +124,12 @@ - + this, the target would silently skip and the baseline version in the source + manifest would ship in the .msix package. --> + <_AppxManifestPath>$(MSBuildThisFileDirectory)Package.appxmanifest <_StrippedVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(Version)', '[-+].*$', '')) @@ -237,7 +269,7 @@ - + true diff --git a/src/OpenClaw.Tray.WinUI/Package.appxmanifest b/src/OpenClaw.Tray.WinUI/Package.appxmanifest index af0301e44..6c3b5d32a 100644 --- a/src/OpenClaw.Tray.WinUI/Package.appxmanifest +++ b/src/OpenClaw.Tray.WinUI/Package.appxmanifest @@ -9,13 +9,17 @@ - + + Version="1.0.0.0" /> OpenClaw Companion diff --git a/src/OpenClaw.Tray.WinUI/Properties/launchSettings.json b/src/OpenClaw.Tray.WinUI/Properties/launchSettings.json new file mode 100644 index 000000000..a540ef651 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "OpenClaw.Tray.WinUI": { + "commandName": "MsixPackage", + "hotReloadEnabled": false, + "debugEngines": "managed,native" + } + } +} From 87e528921420be7cf3f83c7c727d22a24fd00a41 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 5 Jun 2026 15:58:56 -0700 Subject: [PATCH 02/38] Add target-size 16/20/44 tray icon PNGs from PR #468 Master shipped sizes 24/32/48/256; Windows down-scaled larger PNGs for Start Menu / taskbar / Alt+Tab surfaces that prefer 16/20/44. These three PNGs fill the gap and are auto-discovered by MakePri via the existing Assets\**\* content include and the targetsize-NN_altform-unplated filename convention. No code, csproj, or manifest changes required. Cherry-picked from origin/pr-468 (Square44x44Logo.targetsize-{16,20,44}_altform-unplated.png). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...e44x44Logo.targetsize-16_altform-unplated.png | Bin 0 -> 885 bytes ...e44x44Logo.targetsize-20_altform-unplated.png | Bin 0 -> 1158 bytes ...e44x44Logo.targetsize-44_altform-unplated.png | Bin 0 -> 3785 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-16_altform-unplated.png create mode 100644 src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-20_altform-unplated.png create mode 100644 src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-44_altform-unplated.png diff --git a/src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-16_altform-unplated.png b/src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-16_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..516e2c9bd8c06b9f6c44141523d146b5f06efa9e GIT binary patch literal 885 zcmV-*1B(2KP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0}4q*K~y+TeUV#8 z6k!y{2h1MqLR1h1U7&{$im*G8>}E@A?8UvzIOEQYGdts~qpQ|-G1D!v9?XX#sTb)+ zSrOY?4-#~tNXuN!a$VigU3XV^U0tOz>?O++GnCK}{osQ?=luVN?|dJE{03TDGQo#s z$hsfH7N<$;+CRNGVO|HWotcUcd>PQvt>^_39O{jVcx$3W*PO1h5>XsIo}$o8QNo@R zC+xc>6O~%zDu205~eVS@rAF)ordShQuN^nN^a@L=nX-N z;gf_zm?F4!5>97Jl;PpHV0kviVeK)lcs|+kI19N$yH^e1&Y3Ys$=r-kJ{LPGdgZ}` zW^v0$Ty(@tJQcSHo8{UEyy)x{TRJyd#Lq>F#V@1RudhU!_f#(bXsG%cw|iiksDUA| zwq7<*e3Tq%{^}sOUnVIxmTdJ(lz1w?hGQSDN4UO>L zUa#!-%BpRRa~RB43ov$^y(QQD+b(z$hc80f&;*jX=ElM!`Vh!?lieUn2Tz0FR#rF0 z==OD>CU}Ao@SG+fQO(2mtFB#L`^j_RJTG+STqyM+KIO;$qFu|i^_%IRopvHM;@}`b zmqA(-VD?lABn2utg3|MS23MOpw?IdZZDUt1?R$@wf0GK_5W&kJMv0K*1V}Kh*|14` z6v)Nbt8)q=mu9uZ?G}2BTT6SiPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1RF_2K~y+TwUS*- zTXh)5>zp&2I=%I7aVpWxZHZYj39KTemA15np0D<_r|s#-Ij6S3#BLp-Ylj2Gxu_S@ zun*>@%c9xzLNC-gCnFeLKY+GSN>6EN3!@WcVM_?0drg$|DVrcL?}%O_&EYtj z)?x?q7`8j1A$@71q<4mujpUt#6AoKp#_2p#6h?L08%s7CGOY2_JYAp6a>S)slH6M8 z#t9P=)En_>grah|N&X-som-^0*6l z?(pf=E+#WdauhK>XEh}z)okTz9<6P^+e{%UK-th^IqGB@Ou zK8RADvxWS+P`ELn5&txA#Q<7a_^Z|Stw11nq|k--dOpu8ybzPR;SSOXX}#N16#gK% z@I-Le(JzNzJGz{Dy%TLmgB@-&49qvuFnfRop1|+U zlMF;<_CTrQUxfBBFWlT~{>HDetVA#ej4N4~&~T8}u`p?{zzk!CG{GzxQc}Y%0U9LLtf(qzMefasC@ZQj4G=dC*OSV zU)O~W1Iz|-e2XRW$R_a(o z6h$}1sp{_0dPPs)Zr#bye(PAW-V6y`3n?7|w+s|SC5A}LD5YK5%S2WjE^kg9qQ z7SukNGCCH&0(i9a{(p&;yPg`TZoc?ixvRZ@$MJLhFP%6&R@2%Mle^9b%H~;_u07*qoM6N<$f`Nz~^#A|> literal 0 HcmV?d00001 diff --git a/src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-44_altform-unplated.png b/src/OpenClaw.Tray.WinUI/Assets/Square44x44Logo.targetsize-44_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..cc00a0afcbeb3961c390fa2a3fbef1903758c6de GIT binary patch literal 3785 zcmV;)4mRPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D4qiz_K~z{r?U;FR z9Mzr2mF$vKvDt%A$+CtGNeIT+*bvJWvUOP+X*8Pq?zyLX&TieZMv@O~*~YT5WeT&d z-K`C~soEo(glsKI7Q9d~C^kNjbM=6%fUANr$pVt$dd9`eF_83d@m}Sju2mJ)wUh3HWt?462wr2a>mI<29pRXm~`EW0TjY}looifOKmMj`9S~Qp@m2)XVB)=sYTZ~#}nlvk~Em#RG zSv8n4s@_^sD!;WdzT7V7fAEZQW^S*3fbB1|I!|2RUyG}|8!jvyKD_0YWZ6uS_RZ5) z-SQG6!+Ui~L|Yu4?ESR1*o%5GW?K|93$Gr%uRSN{t0}E|aFS9_jL0RGV**v~giibF z9HqmORQmWzzO>Z1ZOLXp+MxW&q(t)6h(s%$A+=XWbc#3G@o~E1+tW065e*{DxRgEY zz1b$?KfGHj$8wDVS6sD=?;m}B?=Lj?XrlPhF`dkTCOcwc3DL*RC_G<8RVsu3Z;2=#VnW zkub=yOzRPAwj5^1(prvZ-YUkmS{3Fy7-_1Ud~`x@czfP#{;SnFqFSB5n>7FDNP;8A%&rG68N`}-y9A3?F(zwn6g=HTom`^znHo|?ImYT6KOFD; z(f3y8$na}RH79B|BWX}0uG3)2ZpK6#eJUeDl=jUV#aOJ-B68HF%Z^g4TX0li4Rq^2E;uQ(y_Qs3Zd>qg6g+d>VnM`ULsD$f^dy9ISGL~@ zZhn(Bt=Nh1YE8ENk!D!RV6bJybaQ%5pKgip}D zw*9{nrO(34Z^d9`+pSjKic#Z1^tT=QM8PqAX544k9jpX6BmXx$mrw`2+*sKVINo+YyiC zbs|u76az&E(N}x`?xJ>t_#JrN(00o&Jfx}m=d_uJ{}ppkTU>_Igc1{WBbFK|yrx%R zkBEIn{QPFTZZr}reo>_~FGL6s)A8{_aVc^h+K4?| zJ{D;Q{%@xP_Pk=`tl5Oqb`8+(z?D|phcm6r&BIYrATtf;?Zk!kyYbG3eRymA-gnPF zcKA&Hqeol=PqyE(?f>4K*=BrWu*8f3sRaqE1s;PgbL3=ks};Y9YvmYJiO{E%;iYl` z`Un#y>`u(q*fD6+43oW-~gdt9wO8jR4Nc4G?+5kFlMHaqMR6YI51yt z!~1PE%(*DcwOFv+X2nz!HMzhr_eEr4&9Fp#%`cT9WUwGDZAMQ~?K9c>pPAm=+I1K6 zoWJYgP#DzM;M1AWT~>wj6~ZyM$$B=k-sP4tbTt#*t$EEuOPn5-o+Sw~{J&VrP|1iw^+ zkk*2*rW#{{77P~E@5|QxTwLB-cWSVp9-cBg&KHQ#t+k+^ZiHLmz_7@X*-j&B1~Dy- zxPi|6(j#UkMo2r-Rx3uG6vmuXruevlL_lFdnrK3TzZ1cthLhQPcZJJOHB@!yGv_XB z)nh=-pqH$NTgbr2b0Q$2Zx$WZ(1__6#P!aMC5(2AkXEE93(__UX@Z82TZaI*>1=5G zuAA?PcNL#z{Y%5TO*Gpc+|1;NZ3OQgRqK5gkTG25Yf;d zJ%9soy$w?ojgZWFE?m@-d4nF(t?w8nHXg{<``iqvH~cnfD8!(A+d8)2C)VzbRMnxc zzzKJW1KtW6zA7h%MGS&61|cPbu#&yfnQNx4aAHQ-V9Tf>Iu!a|2n4HI_DGY_@F0W(t5d~>bao`IeIqJ=-=jmdxsOAatDTZ zPWS~30wVSoUoi%LA%h_vjZrOyA)cxIV!q&TLc&3m$o2I)a_$L|<$J;w&dvA1I|7c} zdjb?^B;~A{9a5>j=Hoy9u&=V@O<$1`-mN5h`6akiC`Qj_27TL{7$|nYQ|7>6xf4T` ztkD_sav8YG*r{T`s8N@R^%ltB=M}v?D$Kbzs4RPMnh_#MR($#!@@FCGC@-9=6(VgF zUYb;yGhcr$a=G8^*+f0vE#h48N(<1vLx9U$E$G=oqc@*Me*ukwBGwr67glFhmbgxh z<$4uHt?JWnZ`9?E3UlvebA=U(4QZ<|KH=nFo3!)p&N>`0lu6U1@OM$I9PTRO<%{|H zJ?9>G9Jy4Wo*a^K(I+iNzlekWayj~o_2@4m(O+W5AXkr&QiU;Ego#=)=4%KHlo?}P zk68~~D$wq8^HitD^#n#Nf*HS-vn^Zqj!EhY@AC-+kNRYaQ|?M)s&BgyXP>ZKIr|iO z#v_z1d!=G{6$1D*JOp(-M2rF?C;?K=DvVVNFiJ}?W>X!kOedDQe^*Cq$g zTW(tlh)E1fjR+`o2ooAa^#l?o0!h})S|m(bjF}Ax%S~fvAF*#g|CnXX>+6-@d}C9^ zpADpY@(l|^t z##>nnUsW{%A_sy}D?$niVU_tKGa;m8w{8->h2|^$8)>|gZnrYGR4m>JZ(C5?qMs>Eo3?(1PxCqgTc}ocq`bQ zkHJut6GK(@%xd8m*)gKE!c%N*J^P4Faxvd{@?5#{XMG~k$&jkd5LMl!>5xsx_TobU zbF1}D5n0wHa4Ne?=~vu^H5fE$uMcZv@QEmRxz!jfuYreK4Zny+OiyCGR*Q(%h@Piu zbeA*#a!y1G&xouQ0sT(fP<7WgvUTnnK|`ZsjBG_**Nljw5&c|d^3u}`r_BaL zjVi<~Y9wtMq!|q+>IqCXsc@yqgqY5D>g;C5)mzCdN0m)T>YEWa?u6e|_r;tgR`8nY z9tj&8wgoixo7jF*A=(g-n?}Nl8U&>dLXuNA}Mv7_v{M+xHfSmTPcJHW*2P94C6Ekp^H)1%i6W%SI=-=FdzTCs`ZaI!X zekaBhE{tlbv8;E&r)_a(=las{7VbCpm+gL^{rJ&S;Cj{b Date: Fri, 5 Jun 2026 16:04:22 -0700 Subject: [PATCH 03/38] Phase 2: drop -p:Unpackaged=true from WinUI csproj MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the conditional dispatch that produced an unpackaged WindowsPackageType=None + app.manifest build for the Inno installer path. Default packaging mode is now unconditionally MSIX; only the PackageMsix=true opt-in for .msix file production remains. Changes: - Make MSIX + true unconditional. - Delete the block (WindowsPackageType=None + ApplicationManifest=app.manifest). - Delete the CopyWebView2Loader target (only needed for the unpackaged layout; MSIX bundles the loader automatically). The unrelated CopyWebView2Loader target in OpenClaw.SetupPreview is untouched. - Rewrite the explanatory comment block to drop the Unpackaged mode paragraph (now describes one opt-in flag instead of two). src/OpenClaw.Tray.WinUI/app.manifest is now orphaned but left in place; Phase 3 will delete it together with the Inno installer. Callers that still pass -p:Unpackaged=true (scripts/build-inno-local.ps1 and the Inno publish job in .github/workflows/ci.yml) are transiently non-functional between Phase 2 and Phase 3 — the property becomes a no-op so the build emits an MSIX layout that Inno cannot consume. Both callers are deleted in Phase 3, per the all-phases-land-together branch workflow. Audit of App.xaml.cs:355-405 _isPostSetupRestart retry: KEEP. The branch is not Inno-specific. The tray itself spawns a fresh tray with --post-setup-restart --wait-for-pid after in-process SetupWindow completes (RestartAfterSetupAsync, line 3097). The 15s retry + AbandonedMutexException handler prevents the new tray from giving up on the single-instance mutex while the old tray is still exiting. AppRefactorContractTests:135-140 enforces the call pattern. Validation (per AGENTS.md): - build.ps1: all 5 projects built. - Shared.Tests: 2049 passed, 29 skipped (env-only), 0 failed. - Tray.Tests: 958 passed, 0 skipped, 0 failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OpenClaw.Tray.WinUI.csproj | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 4cf164d1a..30e8a6e52 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -22,7 +22,7 @@ win-arm64 - - + overrides these properties to sign with the production cert. --> + MSIX true - - None - true - app.manifest - - true Never @@ -268,18 +258,6 @@ - - - - - true - $(OutputPath)runtimes\win-arm64\native\WebView2Loader.dll - $(OutputPath)runtimes\win-x64\native\WebView2Loader.dll - - - - - From e443c3c543b6a467ed1597728d6003e895eac467 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 5 Jun 2026 16:13:12 -0700 Subject: [PATCH 04/38] Phase 3A: Remove Inno installer (MSIX-primary cleanup) Delete Inno Setup distribution path now that MSIX is the always-on package type. Phase 3B will follow up with Updatum removal; Phase 4 will rebuild the release pipeline around the .msix file produced by `-p:PackageMsix=true`. Deleted files (5 source + 2 test): - installer.iss (256 lines) - scripts/build-inno-local.ps1 - scripts/Uninstall-LocalGateway.ps1 - src/OpenClaw.Tray.WinUI/app.manifest (orphaned by Phase 2) - tests/PackagingTests/Test-InnoUninstallOrdering.ps1 (parent dir removed) - tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs (9 tests) - tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs (4 tests; Phase 5 will replace with MSIX-signing assertions) ci.yml: drop -p:Unpackaged=true from build/publish (Phase 2 cleanup), retire the Download VC Redist + Install Inno Setup + Build x64/arm64 Installer + Sign Installers steps in the release job, remove .exe entries from the release files: list and body, and update the paused build-msix comment. App.xaml.cs: remove the AppMutex coordination comment that referenced installer.iss; the OpenClawTray mutex itself stays unchanged. SetupEngine.UI/LogFileLauncher.cs: rewrite the "Unpackaged process" comment to cover both the no-package-identity and library-only call sites. Ship-guard ported from installer.iss to MSBuild: two new targets on OpenClaw.Tray.WinUI.csproj (ValidateSetupEngineUiNotShipped after Build, ValidateSetupEngineUiNotPublished after Publish) fail the build if OpenClaw.SetupEngine.UI.exe lands in the tray bin/publish output. Pairs with a new "Hazards" section in docs/SETUP_ENGINE_REDESIGN.md explaining the in-process design and pointing at PR #468's bootstrapper diffs as the worked example anyone splitting SetupEngine into its own process would need to study first. Test-ReleaseNativeDependencies.ps1: remove -RequireInstallerVCRedist / -InstallerVCRedistPath params and their dead handler (only Inno called them); -RequireAppLocalVCRuntime and -SkipNativeLoadProbe stay (still used by ci.yml's Verify Native Runtime Payload steps). Docs: drop the Inno helper section + .exe-installer references from DEVELOPMENT.md, docs/RELEASING.md, docs/VERSIONING.md, and the Inno comment in scripts/validate-msix-storage-paths.ps1. README.md + docs/SETUP.md download tables are replaced with TODO placeholders that Phase 7 will fill with MSIX-flavored content. Validation (on user/kmahone/msix, Windows): - ./build.ps1 green - Shared.Tests: 2049 passed / 29 skipped (unchanged from Phase 2) - Tray.Tests: 945 passed (was 958; -13 = 9 InstallerIssAssertion + 4 ReleaseSigningWorkflow tests removed, as expected) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 75 +-- DEVELOPMENT.md | 29 +- README.md | 9 +- docs/RELEASING.md | 8 +- docs/SETUP.md | 24 +- docs/SETUP_ENGINE_REDESIGN.md | 42 ++ docs/VERSIONING.md | 3 - installer.iss | 296 -------- scripts/Test-ReleaseNativeDependencies.ps1 | 18 - scripts/Uninstall-LocalGateway.ps1 | 625 ----------------- scripts/build-inno-local.ps1 | 199 ------ scripts/validate-msix-storage-paths.ps1 | 5 +- .../LogFileLauncher.cs | 2 +- src/OpenClaw.Tray.WinUI/App.xaml.cs | 5 - .../OpenClaw.Tray.WinUI.csproj | 24 + src/OpenClaw.Tray.WinUI/app.manifest | 31 - .../InstallerIssAssertionTests.cs | 202 ------ .../ReleaseSigningWorkflowTests.cs | 138 ---- .../Test-InnoUninstallOrdering.ps1 | 637 ------------------ 19 files changed, 104 insertions(+), 2268 deletions(-) delete mode 100644 installer.iss delete mode 100644 scripts/Uninstall-LocalGateway.ps1 delete mode 100644 scripts/build-inno-local.ps1 delete mode 100644 src/OpenClaw.Tray.WinUI/app.manifest delete mode 100644 tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs delete mode 100644 tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs delete mode 100644 tests/PackagingTests/Test-InnoUninstallOrdering.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0940d51b3..e8209d0f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -383,10 +383,10 @@ jobs: run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} - name: Build WinUI Tray App (Release) - run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} -p:Unpackaged=true + run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} - name: Publish WinUI Tray App - run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish -p:Unpackaged=true + run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish - name: Verify x64 Native Runtime Payload if: matrix.rid == 'win-x64' @@ -424,7 +424,7 @@ jobs: build-msix: needs: [test, e2etests] - if: false # Paused for alpha.4; ship Inno setup and portable ZIP artifacts only. + if: false # Paused; will be unpaused when MSIX-primary release pipeline lands (Phase 4). runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }} continue-on-error: true strategy: @@ -614,26 +614,6 @@ jobs: # checks still run. run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireAppLocalVCRuntime -SkipNativeLoadProbe - - name: Download Visual C++ Runtime Redistributables - shell: pwsh - run: | - $redists = @( - @{ Uri = "https://aka.ms/vc14/vc_redist.x64.exe"; Path = "vc_redist.x64.exe" }, - @{ Uri = "https://aka.ms/vc14/vc_redist.arm64.exe"; Path = "vc_redist.arm64.exe" } - ) - - foreach ($redist in $redists) { - Invoke-WebRequest -Uri $redist.Uri -OutFile $redist.Path - $signature = Get-AuthenticodeSignature -LiteralPath $redist.Path - if ($signature.Status -ne "Valid") { - throw "$($redist.Path) Authenticode signature was $($signature.Status)." - } - $subject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" } - if ($subject -notmatch "O=Microsoft Corporation") { - throw "$($redist.Path) signer was not Microsoft Corporation: $subject" - } - } - # Create ZIP files for Updatum auto-update (asset name must contain the RID). # We ship both x64 and arm64 portables now that the build job produces a # libsodium-compatible app-local VC runtime for both architectures (the @@ -647,50 +627,11 @@ jobs: run: | Compress-Archive -Path artifacts/tray-win-arm64/* -DestinationPath OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip - # Inno Setup installer for x64 - - name: Install Inno Setup - run: choco install innosetup -y - - - name: Build x64 Installer - run: | - # Prepare x64 files - mkdir publish-x64 - copy artifacts/tray-win-x64/* publish-x64/ -Recurse - .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath publish-x64 -RequireAppLocalVCRuntime -RequireInstallerVCRedist -InstallerVCRedistPath vc_redist.x64.exe - # Build installer - & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DMyAppVersion=${{ needs.test.outputs.majorMinorPatch }} /DMyAppArch=x64 /Dpublish=publish-x64 /DvcRedist=vc_redist.x64.exe installer.iss - - - name: Build arm64 Installer - run: | - # Prepare arm64 files - mkdir publish-arm64 - copy artifacts/tray-win-arm64/* publish-arm64/ -Recurse - # -RequireAppLocalVCRuntime: arm64 payload now ships VC runtime DLLs from the build job. - # -SkipNativeLoadProbe: this verifier runs on the x64 release runner and cannot - # LoadLibrary an arm64 DLL. - .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath publish-arm64 -RequireAppLocalVCRuntime -RequireInstallerVCRedist -InstallerVCRedistPath vc_redist.arm64.exe -SkipNativeLoadProbe - # Build installer - & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DMyAppVersion=${{ needs.test.outputs.majorMinorPatch }} /DMyAppArch=arm64 /Dpublish=publish-arm64 /DvcRedist=vc_redist.arm64.exe installer.iss - - - name: Sign Installers - uses: azure/artifact-signing-action@v2 - with: - endpoint: https://eus.codesigning.azure.net/ - signing-account-name: openclaw - certificate-profile-name: openclaw - files-folder: Output - files-folder-filter: exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - name: Create Release uses: softprops/action-gh-release@v3 with: generate_release_notes: true files: | - Output/OpenClawCompanion-Setup-x64.exe - Output/OpenClawCompanion-Setup-arm64.exe OpenClawTray-${{ needs.test.outputs.semVer }}-win-x64.zip OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip prerelease: ${{ contains(github.ref_name, '-') }} @@ -699,14 +640,14 @@ jobs: ## OpenClaw Windows Hub ${{ github.ref_name }} ### Downloads - - **Installer (x64)**: `OpenClawCompanion-Setup-x64.exe` - Intel/AMD 64-bit - - **Installer (ARM64)**: `OpenClawCompanion-Setup-arm64.exe` - Windows on ARM (Surface, etc.) - **Portable x64**: `OpenClawTray-${{ needs.test.outputs.semVer }}-win-x64.zip` - **Portable ARM64**: `OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip` + > MSIX installer artifacts arrive in a follow-up release once the + > MSIX-primary distribution pipeline lands. + ### Features - 🦞 System tray integration with gateway status - - 🔄 Auto-updates from GitHub Releases - ✅ Code-signed with Azure Artifact Signing ### Requirements @@ -715,6 +656,6 @@ jobs: - OpenClaw gateway running locally ### Quick Start - 1. Run the installer for your architecture - 2. Launch from Start Menu or system tray + 1. Extract the portable ZIP and run `OpenClaw.Tray.WinUI.exe` + 2. Launch from system tray 3. Right-click tray icon → Settings to configure diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f7f93754c..3f435b11c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -169,23 +169,6 @@ dotnet publish src/OpenClaw.Tray.WinUI -c Release -r win-x64 --self-contained -o This creates a standalone executable with all dependencies bundled. -#### Local Inno Installer Iteration - -Use the local helper to build unsigned installer EXEs without waiting for CI: - -```powershell -# Fast x64 installer for Windows Sandbox smoke tests -.\scripts\build-inno-local.ps1 -Arch x64 -Fast - -# Recompile Inno only after changing installer.iss -.\scripts\build-inno-local.ps1 -Arch x64 -Fast -NoPublish - -# Build both release-shaped architectures locally -.\scripts\build-inno-local.ps1 -Arch All -``` - -`-Fast` uses ZIP/no-solid compression for quick local iteration. CI release builds keep the default LZMA solid compression and Azure signing. - ## Architecture Overview ### Native chat surface (FunctionalUI + OpenClaw.Chat) @@ -577,15 +560,11 @@ When a tag is pushed (e.g., `git tag v1.2.3 && git push origin v1.2.3`): - All artifacts built for x64 and ARM64 - Executables signed with Azure Trusted Signing certificate -2. **Create Installers:** - - Inno Setup creates Windows installers - - Separate installers for x64 and ARM64 - -3. **GitHub Release:** +2. **GitHub Release:** - Automatic release created with tag name - - Includes: - - Installers: `OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe` - - Portable ZIPs: `OpenClawTray-{version}-win-x64.zip`, `OpenClawTray-{version}-win-arm64.zip` + - Includes portable ZIPs: `OpenClawTray-{version}-win-x64.zip`, `OpenClawTray-{version}-win-arm64.zip` + - MSIX installer artifacts will be added once the MSIX-primary distribution + pipeline lands (see the MSIX-primary publishing plan). - Release notes auto-generated from commits ### Monitoring CI diff --git a/README.md b/README.md index 929bcd525..f8eb739a9 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,12 @@ This monorepo contains the Windows hub, shared client libraries, and CLI utiliti Direct downloads from the latest OpenClaw release: -- [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-x64.exe) -- [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-arm64.exe) -- [OpenClawCompanion-SHA256SUMS.txt](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-SHA256SUMS.txt) + +- MSIX installer downloads coming soon — see [docs/SETUP.md](docs/SETUP.md). ### Prerequisites - Windows 10 (20H2+) or Windows 11 diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ee587e170..182dc6a3d 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -75,16 +75,12 @@ git push origin vX.Y.Z-alpha.N For the current alpha flow, ship only: -- Inno setup installers: - - `OpenClawCompanion-Setup-x64.exe` - - `OpenClawCompanion-Setup-arm64.exe` - Portable ZIP payloads for Updatum: - `OpenClawTray--win-x64.zip` - `OpenClawTray--win-arm64.zip` -MSIX artifacts are intentionally paused for alpha while we focus on the Inno -installer path and signed portable update payloads. Re-enable MSIX only when we -explicitly want packaged camera/microphone consent validation again. +MSIX artifacts will become the primary distribution surface in a follow-up +phase; see the MSIX-primary publishing plan on the `user/kmahone/msix` branch. ## Executable signing policy diff --git a/docs/SETUP.md b/docs/SETUP.md index f804f6501..df163b7eb 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -15,21 +15,25 @@ You do **not** need a pre-existing local OpenClaw gateway before installing. On ### 1. Download the Installer -Download the latest stable installer from the canonical OpenClaw release assets: + -| File | Architecture | -|------|-------------| -| [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-x64.exe) | Intel / AMD (most PCs) | -| [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-arm64.exe) | ARM64 (Surface Pro X, Snapdragon laptops) | -| [OpenClawCompanion-SHA256SUMS.txt](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-SHA256SUMS.txt) | SHA-256 checksums | - -If you're unsure, use the **x64** installer. +> **MSIX installer downloads coming soon.** The Inno `.exe` installer has been +> retired and the MSIX-primary distribution pipeline is in progress. Build +> locally from source in the meantime — see [DEVELOPMENT.md](../DEVELOPMENT.md). ### 2. Run the Installer -Double-click the downloaded `.exe`. Windows may show a SmartScreen prompt — click **More info → Run anyway** (this is normal for code-signed apps that haven't yet accumulated reputation). +Once the MSIX download is published, double-click the downloaded `.msix` +package to install it via the Windows App Installer. SmartScreen may show a +prompt the first time — click **More info → Run anyway** (this is normal for +newly published code-signed apps that haven't yet accumulated reputation). -The installer runs without requiring administrator privileges. +MSIX installs do not require administrator privileges. ### 3. Choose Optional Components diff --git a/docs/SETUP_ENGINE_REDESIGN.md b/docs/SETUP_ENGINE_REDESIGN.md index 747cdf43d..47ca2e1ad 100644 --- a/docs/SETUP_ENGINE_REDESIGN.md +++ b/docs/SETUP_ENGINE_REDESIGN.md @@ -12,6 +12,48 @@ The bundled `default-config.json` ships with the tray executable and provides se --- +## Hazards + +### Don't split `OpenClaw.SetupEngine.UI` into its own process + +`OpenClaw.SetupEngine.UI` is intentionally a class library that the tray +references and hosts in-process. The tray's `App.xaml.cs` constructs +`SetupWindow` directly and reuses the tray's single-instance mutex, deep-link +surface, and post-setup restart sequence. + +There is an MSBuild ship-guard in +`src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj` that fails the build if +`OpenClaw.SetupEngine.UI.exe` appears in the tray's `bin/` or `publish/` +output. (Before MSIX became primary, the same guard lived in `installer.iss` +as a `#error` preprocessor check.) Do not weaken or remove it without +deliberately re-evaluating the single-process design below. + +If you ever do split SetupEngine into a standalone executable inside the MSIX: + +- You'll need to copy XAML/resource files into SetupEngine.UI's own publish + directory (its own `CopyXamlResourcesToPublishDirectory` MSBuild target) so + the standalone `.exe` can find them — `OpenClaw.Tray.WinUI` no longer owns + the resource pipeline for SetupEngine.UI in that world. +- Standalone WinUI 3 executables need to bootstrap the Windows App Runtime + themselves before any framework call. That means a `WindowsAppRuntime_EnsureIsLoaded` + P/Invoke in `SetupEngine.UI/Program.cs` (or equivalent C# bootstrapper). + Inside the tray today this happens automatically via the MSIX + `Dependencies > Package Name="Microsoft.WindowsAppRuntime.*"` element. +- The single-instance mutex (`App.xaml.cs:355-405`) and the + `--post-setup-restart`/`--wait-for-pid` handoff would need to coordinate + across two processes instead of being intra-process state. +- The `openclaw://` deep-link surface, the tray flyout, and the StartupTask + registration all live in the tray today. Splitting setup out means each of + those needs an explicit cross-process protocol for what setup needs to + announce back to the tray. + +PR #468 contains a worked example of the bootstrapper diffs needed (search +its tree for `OpenClaw.SetupEngine.UI.csproj` `CopyXamlResourcesToPublishDirectory` +and `Program.cs` `WindowsAppRuntime_EnsureIsLoaded`) — use it as a starting +point if you ever revisit this decision. + +--- + ## Architecture ``` diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 931261a18..2d8ba29cc 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -80,9 +80,6 @@ For example: .\scripts\Get-OpenClawVersion.ps1 -Variable MajorMinorPatch ``` -`scripts\build-inno-local.ps1` uses that helper for Inno's `AppVersion` when -`-Version` is not explicitly supplied. - ## Guardrails - Do not add `` release literals to product `.csproj` files. diff --git a/installer.iss b/installer.iss deleted file mode 100644 index d4dbd9aaf..000000000 --- a/installer.iss +++ /dev/null @@ -1,296 +0,0 @@ -; OpenClaw Companion Inno Setup Script (WinUI version) -#define MyAppName "OpenClaw Companion" -#define MyAppPublisher "Scott Hanselman" -#define MyAppURL "https://github.com/openclaw/openclaw-windows-node" -#define MyAppExeName "OpenClaw.Tray.WinUI.exe" - -; MyAppArch should be passed via /DMyAppArch=x64 or /DMyAppArch=arm64 -#ifndef MyAppArch - #define MyAppArch "x64" -#endif - -#ifndef MyCompression - #define MyCompression "lzma" -#endif - -#ifndef MySolidCompression - #define MySolidCompression "yes" -#endif - -[Setup] -; Inno requires "{{" to emit a literal opening brace in AppId. -; Do not add a second closing brace here; that creates a malformed uninstall registry key. -AppId={{M0LTB0T-TRAY-4PP1-D3N7} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL=https://github.com/openclaw/openclaw-windows-node/issues -AppUpdatesURL=https://github.com/openclaw/openclaw-windows-node/releases -DefaultDirName={localappdata}\OpenClawTray -DefaultGroupName={#MyAppName} -DisableProgramGroupPage=yes -OutputBaseFilename=OpenClawCompanion-Setup-{#MyAppArch} -Compression={#MyCompression} -SolidCompression={#MySolidCompression} -WizardStyle=modern -PrivilegesRequired=lowest -SetupIconFile=src\OpenClaw.Tray.WinUI\Assets\openclaw.ico -UninstallDisplayIcon={app}\{#MyAppExeName} -; Round 2 (Scott #5): block install/uninstall while the tray is running. -; Mutex name matches App.xaml.cs (`new Mutex(true, "OpenClawTray", …)`). -; Tray and Inno run in the same user session, so the bare name resolves -; against Local\OpenClawTray — no Global\ prefix needed. -AppMutex=OpenClawTray -#if MyAppArch == "arm64" -ArchitecturesInstallIn64BitMode=arm64 -ArchitecturesAllowed=arm64 -#else -ArchitecturesInstallIn64BitMode=x64 -ArchitecturesAllowed=x64 -#endif - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -; publish folder should be passed via /Dpublish=publish-x64 or /Dpublish=publish-arm64 -#ifndef publish - #define publish "publish" -#endif - -#if !FileExists(publish + "\OpenClaw.Tray.WinUI.exe") - #error Tray payload missing. Publish OpenClaw.Tray.WinUI before compiling the installer. -#endif - -#if FileExists(publish + "\SetupEngine\OpenClaw.SetupEngine.UI.exe") - #error SetupEngine.UI.exe should not be shipped. Setup UI is hosted by OpenClaw.Tray.WinUI.exe. -#endif - -; vcRedist should point at the architecture-matching Visual C++ Runtime -; redistributable in CI release builds. -#ifndef vcRedist - #define vcRedist "" -#endif - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "startupicon"; Description: "Start OpenClaw Companion when Windows starts"; GroupDescription: "Startup:"; Flags: unchecked - -[Files] -; WinUI Tray app - include all files (WinUI needs DLLs, not single-file) -Source: "{#publish}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs -; WSL gateway uninstall helper copied to {tmp} by [Code] during uninstall. -Source: "scripts\Uninstall-LocalGateway.ps1"; DestDir: "{app}"; Flags: ignoreversion -#if vcRedist != "" -Source: "{#vcRedist}"; DestDir: "{tmp}"; DestName: "vc_redist.exe"; Flags: deleteafterinstall; AfterInstall: InstallVCRuntime -#endif - -[Registry] -Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: ""; ValueData: "URL:OpenClaw Protocol"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" -Root: HKCU; Subkey: "Software\Classes\openclaw\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "Software\Classes\openclaw\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" - -[Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Gateway Setup"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://setup"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Companion Settings"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://commandcenter"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Chat"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://chat"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\Check for Updates"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://check-updates"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon -Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon - -[Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent; Check: ShouldLaunchTray - -[Code] -var - VCRuntimeInstallSucceeded: Boolean; - LocalGatewayCleanupChoiceInitialized: Boolean; - LocalGatewayCleanupRequested: Boolean; - LocalGatewayCleanupSucceeded: Boolean; - -#if vcRedist != "" -procedure InstallVCRuntime; -var - ResultCode: Integer; - Started: Boolean; -begin - VCRuntimeInstallSucceeded := False; - Log('Running bundled Visual C++ Runtime redistributable.'); - Started := - Exec( - ExpandConstant('{tmp}\vc_redist.exe'), - '/install /quiet /norestart', - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode); - - if not Started then - begin - Log('Failed to start Visual C++ Runtime redistributable. System error: ' + IntToStr(ResultCode) + '.'); - Exit; - end; - - VCRuntimeInstallSucceeded := (ResultCode = 0) or (ResultCode = 3010) or (ResultCode = 1641); - if VCRuntimeInstallSucceeded then - Log('Visual C++ Runtime redistributable exited with success code ' + IntToStr(ResultCode) + '.') - else - Log('Visual C++ Runtime redistributable failed with exit code ' + IntToStr(ResultCode) + '.'); -end; -#endif - -function ShouldLaunchTray: Boolean; -begin -#if vcRedist != "" - Result := VCRuntimeInstallSucceeded; - if not Result then - Log('Skipping post-install tray launch because Visual C++ Runtime installation did not succeed.'); -#else - Result := True; -#endif -end; - -procedure EnsureLocalGatewayCleanupChoice; -begin - if LocalGatewayCleanupChoiceInitialized then - Exit; - - LocalGatewayCleanupChoiceInitialized := True; - - if UninstallSilent() then - begin - LocalGatewayCleanupRequested := True; - Log('Silent uninstall: local gateway cleanup will run automatically.'); - end - else - begin - LocalGatewayCleanupRequested := - MsgBox( - 'Do you also want to remove the OpenClaw local WSL gateway?' + #13#10#13#10 + - 'Choose Yes to unregister the OpenClawGateway WSL distro and remove generated local gateway state.' + #13#10 + - 'Choose No to leave the local gateway and generated local state on this computer.', - mbConfirmation, - MB_YESNO) = IDYES; - - if LocalGatewayCleanupRequested then - Log('User chose to remove the local WSL gateway.') - else - Log('User chose to preserve the local WSL gateway and generated state.'); - end; -end; - -function RunLocalGatewayCleanupOnce(var ResultCode: Integer): Boolean; -var - SourceScriptPath: string; - TempScriptPath: string; - Params: string; -begin - SourceScriptPath := ExpandConstant('{app}\Uninstall-LocalGateway.ps1'); - TempScriptPath := ExpandConstant('{tmp}\Uninstall-LocalGateway.ps1'); - - if not FileExists(SourceScriptPath) then - begin - ResultCode := 2; - Log('Local gateway cleanup script is missing: ' + SourceScriptPath); - Result := False; - Exit; - end; - - if FileExists(TempScriptPath) then - DeleteFile(TempScriptPath); - - if not CopyFile(SourceScriptPath, TempScriptPath, False) then - begin - ResultCode := 3; - Log('Failed to copy local gateway cleanup script to: ' + TempScriptPath); - Result := False; - Exit; - end; - - Params := - '-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(TempScriptPath) + - ' -AppRoot ' + AddQuotes(ExpandConstant('{app}')); - - Log('Running local gateway cleanup script from {tmp}.'); - Result := - Exec( - ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'), - Params, - '', - SW_HIDE, - ewWaitUntilTerminated, - ResultCode); - - if Result then - Log('Local gateway cleanup script exited with code ' + IntToStr(ResultCode) + '.') - else - Log('Failed to start local gateway cleanup script. System error: ' + IntToStr(ResultCode) + '.'); -end; - -procedure RunLocalGatewayCleanup; -var - ResultCode: Integer; - Retry: Boolean; - Started: Boolean; -begin - if not LocalGatewayCleanupRequested then - Exit; - - LocalGatewayCleanupSucceeded := False; - - repeat - Retry := False; - UninstallProgressForm.StatusLabel.Caption := 'Removing local WSL gateway...'; - Started := RunLocalGatewayCleanupOnce(ResultCode); - - if Started and (ResultCode = 0) then - begin - LocalGatewayCleanupSucceeded := True; - Log('Local gateway cleanup completed successfully.'); - Exit; - end; - - if UninstallSilent() then - begin - Log('Local gateway cleanup failed during silent uninstall; continuing without deleting generated state.'); - Exit; - end; - - Retry := - MsgBox( - 'OpenClaw could not remove the local WSL gateway.' + #13#10#13#10 + - 'Exit code: ' + IntToStr(ResultCode) + #13#10#13#10 + - 'Select Retry to try again, or Cancel to continue uninstalling OpenClaw and leave local gateway state on disk.', - mbError, - MB_RETRYCANCEL) = IDRETRY; - until not Retry; - - Log('User continued uninstall after local gateway cleanup failed; generated state will be preserved.'); -end; - -procedure DeleteGeneratedAppState; -begin - if not LocalGatewayCleanupSucceeded then - Exit; - - if DelTree(ExpandConstant('{app}'), True, True, True) then - Log('Deleted generated app state from {app}.') - else - Log('Generated app state in {app} could not be fully deleted; continuing uninstall.'); -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -begin - if CurUninstallStep = usUninstall then - begin - EnsureLocalGatewayCleanupChoice; - RunLocalGatewayCleanup; - end - else if CurUninstallStep = usPostUninstall then - begin - DeleteGeneratedAppState; - end; -end; diff --git a/scripts/Test-ReleaseNativeDependencies.ps1 b/scripts/Test-ReleaseNativeDependencies.ps1 index 5661ce230..eb5fb98c6 100644 --- a/scripts/Test-ReleaseNativeDependencies.ps1 +++ b/scripts/Test-ReleaseNativeDependencies.ps1 @@ -15,10 +15,6 @@ param( [switch]$RequireAppLocalVCRuntime, - [switch]$RequireInstallerVCRedist, - - [string]$InstallerVCRedistPath, - [switch]$SkipNativeLoadProbe ) @@ -330,20 +326,6 @@ if ($shouldProbeNativeLoad) { Add-TtsNativeStackProbeErrors } -if ($RequireInstallerVCRedist) { - $redist = if ([string]::IsNullOrWhiteSpace($InstallerVCRedistPath)) { - Join-Path $payloadRoot "vc_redist.x64.exe" - } else { - $InstallerVCRedistPath - } - - if (-not (Test-Path -LiteralPath $redist)) { - $errors.Add("Missing bundled Visual C++ Runtime redistributable at $redist.") - } elseif ($runningOnWindows) { - Add-MicrosoftSignatureErrors -File (Get-Item -LiteralPath $redist) - } -} - if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_ } exit 1 diff --git a/scripts/Uninstall-LocalGateway.ps1 b/scripts/Uninstall-LocalGateway.ps1 deleted file mode 100644 index f8eefbf81..000000000 --- a/scripts/Uninstall-LocalGateway.ps1 +++ /dev/null @@ -1,625 +0,0 @@ -<# -.SYNOPSIS - Removes the OpenClaw local WSL gateway during app uninstall. - -.DESCRIPTION - This helper is launched by the Inno uninstaller after the user chooses to - remove the local gateway. It deliberately calls WSL directly instead of - launching OpenClaw binaries from the install directory, so the app payload is - not kept loaded while Inno removes installed files. -#> - -[CmdletBinding()] -param( - [string]$AppRoot = $PSScriptRoot -) - -$ErrorActionPreference = 'Stop' - -$DistroName = 'OpenClawGateway' -$resultPath = Join-Path $AppRoot 'uninstall-gateway-result.json' -$errorPath = Join-Path $AppRoot 'uninstall-gateway-error.log' -$wslLogPath = Join-Path $AppRoot 'uninstall-gateway-wsl.log' -$cleanupWarnings = New-Object 'System.Collections.Generic.List[string]' - -function Ensure-AppRoot { - if (-not [string]::IsNullOrWhiteSpace($AppRoot) -and -not (Test-Path -LiteralPath $AppRoot)) { - New-Item -ItemType Directory -Path $AppRoot -Force | Out-Null - } -} - -function Write-GatewayLog { - param([string]$Message) - - try { - Ensure-AppRoot - "[$(Get-Date -Format 'o')] $Message" | Out-File -LiteralPath $wslLogPath -Encoding UTF8 -Append -Force - } catch { - Write-Verbose "Failed to write gateway uninstall log: $($_.Exception.Message)" - } -} - -function Add-CleanupWarning { - param([string]$Message) - - $script:cleanupWarnings.Add($Message) - Write-GatewayLog "Windows artifact cleanup warning: $Message" -} - -function Write-GatewayResult { - param( - [bool]$Succeeded, - [int]$ExitCode, - [string]$Message, - [object]$Details = $null - ) - - try { - Ensure-AppRoot - [ordered]@{ - timestamp = (Get-Date).ToString('o') - succeeded = $Succeeded - exitCode = $ExitCode - message = $Message - details = $Details - } | ConvertTo-Json -Depth 5 | Out-File -LiteralPath $resultPath -Encoding UTF8 -Force - } catch { - $fallback = "[$(Get-Date -Format 'o')] Failed to write gateway uninstall result: $($_.Exception.Message)" - try { $fallback | Out-File -LiteralPath $errorPath -Encoding UTF8 -Force } catch {} - } -} - -function Resolve-AppDataDir { - if ($env:OPENCLAW_TRAY_DATA_DIR) { - return $env:OPENCLAW_TRAY_DATA_DIR - } - - return Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData)) 'OpenClawTray' - } - - function Resolve-LocalDataDir { - if ($env:OPENCLAW_TRAY_LOCALAPPDATA_DIR) { - return Join-Path $env:OPENCLAW_TRAY_LOCALAPPDATA_DIR 'OpenClawTray' - } - - if ($env:OPENCLAW_TRAY_LOCAL_DATA_DIR) { - return $env:OPENCLAW_TRAY_LOCAL_DATA_DIR - } - - return Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) 'OpenClawTray' - } - - function Get-JsonPropertyValue { - param( - [object]$Object, - [string]$Name - ) - - if ($null -eq $Object) { - return $null - } - - $property = $Object.PSObject.Properties[$Name] - if ($null -eq $property) { - return $null - } - - return $property.Value - } - - function Read-JsonFile { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - return $null - } - - try { - return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop - } catch { - Add-CleanupWarning "Failed to read JSON file '$Path': $($_.Exception.Message)" - return $null - } - } - - function Write-JsonFileAtomic { - param( - [string]$Path, - [object]$Value - ) - - $directory = Split-Path -Parent $Path - if (-not [string]::IsNullOrWhiteSpace($directory) -and -not (Test-Path -LiteralPath $directory)) { - New-Item -ItemType Directory -Path $directory -Force | Out-Null - } - - $tempPath = Join-Path $directory ('.' + (Split-Path -Leaf $Path) + '.' + [Guid]::NewGuid().ToString('N') + '.tmp') - try { - $Value | ConvertTo-Json -Depth 50 | Out-File -LiteralPath $tempPath -Encoding UTF8 -Force - Move-Item -LiteralPath $tempPath -Destination $Path -Force - } catch { - Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue - throw - } - } - - function Test-LocalGatewayUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $false - } - - try { - $uri = [Uri]$Url - $host = $uri.Host.ToLowerInvariant() - return $host -eq 'localhost' -or $host -eq '127.0.0.1' -or $host -eq '::1' -or $host -eq '[::1]' - } catch { - return $false - } - } - - function Test-SetupManagedLocalRecord { - param([object]$Record) - - $isLocal = [bool](Get-JsonPropertyValue $Record 'isLocal') - $sshTunnel = Get-JsonPropertyValue $Record 'sshTunnel' - if (-not $isLocal -or $null -ne $sshTunnel) { - return $false - } - - $setupManagedDistroName = [string](Get-JsonPropertyValue $Record 'setupManagedDistroName') - if ([string]::Equals($setupManagedDistroName, $DistroName, [StringComparison]::Ordinal)) { - return $true - } - - if (-not [string]::IsNullOrWhiteSpace($setupManagedDistroName)) { - return $false - } - - $friendlyName = [string](Get-JsonPropertyValue $Record 'friendlyName') - $url = [string](Get-JsonPropertyValue $Record 'url') - return [string]::Equals($friendlyName, "Local ($DistroName)", [StringComparison]::Ordinal) -and (Test-LocalGatewayUrl $url) - } - - function Test-ExternalGatewayRecord { - param([object]$Record) - - $isLocal = [bool](Get-JsonPropertyValue $Record 'isLocal') - $sshTunnel = Get-JsonPropertyValue $Record 'sshTunnel' - $url = [string](Get-JsonPropertyValue $Record 'url') - return (-not $isLocal) -and -not ($null -eq $sshTunnel -and (Test-LocalGatewayUrl $url)) - } - - function Remove-FileIfExists { - param( - [string]$Path, - [string]$Label - ) - - try { - if (Test-Path -LiteralPath $Path -PathType Leaf) { - Remove-Item -LiteralPath $Path -Force -ErrorAction Stop - Write-GatewayLog "Deleted $Label." - } else { - Write-GatewayLog "$Label already absent." - } - } catch { - Add-CleanupWarning "Failed to delete $Label '$Path': $($_.Exception.Message)" - } - } - - function Remove-DirectoryIfExists { - param( - [string]$Path, - [string]$Label - ) - - try { - if (Test-Path -LiteralPath $Path -PathType Container) { - Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop - Write-GatewayLog "Deleted $Label directory." - } - } catch { - Add-CleanupWarning "Failed to delete $Label directory '$Path': $($_.Exception.Message)" - } - } - - function Remove-AutostartRegistryValue { - $runKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' - try { - $value = Get-ItemProperty -LiteralPath $runKey -Name 'OpenClawTray' -ErrorAction SilentlyContinue - if ($null -ne $value) { - Remove-ItemProperty -LiteralPath $runKey -Name 'OpenClawTray' -ErrorAction Stop - Write-GatewayLog 'Removed OpenClawTray autostart registry value.' - } else { - Write-GatewayLog 'OpenClawTray autostart registry value already absent.' - } - } catch { - Add-CleanupWarning "Failed to remove OpenClawTray autostart registry value: $($_.Exception.Message)" - } - } - - function Remove-SetupManagedGatewayRecords { - param([string]$DataDir) - - $gatewaysPath = Join-Path $DataDir 'gateways.json' - $registry = Read-JsonFile $gatewaysPath - if ($null -eq $registry) { - return [pscustomobject]@{ - RemainingCount = 0 - HasExternalGateways = $false - } - } - - $gatewayProperty = $registry.PSObject.Properties['gateways'] - $records = @() - if ($null -ne $gatewayProperty -and $null -ne $gatewayProperty.Value) { - $records = @($gatewayProperty.Value) - } - - $remaining = New-Object System.Collections.ArrayList - $removed = New-Object System.Collections.ArrayList - foreach ($record in $records) { - if (Test-SetupManagedLocalRecord $record) { - [void]$removed.Add($record) - } else { - [void]$remaining.Add($record) - } - } - - foreach ($record in $removed) { - $id = [string](Get-JsonPropertyValue $record 'id') - if ([string]::IsNullOrWhiteSpace($id)) { - continue - } - - $identityDir = Join-Path (Join-Path $DataDir 'gateways') $id - try { - if (Test-Path -LiteralPath $identityDir -PathType Container) { - Remove-Item -LiteralPath $identityDir -Recurse -Force -ErrorAction Stop - Write-GatewayLog "Deleted identity directory for local gateway record $id." - } - } catch { - Add-CleanupWarning "Failed to delete identity directory '$identityDir': $($_.Exception.Message)" - } - } - - if ($removed.Count -gt 0) { - try { - $registry.gateways = @($remaining.ToArray()) - $activeId = [string](Get-JsonPropertyValue $registry 'activeId') - if ($removed | Where-Object { [string](Get-JsonPropertyValue $_ 'id') -eq $activeId }) { - $registry.activeId = $null - } - - Write-JsonFileAtomic -Path $gatewaysPath -Value $registry - Write-GatewayLog "Removed $($removed.Count) setup-managed local gateway record(s)." - } catch { - Add-CleanupWarning "Failed to update gateways.json: $($_.Exception.Message)" - } - } else { - Write-GatewayLog 'No setup-managed local gateway records found.' - } - - $hasExternalGateways = $false - foreach ($record in @($remaining.ToArray())) { - if (Test-ExternalGatewayRecord $record) { - $hasExternalGateways = $true - break - } - } - - return [pscustomobject]@{ - RemainingCount = $remaining.Count - HasExternalGateways = $hasExternalGateways - } - } - - function Clear-RootDeviceTokenForRole { - param( - [string]$DataDir, - [string]$Role - ) - - $keyPath = Join-Path $DataDir 'device-key-ed25519.json' - $keyData = Read-JsonFile $keyPath - if ($null -eq $keyData) { - Write-GatewayLog "Root device identity file absent or unreadable for $Role token cleanup." - return - } - - $tokenPropertyName = if ($Role -eq 'node') { 'NodeDeviceToken' } else { 'DeviceToken' } - $scopesPropertyName = if ($Role -eq 'node') { 'NodeDeviceTokenScopes' } else { 'DeviceTokenScopes' } - $tokenProperty = $keyData.PSObject.Properties[$tokenPropertyName] - - if ($null -eq $tokenProperty -or [string]::IsNullOrEmpty([string]$tokenProperty.Value)) { - Write-GatewayLog "Root $Role device token already absent." - return - } - - try { - $tokenProperty.Value = $null - $scopesProperty = $keyData.PSObject.Properties[$scopesPropertyName] - if ($null -ne $scopesProperty) { - $scopesProperty.Value = $null - } - - Write-JsonFileAtomic -Path $keyPath -Value $keyData - Write-GatewayLog "Cleared root $Role device token." - } catch { - Add-CleanupWarning "Failed to clear root $Role device token: $($_.Exception.Message)" - } - } - - function Reset-OnboardingSettings { - param( - [string]$DataDir, - [bool]$PreserveNodeSettings - ) - - $settingsPath = Join-Path $DataDir 'settings.json' - $settings = Read-JsonFile $settingsPath - if ($null -eq $settings) { - Write-GatewayLog 'settings.json absent or unreadable; onboarding settings not reset.' - return - } - - $changed = $false - if ($settings.PSObject.Properties['GatewayUrl']) { - $settings.PSObject.Properties.Remove('GatewayUrl') - $changed = $true - } - - if (-not $PreserveNodeSettings -and $settings.PSObject.Properties['EnableNodeMode']) { - $settings.EnableNodeMode = $false - $changed = $true - } - - if (-not $PreserveNodeSettings -and $settings.PSObject.Properties['AutoStart']) { - $settings.AutoStart = $false - $changed = $true - } - - if (-not $changed) { - Write-GatewayLog 'No onboarding settings needed reset.' - return - } - - try { - Write-JsonFileAtomic -Path $settingsPath -Value $settings - Write-GatewayLog 'Reset onboarding settings.' - } catch { - Add-CleanupWarning "Failed to reset onboarding settings: $($_.Exception.Message)" - } - } - - function Remove-KeepaliveMarker { - param([string]$LocalDataDir) - - $markerDir = Join-Path $LocalDataDir 'wsl-keepalive' - $markerPath = Join-Path $markerDir "$DistroName.json" - Remove-FileIfExists -Path $markerPath -Label 'keepalive marker' - - try { - if ((Test-Path -LiteralPath $markerDir -PathType Container) -and -not (Get-ChildItem -LiteralPath $markerDir -Force -ErrorAction Stop | Select-Object -First 1)) { - Remove-Item -LiteralPath $markerDir -Force -ErrorAction Stop - Write-GatewayLog 'Deleted empty wsl-keepalive directory.' - } - } catch { - Add-CleanupWarning "Failed to remove empty wsl-keepalive directory '$markerDir': $($_.Exception.Message)" - } - } - - function Remove-WindowsGatewayArtifacts { - $dataDir = Resolve-AppDataDir - $localDataDir = Resolve-LocalDataDir - - Write-GatewayLog "Cleaning Windows-side local gateway artifacts. AppData='$dataDir'; LocalData='$localDataDir'." - - Remove-AutostartRegistryValue - Remove-FileIfExists -Path (Join-Path $dataDir 'setup-state.json') -Label 'legacy setup-state.json' - Remove-FileIfExists -Path (Join-Path $localDataDir 'setup-state.json') -Label 'setup-state.json' - Remove-FileIfExists -Path (Join-Path $localDataDir 'run.marker') -Label 'run.marker' - Remove-FileIfExists -Path (Join-Path $dataDir 'exec-policy.json') -Label 'exec-policy.json' - Remove-KeepaliveMarker -LocalDataDir $localDataDir - - $registryCleanup = Remove-SetupManagedGatewayRecords -DataDir $dataDir - if ($registryCleanup.HasExternalGateways) { - Write-GatewayLog 'External gateway records remain; preserving root device tokens.' - } else { - Clear-RootDeviceTokenForRole -DataDir $dataDir -Role 'operator' - Clear-RootDeviceTokenForRole -DataDir $dataDir -Role 'node' - } - - Reset-OnboardingSettings -DataDir $dataDir -PreserveNodeSettings:($registryCleanup.RemainingCount -gt 0) - Remove-DirectoryIfExists -Path (Join-Path $dataDir 'Logs') -Label 'AppData Logs' - Remove-DirectoryIfExists -Path (Join-Path $localDataDir 'Logs') -Label 'LocalAppData Logs' - } - - function Complete-GatewayCleanup { - param([string]$Message) - - Remove-WindowsGatewayArtifacts - Write-GatewayResult ` - -Succeeded $true ` - -ExitCode 0 ` - -Message $Message ` - -Details ([ordered]@{ artifactWarnings = @($script:cleanupWarnings) }) - Write-Host "OpenClaw local WSL gateway removed successfully." - exit 0 -} - -function Get-WslExePath { - $candidates = @( - (Join-Path $env:WINDIR 'Sysnative\wsl.exe'), - (Join-Path $env:WINDIR 'System32\wsl.exe') - ) - - foreach ($candidate in $candidates) { - if (Test-Path -LiteralPath $candidate) { - return $candidate - } - } - - $command = Get-Command wsl.exe -ErrorAction SilentlyContinue - if ($command) { - return $command.Source - } - - return $null -} - -function Format-Arguments { - param([string[]]$Arguments) - - return ($Arguments | ForEach-Object { - if ($_ -match '\s') { - '"' + ($_ -replace '"', '\"') + '"' - } else { - $_ - } - }) -join ' ' -} - -function Invoke-Wsl { - param([string[]]$Arguments) - - $stdoutPath = [System.IO.Path]::GetTempFileName() - $stderrPath = [System.IO.Path]::GetTempFileName() - - try { - $process = Start-Process ` - -FilePath $script:WslPath ` - -ArgumentList $Arguments ` - -WindowStyle Hidden ` - -Wait ` - -PassThru ` - -RedirectStandardOutput $stdoutPath ` - -RedirectStandardError $stderrPath - - $stdout = if (Test-Path -LiteralPath $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw -ErrorAction SilentlyContinue } else { '' } - $stderr = if (Test-Path -LiteralPath $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw -ErrorAction SilentlyContinue } else { '' } - $output = (($stdout, $stderr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine - - Write-GatewayLog ("wsl.exe {0} exited {1}.{2}{3}" -f (Format-Arguments $Arguments), $process.ExitCode, [Environment]::NewLine, $output) - - return [pscustomobject]@{ - ExitCode = [int]$process.ExitCode - Output = $output - } - } finally { - Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue - } -} - -function Test-DistroNotFound { - param([string]$Output) - - if ([string]::IsNullOrWhiteSpace($Output)) { - return $false - } - - return $Output -match 'WSL_E_DISTRO_NOT_FOUND' -or - $Output -match 'There is no distribution with the supplied name' -or - $Output -match 'The specified distribution.*(could not be found|not found)' -or - $Output -match 'distribution.*not.*found' -} - -function Test-DistroListed { - param([string]$Output) - - if ([string]::IsNullOrWhiteSpace($Output)) { - return $false - } - - $distros = ($Output -replace "`0", '') -split '\r?\n' | ForEach-Object { $_.Trim() } - return $distros -contains $DistroName -} - -function Remove-GatewayDirectory { - $gatewayDirectory = Join-Path $AppRoot 'wsl\OpenClawGateway' - - if (-not (Test-Path -LiteralPath $gatewayDirectory)) { - Write-GatewayLog "Gateway directory does not exist: $gatewayDirectory" - return - } - - $gatewayItem = Get-Item -LiteralPath $gatewayDirectory -Force -ErrorAction Stop - if (($gatewayItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { - throw "Refusing to recursively delete reparse point '$gatewayDirectory'." - } - - $lastError = $null - for ($attempt = 1; $attempt -le 6; $attempt++) { - try { - Remove-Item -LiteralPath $gatewayDirectory -Recurse -Force -ErrorAction Stop - if (-not (Test-Path -LiteralPath $gatewayDirectory)) { - Write-GatewayLog "Removed gateway directory: $gatewayDirectory" - return - } - } catch { - $lastError = $_.Exception.Message - Write-GatewayLog "Attempt $attempt failed to remove gateway directory '$gatewayDirectory': $lastError" - } - - Start-Sleep -Seconds 1 - } - - throw "Failed to remove gateway directory '$gatewayDirectory': $lastError" -} - -try { - Ensure-AppRoot - Write-GatewayLog "Starting local gateway cleanup for $DistroName." - - $script:WslPath = Get-WslExePath - if (-not $script:WslPath) { - Write-GatewayLog 'wsl.exe was not found; removing stale gateway directory if present.' - Remove-GatewayDirectory - Complete-GatewayCleanup -Message 'wsl.exe was not found; no registered WSL gateway can be removed.' - } - - $listResult = Invoke-Wsl -Arguments @('--list', '--quiet') - if ($listResult.ExitCode -eq 0 -and -not (Test-DistroListed $listResult.Output)) { - Write-GatewayLog "WSL distro '$DistroName' is not registered; removing stale gateway directory if present." - Remove-GatewayDirectory - Complete-GatewayCleanup -Message "Local WSL gateway '$DistroName' was already unregistered." - } - - $terminateResult = Invoke-Wsl -Arguments @('--terminate', $DistroName) - if ($terminateResult.ExitCode -ne 0) { - Write-GatewayLog "Ignoring terminate exit code $($terminateResult.ExitCode); unregister handles stopped or missing distros." - } - - $shutdownResult = Invoke-Wsl -Arguments @('--shutdown') - if ($shutdownResult.ExitCode -ne 0) { - Write-GatewayLog "Ignoring shutdown exit code $($shutdownResult.ExitCode); unregister will still be attempted." - } - Start-Sleep -Seconds 2 - - $unregisterResult = Invoke-Wsl -Arguments @('--unregister', $DistroName) - if ($unregisterResult.ExitCode -ne 0 -and -not (Test-DistroNotFound $unregisterResult.Output)) { - Write-GatewayResult ` - -Succeeded $false ` - -ExitCode $unregisterResult.ExitCode ` - -Message "Failed to unregister WSL distro '$DistroName'." ` - -Details $unregisterResult.Output - exit $unregisterResult.ExitCode - } - - if ($unregisterResult.ExitCode -ne 0) { - Write-GatewayLog "Treating missing distro '$DistroName' as already removed." - } - - Remove-GatewayDirectory - - Complete-GatewayCleanup -Message "Local WSL gateway '$DistroName' removed." -} catch { - $message = $_.Exception.Message - Write-GatewayLog "Local gateway cleanup failed: $message" - try { "[$(Get-Date -Format 'o')] $message" | Out-File -LiteralPath $errorPath -Encoding UTF8 -Force } catch {} - Write-GatewayResult -Succeeded $false -ExitCode 1 -Message $message - Write-Warning $message - exit 1 -} diff --git a/scripts/build-inno-local.ps1 b/scripts/build-inno-local.ps1 deleted file mode 100644 index c928aae13..000000000 --- a/scripts/build-inno-local.ps1 +++ /dev/null @@ -1,199 +0,0 @@ -<# -.SYNOPSIS - Build local OpenClaw Companion Inno installers for quick validation. - -.DESCRIPTION - Publishes the tray app into a production-style layout, then runs ISCC to - create local unsigned installers. - - Use -NoPublish after changing only installer.iss or docs/tests; it reuses - the existing publish-local-* payloads and only recompiles Inno. - -.EXAMPLE - .\scripts\build-inno-local.ps1 -Arch x64 -Fast - .\scripts\build-inno-local.ps1 -Arch All - .\scripts\build-inno-local.ps1 -Arch x64 -NoPublish -Fast -#> - -[CmdletBinding()] -param( - [ValidateSet("x64", "arm64", "All")] - [string]$Arch = "x64", - - [ValidateSet("Debug", "Release")] - [string]$Configuration = "Release", - - [string]$Version, - - [switch]$NoPublish, - - [switch]$Fast, - - [switch]$InstallInno -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") -Set-Location $repoRoot - -function Write-Step { - param([string]$Message) - Write-Host "`n=== $Message ===" -ForegroundColor Cyan -} - -function Resolve-InnoCompiler { - $candidates = @( - "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe", - "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", - "$env:ProgramFiles\Inno Setup 6\ISCC.exe" - ) - - foreach ($candidate in $candidates) { - if ($candidate -and (Test-Path -LiteralPath $candidate)) { - return (Resolve-Path -LiteralPath $candidate).Path - } - } - - $command = Get-Command ISCC.exe -ErrorAction SilentlyContinue - if ($command) { - return $command.Source - } - - if ($InstallInno) { - Write-Step "Installing Inno Setup with winget" - winget install --id JRSoftware.InnoSetup -e --accept-source-agreements --accept-package-agreements --disable-interactivity - if ($LASTEXITCODE -ne 0) { - throw "winget failed to install Inno Setup." - } - return Resolve-InnoCompiler - } - - throw "Inno Setup compiler (ISCC.exe) was not found. Install it, or rerun with -InstallInno." -} - -function Get-RidForArch { - param([string]$Architecture) - if ($Architecture -eq "arm64") { - return "win-arm64" - } - return "win-x64" -} - -function Publish-ArchitecturePayload { - param( - [string]$Architecture, - [string]$RuntimeIdentifier, - [string]$PublishVersion - ) - - $publishDir = Join-Path $repoRoot "publish-local-$Architecture" - - Write-Step "Publishing $Architecture payload" - Remove-Item -LiteralPath $publishDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Path $publishDir | Out-Null - - $trayPublishArgs = @( - ".\src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj", - "-c", $Configuration, - "-r", $RuntimeIdentifier, - "--self-contained", - "-o", $publishDir, - "-v:minimal", - "-p:Unpackaged=true" - ) - if ($PublishVersion) { - $trayPublishArgs += "-p:Version=$PublishVersion" - } - - dotnet publish @trayPublishArgs - if ($LASTEXITCODE -ne 0) { - throw "Tray publish failed for $Architecture." - } -} - -function Assert-PayloadReady { - param([string]$Architecture) - - $publishDir = Join-Path $repoRoot "publish-local-$Architecture" - $trayExe = Join-Path $publishDir "OpenClaw.Tray.WinUI.exe" - - if (-not (Test-Path -LiteralPath $trayExe)) { - throw "Missing tray payload at $trayExe. Rerun without -NoPublish." - } - - $setupExe = Get-ChildItem -LiteralPath $publishDir -Recurse -File -Filter "OpenClaw.SetupEngine.UI.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($setupExe) { - throw "SetupEngine.UI.exe should not be present in the installer payload: $($setupExe.FullName)" - } - - return $publishDir -} - -function Invoke-InnoCompiler { - param( - [string]$InnoCompiler, - [string]$Architecture, - [string]$PublishDir, - [string]$InstallerVersion - ) - - Write-Step "Compiling $Architecture installer" - - $args = @( - "/DMyAppVersion=$InstallerVersion", - "/DMyAppArch=$Architecture", - "/Dpublish=$PublishDir" - ) - - if ($Fast) { - $args += "/DMyCompression=zip" - $args += "/DMySolidCompression=no" - } - - $args += ".\installer.iss" - - & $InnoCompiler @args - if ($LASTEXITCODE -ne 0) { - throw "ISCC failed for $Architecture." - } -} - -$versionWasProvided = $PSBoundParameters.ContainsKey("Version") - -if (-not $Version) { - $versionScript = Join-Path $PSScriptRoot "Get-OpenClawVersion.ps1" - $Version = & $versionScript -Variable SemVer -} - -if (-not $Version) { - throw "Could not determine a version. Pass -Version explicitly." -} - -$iscc = Resolve-InnoCompiler -$architectures = if ($Arch -eq "All") { @("x64", "arm64") } else { @($Arch) } - -Write-Step "Using ISCC: $iscc" -Write-Host "Version: $Version" -Write-Host "Configuration: $Configuration" -Write-Host "Fast compression: $($Fast.IsPresent)" -Write-Host "No publish: $($NoPublish.IsPresent)" - -foreach ($architecture in $architectures) { - $rid = Get-RidForArch $architecture - if (-not $NoPublish) { - $publishVersion = if ($versionWasProvided) { $Version } else { $null } - Publish-ArchitecturePayload -Architecture $architecture -RuntimeIdentifier $rid -PublishVersion $publishVersion - } - - $payload = Assert-PayloadReady $architecture - Invoke-InnoCompiler -InnoCompiler $iscc -Architecture $architecture -PublishDir $payload -InstallerVersion $Version -} - -Write-Step "Built installers" -Get-ChildItem -Path (Join-Path $repoRoot "Output\OpenClawCompanion-Setup-*.exe") | - Sort-Object Name | - ForEach-Object { - "{0}`t{1:N2} MB`t{2}" -f $_.FullName, ($_.Length / 1MB), $_.LastWriteTime - } diff --git a/scripts/validate-msix-storage-paths.ps1 b/scripts/validate-msix-storage-paths.ps1 index 4a2d62924..1947d4d7c 100644 --- a/scripts/validate-msix-storage-paths.ps1 +++ b/scripts/validate-msix-storage-paths.ps1 @@ -38,8 +38,9 @@ - MUST add in-app pre-uninstall warning banner gated on: PackageHelper.IsPackaged() && File.Exists(setupStatePath) so users are warned before removing the MSIX package. - - The Inno uninstaller script (Uninstall-LocalGateway.ps1) targets real paths - unconditionally — no change needed there. + - Pre-MSIX cleanup paths (now removed: scripts/Uninstall-LocalGateway.ps1 + + installer.iss) targeted real paths unconditionally; an MSIX-flavored + cleanup path will need the same behavior. - Recovery: scripts/validate-wsl-gateway-uninstall.ps1 -Scenario Full -ConfirmDestructiveClean is still relevant for orphaned state. diff --git a/src/OpenClaw.SetupEngine.UI/LogFileLauncher.cs b/src/OpenClaw.SetupEngine.UI/LogFileLauncher.cs index 3713f0f3d..342581e2f 100644 --- a/src/OpenClaw.SetupEngine.UI/LogFileLauncher.cs +++ b/src/OpenClaw.SetupEngine.UI/LogFileLauncher.cs @@ -95,7 +95,7 @@ private static bool TryStrip(string path, string prefix, out string rest) } catch { - // Unpackaged process — no virtualization in play. + // Process is unpackaged or the package identity API is unavailable; no virtualization in play. return null; } } diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 37ddf70a4..edcf3cb6d 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -384,11 +384,6 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args) // two test runs against the same data dir would otherwise pick different // mutex names — and `Math.Abs(int.MinValue)` overflows. Use a stable // SHA-256 prefix instead. - // NOTE: The bare "OpenClawTray" mutex name is also referenced by - // installer.iss `AppMutex=` for install/uninstall race coordination - // (round 2, Scott #5). The suffixed test-isolation variant is - // intentionally not covered by AppMutex — production installs only - // ever use the unsuffixed name. var mutexName = "OpenClawTray"; if (DataDirOverride is not null) { diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 30e8a6e52..00cc4d8de 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -258,6 +258,30 @@ + + + + + + + + + diff --git a/src/OpenClaw.Tray.WinUI/app.manifest b/src/OpenClaw.Tray.WinUI/app.manifest deleted file mode 100644 index 7361ce5c9..000000000 --- a/src/OpenClaw.Tray.WinUI/app.manifest +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - true/pm - PerMonitorV2 - - - - - - - - - - - - - - - - diff --git a/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs b/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs deleted file mode 100644 index 52c0eca18..000000000 --- a/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -namespace OpenClaw.Tray.Tests; - -/// -/// Structural assertions on installer.iss. These pin contracts that cannot -/// be exercised by an in-process unit test because they require ISCC + the -/// resulting unins000.exe to verify end-to-end. -/// -/// Round 2 (Scott #5) — AppMutex coordination prevents the Inno uninstaller -/// from racing the running tray on shared state (settings.json, -/// gateways.json, device-key-ed25519.json, Logs/). The mutex name must -/// match App.xaml.cs's single-instance mutex. -/// -public sealed class InstallerIssAssertionTests -{ - private static string GetRepositoryRoot() - { - var envRepoRoot = Environment.GetEnvironmentVariable("OPENCLAW_REPO_ROOT"); - if (!string.IsNullOrWhiteSpace(envRepoRoot) && Directory.Exists(envRepoRoot)) - return envRepoRoot; - - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory != null) - { - if ((Directory.Exists(Path.Combine(directory.FullName, ".git")) || - File.Exists(Path.Combine(directory.FullName, ".git"))) && - File.Exists(Path.Combine(directory.FullName, "README.md"))) - return directory.FullName; - directory = directory.Parent; - } - - throw new InvalidOperationException( - "Could not find repository root. Set OPENCLAW_REPO_ROOT to the repo path."); - } - - [Fact] - public void Installer_HasAppMutexMatchingTraySingleInstance() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - Assert.Contains("AppMutex=OpenClawTray", iss); - Assert.Contains("Inno requires \"{{\" to emit a literal opening brace in AppId.", iss); - Assert.Contains("AppId={{M0LTB0T-TRAY-4PP1-D3N7}", iss); - Assert.DoesNotContain("AppId={{M0LTB0T-TRAY-4PP1-D3N7}}", iss); - - // The matching tray-side mutex name must be present in App.xaml.cs. - var appXamlCs = File.ReadAllText(Path.Combine( - GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", "App.xaml.cs")); - Assert.Contains("var mutexName = \"OpenClawTray\";", appXamlCs); - } - - [Fact] - public void Installer_DoesNotShipCommandPaletteExtension() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - - Assert.DoesNotContain("cmdpalette", iss, StringComparison.OrdinalIgnoreCase); - Assert.DoesNotContain("CommandPalette", iss, StringComparison.OrdinalIgnoreCase); - Assert.DoesNotContain("Add-AppxPackage", iss, StringComparison.OrdinalIgnoreCase); - Assert.DoesNotContain("Remove-AppxPackage", iss, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void Installer_CreatesStartMenuEntrypointsForTraySetupAndSupport() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - - Assert.Contains(@"#define MyAppName ""OpenClaw Companion""", iss); - Assert.Contains(@"#define MyCompression ""lzma""", iss); - Assert.Contains(@"#define MySolidCompression ""yes""", iss); - Assert.Contains("OutputBaseFilename=OpenClawCompanion-Setup-{#MyAppArch}", iss); - Assert.Contains(@"Name: ""{group}\{#MyAppName}""; Filename: ""{app}\{#MyAppExeName}""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Gateway Setup""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://setup""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Companion Settings""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://commandcenter""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Chat""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://chat""", iss); - Assert.Contains(@"Name: ""{group}\Check for Updates""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://check-updates""", iss); - } - - [Fact] - public void Installer_RemovesGeneratedAppStateOnlyAfterGatewayCleanup() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - - Assert.DoesNotContain("[UninstallRun]", iss); - Assert.Contains("[Code]", iss); - Assert.Contains("Uninstall-LocalGateway.ps1", iss); - Assert.Contains("UninstallSilent()", iss); - Assert.Contains("LocalGatewayCleanupRequested := True", iss); - Assert.Contains("OpenClawGateway WSL distro", iss); - Assert.Contains("MB_YESNO", iss); - Assert.Contains("ExpandConstant('{sys}\\WindowsPowerShell\\v1.0\\powershell.exe')", iss); - Assert.Contains("ewWaitUntilTerminated", iss); - Assert.Contains("MB_RETRYCANCEL", iss); - Assert.Contains("DeleteGeneratedAppState", iss); - Assert.Contains("CurUninstallStep = usPostUninstall", iss); - Assert.Contains("DelTree(ExpandConstant('{app}'), True, True, True)", iss); - Assert.DoesNotContain("Start-Sleep -Seconds 3", iss); - Assert.DoesNotContain("--uninstall --confirm-destructive", iss); - Assert.DoesNotContain("[UninstallDelete]", iss); - } - - [Fact] - public void UninstallLocalGatewayScript_DirectlyUnregistersWslDistro() - { - var script = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "scripts", "Uninstall-LocalGateway.ps1")); - - Assert.Contains("$DistroName = 'OpenClawGateway'", script); - Assert.Contains("'--list', '--quiet'", script); - Assert.Contains("'--terminate', $DistroName", script); - Assert.Contains("'--shutdown'", script); - Assert.Contains("'--unregister', $DistroName", script); - Assert.Contains("Start-Sleep -Seconds 2", script); - Assert.Contains("Remove-GatewayDirectory", script); - Assert.Contains("Remove-WindowsGatewayArtifacts", script); - Assert.Contains("gateways.json", script); - Assert.Contains("device-key-ed25519.json", script); - Assert.Contains("OpenClawTray", script); - Assert.Contains("setup-state.json", script); - Assert.Contains("wsl-keepalive", script); - Assert.Contains("Test-DistroListed", script); - Assert.Contains("Test-DistroNotFound", script); - Assert.Contains("FileAttributes]::ReparsePoint", script); - Assert.Contains("Refusing to recursively delete reparse point", script); - Assert.Contains("for ($attempt = 1; $attempt -le 6; $attempt++)", script); - Assert.Contains("exit $unregisterResult.ExitCode", script); - Assert.DoesNotContain("OpenClaw.Tray.WinUI.exe", script); - Assert.DoesNotContain("OpenClaw.SetupEngine.UI.exe", script); - Assert.DoesNotContain("--headless", script); - Assert.DoesNotContain("--confirm-destructive", script); - } - - [Fact] - public void Installer_RegistersOpenClawProtocol() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - - Assert.Contains(@"Subkey: ""Software\Classes\openclaw""", iss); - Assert.Contains(@"ValueName: ""URL Protocol""", iss); - Assert.Contains(@"Subkey: ""Software\Classes\openclaw\shell\open\command""", iss); - Assert.Contains(@"{app}\{#MyAppExeName}", iss); - Assert.Contains(@"""%1""", iss); - } - - [Fact] - public void ReleaseBuildDoesNotShipSeparateSetupUiExecutable() - { - var iss = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "installer.iss")); - var ci = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "ci.yml")); - - Assert.Contains(@"FileExists(publish + ""\OpenClaw.Tray.WinUI.exe"")", iss); - Assert.Contains(@"FileExists(publish + ""\SetupEngine\OpenClaw.SetupEngine.UI.exe"")", iss); - Assert.Contains("SetupEngine.UI.exe should not be shipped", iss); - Assert.DoesNotContain("Publish SetupEngine.UI", ci); - Assert.DoesNotContain(@"dotnet publish src/OpenClaw.SetupEngine.UI", ci); - Assert.DoesNotContain("publish-setup", ci); - Assert.DoesNotContain(@"mkdir publish\SetupEngine", ci); - Assert.DoesNotContain(@"copy publish-setup\* publish\SetupEngine\ -Recurse", ci); - } - - [Fact] - public void MxcSdk_IsRestoredCopiedValidatedAndIncludedInInstallerPayload() - { - var repositoryRoot = GetRepositoryRoot(); - var packageJson = File.ReadAllText(Path.Combine(repositoryRoot, "package.json")); - var trayProject = File.ReadAllText(Path.Combine( - repositoryRoot, "src", "OpenClaw.Tray.WinUI", "OpenClaw.Tray.WinUI.csproj")); - var iss = File.ReadAllText(Path.Combine(repositoryRoot, "installer.iss")); - - Assert.Contains(@"""@microsoft/mxc-sdk""", packageJson); - Assert.Contains("RestoreMxcNodeBridge", trayProject); - Assert.Contains("npm ci --no-audit --no-fund", trayProject); - Assert.Contains("CopyWxcExecToOutput", trayProject); - Assert.Contains("CopyWxcExecToPublish", trayProject); - Assert.Contains("ValidateWxcExecShipped", trayProject); - Assert.Contains("ValidateWxcExecPublished", trayProject); - Assert.Contains(@"tools\mxc\$(MxcArch)\wxc-exec.exe", trayProject); - - // The Inno payload recurses through the prepared publish directory, so - // publish-time tools\mxc\\wxc-exec.exe is shipped with the app. - Assert.Contains(@"Source: ""{#publish}\*""; DestDir: ""{app}""; Flags: ignoreversion recursesubdirs", iss); - } - - [Fact] - public void MxcRuntime_ProbesShippedWxcExecAndSystemRunUsesIt() - { - var repositoryRoot = GetRepositoryRoot(); - var availability = File.ReadAllText(Path.Combine( - repositoryRoot, "src", "OpenClaw.Shared", "Mxc", "MxcAvailability.cs")); - var nodeService = File.ReadAllText(Path.Combine( - repositoryRoot, "src", "OpenClaw.Tray.WinUI", "Services", "NodeService.cs")); - - Assert.Contains(@"Path.Combine(root, ""tools"", ""mxc"", arch, ""wxc-exec.exe"")", availability); - Assert.Contains("WxcExecOverrideEnvVar", availability); - Assert.Contains("node_modules", availability); - Assert.Contains("@microsoft", availability); - Assert.Contains("mxc-sdk", availability); - - Assert.Contains("private ICommandRunner BuildSystemRunRunner()", nodeService); - Assert.Contains("MxcAvailability.Probe(_logger)", nodeService); - Assert.Contains("new DirectAppContainerExecutor(availability, _logger)", nodeService); - Assert.Contains("return new MxcCommandRunner(", nodeService); - } - -} diff --git a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs b/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs deleted file mode 100644 index 03f15efe0..000000000 --- a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -namespace OpenClaw.Tray.Tests; - -public sealed class ReleaseSigningWorkflowTests -{ - [Fact] - public void ReleaseWorkflow_SignsOnlyOpenClawOwnedPayloadExecutables() - { - var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "ci.yml")); - - Assert.DoesNotContain("azure/trusted-signing-action", workflow); - Assert.DoesNotContain("AZURE_CLIENT_SECRET", workflow); - Assert.Contains("environment: release-signing", workflow); - Assert.Contains("id-token: write", workflow); - Assert.Contains("uses: azure/artifact-signing-action@v2", workflow); - Assert.Contains("endpoint: https://eus.codesigning.azure.net/", workflow); - Assert.Contains("signing-account-name: openclaw", workflow); - Assert.Contains("certificate-profile-name: openclaw", workflow); - Assert.Contains("Stage x64 OpenClaw Executables for Signing", workflow); - Assert.Contains(@"New-Item -ItemType HardLink -Path signing-input-x64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-x64\OpenClaw.Tray.WinUI.exe", workflow); - Assert.DoesNotContain("signing-input-x64\\OpenClaw.SetupEngine.exe", workflow); - Assert.DoesNotContain("signing-input-x64\\OpenClaw.SetupEngine.UI.exe", workflow); - Assert.Contains("Sign x64 OpenClaw Executables", workflow); - Assert.Contains("files-folder: signing-input-x64", workflow); - Assert.Contains("Stage ARM64 OpenClaw Executables for Signing", workflow); - Assert.Contains(@"New-Item -ItemType HardLink -Path signing-input-arm64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-arm64\OpenClaw.Tray.WinUI.exe", workflow); - Assert.DoesNotContain("signing-input-arm64\\OpenClaw.SetupEngine.exe", workflow); - Assert.DoesNotContain("signing-input-arm64\\OpenClaw.SetupEngine.UI.exe", workflow); - Assert.Contains("Sign ARM64 OpenClaw Executables", workflow); - Assert.Contains("files-folder: signing-input-arm64", workflow); - Assert.Contains("files-folder-filter: exe", workflow); - Assert.DoesNotContain("files-folder-recurse: true", workflow); - } - - [Fact] - public void ReleaseWorkflow_VerifiesExecutableSigningPolicy() - { - var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "ci.yml")); - var verifier = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "scripts", "Test-ReleaseExecutableSignatures.ps1")); - - Assert.Contains("Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-x64 -RequireSignedOpenClaw", workflow); - Assert.Contains("Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireSignedOpenClaw", workflow); - Assert.Contains(@"^OpenClaw\.Tray\.WinUI\.exe$", verifier); - Assert.DoesNotContain(@"^SetupEngine\\OpenClaw\.SetupEngine\.exe$", verifier); - Assert.DoesNotContain(@"^SetupEngine\\OpenClaw\.SetupEngine\.UI\.exe$", verifier); - Assert.Contains("SetupEngine\\OpenClaw.SetupEngine.exe should not be present", verifier); - Assert.Contains("SetupEngine\\OpenClaw.SetupEngine.UI.exe should not be present", verifier); - Assert.Contains(@"(^|\\)createdump\.exe$", verifier); - Assert.Contains(@"(^|\\)RestartAgent\.exe$", verifier); - Assert.Contains(@"^tools\\mxc\\[^\\]+\\wxc-exec\.exe$", verifier); - Assert.Contains("Unknown executable in release payload", verifier); - } - - [Fact] - public void ReleaseWorkflow_BundlesAndVerifiesNativeRuntimeDependencies() - { - var root = GetRepositoryRoot(); - var workflow = File.ReadAllText(Path.Combine(root, ".github", "workflows", "ci.yml")); - var installer = File.ReadAllText(Path.Combine(root, "installer.iss")); - var verifier = File.ReadAllText(Path.Combine(root, "scripts", "Test-ReleaseNativeDependencies.ps1")); - var targets = File.ReadAllText(Path.Combine(root, "src", "Directory.Build.targets")); - - Assert.Contains("Test-ReleaseNativeDependencies.ps1 -PayloadPath publish -RequireAppLocalVCRuntime", workflow); - Assert.Contains("Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-x64 -RequireAppLocalVCRuntime", workflow); - Assert.Contains("Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireAppLocalVCRuntime -SkipNativeLoadProbe", workflow); - Assert.Contains("https://aka.ms/vc14/vc_redist.x64.exe", workflow); - Assert.Contains("https://aka.ms/vc14/vc_redist.arm64.exe", workflow); - Assert.Contains("Get-AuthenticodeSignature -LiteralPath $redist.Path", workflow); - Assert.Contains("O=Microsoft Corporation", workflow); - Assert.Contains("-InstallerVCRedistPath vc_redist.x64.exe", workflow); - Assert.Contains("publish-arm64 -RequireAppLocalVCRuntime -RequireInstallerVCRedist -InstallerVCRedistPath vc_redist.arm64.exe -SkipNativeLoadProbe", workflow); - Assert.Contains("/DvcRedist=vc_redist.x64.exe", workflow); - Assert.Contains("/DvcRedist=vc_redist.arm64.exe", workflow); - Assert.DoesNotContain("copy vc_redist.x64.exe publish-x64", workflow); - Assert.DoesNotContain("copy vc_redist.x64.exe publish-arm64", workflow); - Assert.Contains("OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip", workflow); - Assert.Contains("AfterInstall: InstallVCRuntime", installer); - Assert.Contains("Exec(", installer); - Assert.Contains("ResultCode = 3010", installer); - Assert.Contains("ShouldLaunchTray", installer); - Assert.Contains("Skipping post-install tray launch", installer); - Assert.DoesNotContain(@"Filename: ""{tmp}\vc_redist.exe""", installer); - Assert.Contains("Get-AuthenticodeSignature -LiteralPath $File.FullName", verifier); - Assert.Contains("Get-VCRuntimeFiles", verifier); - Assert.Contains("vcruntime140.dll", verifier); - Assert.Contains("libsodium.dll", verifier); - Assert.Contains("OpenClawNativeDependencyProbe", verifier); - Assert.Contains("Microsoft.ML.OnnxRuntime.dll", verifier); - Assert.Contains("onnxruntime.dll", verifier); - Assert.Contains("sherpa-onnx-c-api.dll", verifier); - Assert.Contains("TTS native stack probe", verifier); - Assert.Contains("SkipNativeLoadProbe", verifier); - Assert.Contains("CopyOpenClawVCRuntimeToPublish", targets); - Assert.Contains("ResolveOpenClawVCRuntimeFromVSInstall", targets); - Assert.Contains("ResolveOpenClawVCRuntimeArm64FromVSInstall", targets); - Assert.Contains("VCRuntimeMinVersion", verifier); - } - - [Fact] - public void ReleaseWorkflow_PausesMsixForAlpha() - { - var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "ci.yml")); - - Assert.Contains("if: false # Paused for alpha.4; ship Inno setup and portable ZIP artifacts only.", workflow); - Assert.Contains("needs: [repo-hygiene, test, e2etests, build]", workflow); - Assert.DoesNotContain("Download win-x64 MSIX artifact", workflow); - Assert.DoesNotContain("Download win-arm64 MSIX artifact", workflow); - Assert.DoesNotContain("Sign Release MSIX Packages", workflow); - Assert.DoesNotContain(".msix", ExtractReleaseStep(workflow)); - } - - private static string ExtractReleaseStep(string workflow) - { - var start = workflow.IndexOf(" - name: Create Release", StringComparison.Ordinal); - Assert.True(start >= 0, "Could not find Create Release step."); - return workflow[start..]; - } - - private static string GetRepositoryRoot() - { - var env = Environment.GetEnvironmentVariable("OPENCLAW_REPO_ROOT"); - if (!string.IsNullOrWhiteSpace(env) && Directory.Exists(env)) - return env; - - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory != null) - { - if (File.Exists(Path.Combine(directory.FullName, "openclaw-windows-node.slnx")) && - Directory.Exists(Path.Combine(directory.FullName, "src"))) - { - return directory.FullName; - } - - directory = directory.Parent; - } - - throw new InvalidOperationException("Could not find repository root."); - } -} diff --git a/tests/PackagingTests/Test-InnoUninstallOrdering.ps1 b/tests/PackagingTests/Test-InnoUninstallOrdering.ps1 deleted file mode 100644 index f1385d585..000000000 --- a/tests/PackagingTests/Test-InnoUninstallOrdering.ps1 +++ /dev/null @@ -1,637 +0,0 @@ -<# -.SYNOPSIS - Packaging test — verifies that Inno Setup's [UninstallRun] entry for - Uninstall-LocalGateway.ps1 runs BEFORE {app} directory deletion. - -.DESCRIPTION - WHAT THIS TEST VERIFIES - ----------------------- - RubberDucky finding 8 requires a packaging test that proves the script at - {app}\Uninstall-LocalGateway.ps1 can run (and does run) BEFORE Inno Setup - deletes the {app}\ directory during a silent uninstall. - - HOW IT WORKS - ------------ - 1. [BUILD] Require a pre-built Inno installer (.exe) via -InstallerPath, or - attempt to locate one in the expected build output path. - 2. [INSTALL] Run the installer silently to a temp prefix directory. - 3. [VERIFY] Assert that {app}\OpenClaw.Tray.WinUI.exe and - {app}\Uninstall-LocalGateway.ps1 both exist post-install. - 4. [UNINSTALL] Run unins000.exe /VERYSILENT /LOG=. - 5. [PARSE LOG] Grep the Inno uninstall log for: - a) Evidence that Uninstall-LocalGateway.ps1 was invoked (or - that the [UninstallRun] powershell entry ran). - b) Evidence that {app}\ directory was deleted. - c) Ordering: (a) appears BEFORE (b) in the log (by line number). - 6. [CLEANUP] Remove temp install directory and any WSL residual state created - by the test. (A fresh Inno install with no real gateway means - no WSL distro is ever registered, so WSL cleanup is a no-op.) - 7. [VERDICT] - PASS = files existed post-install AND hook line found before dir- - deletion line in the uninstall log. - FAIL = any of: files missing post-install, hook did not run, - hook ran AFTER directory deletion, or uninstall crashed. - SKIP = no Inno installer available at the expected/given path. - - NOTES ON INNO LOG FORMAT - ------------------------ - When Inno runs with /LOG= it writes a plain-text log with entries like: - Log opened. (YYYY-MM-DD) - ... - -- Run entry #0: Filename: powershell.exe ...Uninstall-LocalGateway.ps1... - ... - Dir: C:\...\OpenClawTray (directory): deleted. - Line ordering is the authoritative source of truth for the ordering check. - -.PARAMETER InstallerPath - Absolute path to the Inno-produced installer EXE. If omitted the test - searches standard build-output locations (publish-x64\installer\, - Output\OpenClawTray-Setup-x64.exe). If still not found the test exits - with SKIP. - -.PARAMETER TempInstallDir - Base directory under which a unique per-run subdirectory is created for - the test installation. Defaults to $env:TEMP\InnoOrderingTest. - The test cleans up this directory after completion (pass or fail). - -.PARAMETER KeepTempDir - When set, do NOT remove the temp install directory after the test. - Use for post-mortem investigation of a FAIL result. - -.PARAMETER OutputDir - Directory to write test artifacts (log, verdict.json, summary.md). - Defaults to .\packaging-test-output\\. - -.EXAMPLE - # Typical run (will locate installer automatically): - .\Test-InnoUninstallOrdering.ps1 - -.EXAMPLE - # Explicit installer path: - .\Test-InnoUninstallOrdering.ps1 -InstallerPath C:\build\OpenClawTray-Setup-x64.exe - -.EXAMPLE - # Keep temp dir for debugging: - .\Test-InnoUninstallOrdering.ps1 -InstallerPath C:\build\OpenClawTray-Setup-x64.exe -KeepTempDir - -.NOTES - Date: 2026-05-07 - Author: Bostick (Tester / Quality / Validation) - Branch: feat/wsl-gateway-uninstall - - Style mirrors validate-wsl-gateway-uninstall.ps1: - - Set-StrictMode -Version Latest - - $ErrorActionPreference = 'Stop' - - Structured step logging - - Stops processes by PID only - - No \\wsl$ or \\wsl.localhost paths - - EXIT CODES - ---------- - 0 PASS Ordering confirmed: hook ran, app dir deleted after. - 1 FAIL Ordering wrong, files missing, hook didn't run, or uninstall crashed. - 2 SKIP No installer available; test cannot run on this machine. - 3 ERROR Unexpected script error. -#> - -[CmdletBinding()] -param( - [string]$InstallerPath = "", - [string]$TempInstallDir = "", - [switch]$KeepTempDir, - [string]$OutputDir = "" -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Exit-code sentinels -# --------------------------------------------------------------------------- -$EXIT_PASS = 0 -$EXIT_FAIL = 1 -$EXIT_SKIP = 2 -$EXIT_ERROR = 3 - -# --------------------------------------------------------------------------- -# Script-level state -# --------------------------------------------------------------------------- -$script:steps = [System.Collections.Generic.List[object]]::new() -$script:verdict = 'UNKNOWN' -$utcStamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd-HHmmssZ") - -if ([string]::IsNullOrEmpty($OutputDir)) { - $OutputDir = Join-Path (Get-Location) "packaging-test-output\$utcStamp" -} -New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null - -# --------------------------------------------------------------------------- -# Logging helpers (mirror validate-wsl-gateway patterns) -# --------------------------------------------------------------------------- -function Add-Step { - param( - [string]$Name, - [string]$Status, # Passed | Failed | Skipped | Warning | Info - [string]$Message, - [hashtable]$Data = @{} - ) - $entry = [ordered]@{ - name = $Name - status = $Status - message = $Message - data = $Data - timestamp = (Get-Date).ToString("o") - } - $script:steps.Add($entry) - - $ts = (Get-Date).ToString("HH:mm:ss") - $color = switch ($Status) { - "Passed" { "Green" } - "Failed" { "Red" } - "Skipped" { "DarkGray" } - "Warning" { "Yellow" } - "Info" { "Cyan" } - default { "White" } - } - Write-Host "[$ts] [$Status] $Name — $Message" -ForegroundColor $color -} - -function Write-Info { - param([string]$Message) - $ts = (Get-Date).ToString("HH:mm:ss") - Write-Host "[$ts] $Message" -ForegroundColor DarkCyan -} - -# --------------------------------------------------------------------------- -# Write verdict JSON + summary MD -# --------------------------------------------------------------------------- -function Write-Results { - param( - [string]$Verdict, - [string]$Notes = "", - [int]$ExitCode = $EXIT_FAIL - ) - - $verdictData = [ordered]@{ - verdict = $Verdict - exit_code = $ExitCode - notes = $Notes - started_at = $script:startedAt - finished_at = (Get-Date).ToString("o") - installer = $script:installerPath - temp_dir = $script:tempInstallPath - output_dir = $OutputDir - steps = @($script:steps) - } - - $verdictPath = Join-Path $OutputDir "verdict.json" - $verdictData | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $verdictPath -Encoding UTF8 - - $summaryPath = Join-Path $OutputDir "summary.md" - $lines = @( - "# Inno Uninstall Ordering Test", - "", - "| Field | Value |", - "|-----------|-------|", - "| Verdict | $Verdict |", - "| ExitCode | $ExitCode |", - "| Installer | $($script:installerPath) |", - "| OutputDir | $OutputDir |", - "| Date | 2026-05-07 |", - "", - "## Notes", "", - $Notes, "", - "## Steps", "" - ) - foreach ($s in $script:steps) { - $lines += "- [$($s.status)] $($s.name): $($s.message)" - } - $lines | Set-Content -LiteralPath $summaryPath -Encoding UTF8 - - $verdictColor = switch ($Verdict) { - "PASS" { "Green" } - "SKIP" { "Cyan" } - "FAIL" { "Red" } - "ERROR" { "Red" } - default { "Yellow" } - } - Write-Host "" - Write-Host "════════════════════════════════════════" -ForegroundColor $verdictColor - Write-Host " VERDICT : $Verdict" -ForegroundColor $verdictColor - Write-Host " ExitCode : $ExitCode" -ForegroundColor $verdictColor - Write-Host " Output : $OutputDir" -ForegroundColor $verdictColor - Write-Host "════════════════════════════════════════" -ForegroundColor $verdictColor - Write-Host "" -} - -# --------------------------------------------------------------------------- -# Installer locator -# --------------------------------------------------------------------------- -function Find-Installer { - # Caller-supplied hint - if (-not [string]::IsNullOrEmpty($InstallerPath) -and (Test-Path -LiteralPath $InstallerPath)) { - return $InstallerPath - } - - # Script is in tests\PackagingTests\ → repo root is 3 levels up - $repoRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent - $candidates = @( - (Join-Path $repoRoot "Output\OpenClawTray-Setup-x64.exe"), - (Join-Path $repoRoot "installer-output\OpenClawTray-Setup-x64.exe"), - (Join-Path $repoRoot "publish-x64\installer\OpenClawTray-Setup-x64.exe") - ) - foreach ($c in $candidates) { - if (Test-Path -LiteralPath $c) { return $c } - } - - # Search Output/ recursively for any matching file - foreach ($searchRoot in @((Join-Path $repoRoot "Output"), (Join-Path $repoRoot "installer-output"))) { - if (Test-Path -LiteralPath $searchRoot) { - $found = Get-ChildItem -LiteralPath $searchRoot -Recurse -Filter "OpenClawTray-Setup*.exe" -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending | Select-Object -First 1 - if ($found) { return $found.FullName } - } - } - - return $null -} - -# --------------------------------------------------------------------------- -# Parse Inno uninstall log for ordering evidence -# --------------------------------------------------------------------------- -function Test-UninstallLogOrdering { - param([string]$LogPath) - - if (-not (Test-Path -LiteralPath $LogPath)) { - return [ordered]@{ - log_found = $false - hook_line_index = -1 - dir_delete_line_index = -1 - hook_ran = $false - dir_deleted = $false - ordering_correct = $false - notes = "Log file not found: $LogPath" - } - } - - $lines = Get-Content -LiteralPath $LogPath -Encoding UTF8 -ErrorAction SilentlyContinue - - $hookIdx = -1 - $dirDelIdx = -1 - - for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i] - - # Hook evidence: Inno logs [UninstallRun] execution in multiple ways: - # "-- Run entry #0:" (start of run-entry block) - # "Executing: powershell.exe" ... "Uninstall-LocalGateway" (run detail) - # "Process exit code: 0" (completion) - # We key on the first mention of the script name in the run section. - if ($hookIdx -eq -1) { - if ($line -match 'Uninstall-LocalGateway' -or - $line -match 'UninstallLocalGateway' -or - ($line -match 'Run entry' -and $line -match 'powershell') -or - ($line -match 'Exec:.*powershell.*Uninstall') -or - ($line -match 'StatusMsg.*Removing local WSL gateway')) { - $hookIdx = $i - } - } - - # Directory deletion evidence: - # "Dir: C:\...\OpenClawTray (directory): deleted." - # "Deleting directory: C:\..." - if ($dirDelIdx -eq -1) { - if (($line -match '(?i)deleting directory') -or - ($line -match '(?i)Dir:.*directory.*delet')) { - # Ensure it's the {app} directory, not a subdirectory cleanup - if ($line -match 'OpenClawTray' -or $line -match [regex]::Escape($script:appDirPattern)) { - $dirDelIdx = $i - } - } - } - } - - $hookRan = ($hookIdx -ge 0) - $dirDeleted = ($dirDelIdx -ge 0) - $orderingOk = $hookRan -and $dirDeleted -and ($hookIdx -lt $dirDelIdx) - - return [ordered]@{ - log_found = $true - log_line_count = $lines.Count - hook_line_index = $hookIdx - hook_line_text = if ($hookIdx -ge 0) { $lines[$hookIdx] } else { "" } - dir_delete_line_index = $dirDelIdx - dir_delete_line_text = if ($dirDelIdx -ge 0) { $lines[$dirDelIdx] } else { "" } - hook_ran = $hookRan - dir_deleted = $dirDeleted - ordering_correct = $orderingOk - notes = if ($orderingOk) { - "hook at line $hookIdx < dir-delete at line $dirDelIdx — ordering CORRECT" - } elseif (-not $hookRan) { - "hook entry not found in log — [UninstallRun] may not have run" - } elseif (-not $dirDeleted) { - "dir-delete entry not found in log — check Inno verbosity" - } else { - "ORDERING WRONG: hook at line $hookIdx >= dir-delete at line $dirDelIdx" - } - } -} - -# --------------------------------------------------------------------------- -# WSL residual cleanup (no-op for a clean install with no gateway) -# --------------------------------------------------------------------------- -function Invoke-WslCleanupCheck { - $wslLines = @() - try { - $raw = & wsl --list --quiet 2>&1 - $wslLines = ($raw | Out-String) -split "`r?`n" | - ForEach-Object { ($_ -replace '\x00', '').Trim() } | - Where-Object { $_ } - } - catch { } - - $openClawDistros = @($wslLines | Where-Object { $_ -like '*OpenClawGateway*' }) - if ($openClawDistros.Count -gt 0) { - Add-Step "wsl-cleanup-check" "Warning" "$($openClawDistros.Count) OpenClawGateway distro(s) still registered after uninstall. Unexpected for a fresh-install test." @{ - distros = $openClawDistros - } - } - else { - Add-Step "wsl-cleanup-check" "Passed" "No OpenClawGateway WSL distros registered (expected for a fresh-install test)." - } -} - -# --------------------------------------------------------------------------- -# MAIN -# --------------------------------------------------------------------------- -$script:startedAt = (Get-Date).ToString("o") -$script:installerPath = "" -$script:tempInstallPath = "" -$script:appDirPattern = "" - -Write-Host "" -Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan -Write-Host "║ Test-InnoUninstallOrdering.ps1 (2026-05-07) ║" -ForegroundColor Cyan -Write-Host "║ Verifies [UninstallRun] hook runs BEFORE {app} deletion ║" -ForegroundColor Cyan -Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan -Write-Host " OutputDir : $OutputDir" -Write-Host "" - -$exitCode = $EXIT_ERROR - -try { - - # ===================================================================== - # STEP 1 — Locate installer - # ===================================================================== - $foundInstaller = Find-Installer - if ([string]::IsNullOrEmpty($foundInstaller)) { - Add-Step "locate-installer" "Skipped" "No Inno installer found. Pass -InstallerPath to specify one explicitly." @{ - searchedPaths = @("Output\OpenClawTray-Setup-x64.exe", - "installer-output\OpenClawTray-Setup-x64.exe", - "publish-x64\installer\OpenClawTray-Setup-x64.exe") - } - Write-Info "SKIP: No installer available on this machine. Build the installer first or use -InstallerPath." - Write-Results -Verdict "SKIP" -ExitCode $EXIT_SKIP ` - -Notes "Installer not found. Build with 'iscc installer.iss' or pass -InstallerPath." - exit $EXIT_SKIP - } - - $script:installerPath = $foundInstaller - Add-Step "locate-installer" "Passed" "Installer found: $foundInstaller" - Write-Info "Installer: $foundInstaller" - - # ===================================================================== - # STEP 2 — Create a temp install prefix - # ===================================================================== - if ([string]::IsNullOrEmpty($TempInstallDir)) { - $TempInstallDir = Join-Path $env:TEMP "InnoOrderingTest" - } - $runId = [System.Guid]::NewGuid().ToString("N").Substring(0, 8) - $tempInstallPath = Join-Path $TempInstallDir "run-$runId" - New-Item -ItemType Directory -Force -Path $tempInstallPath | Out-Null - $script:tempInstallPath = $tempInstallPath - $script:appDirPattern = $tempInstallPath # Inno will install to this dir - - Add-Step "create-temp-dir" "Passed" "Temp install prefix: $tempInstallPath" - Write-Info "Temp install dir: $tempInstallPath" - - # ===================================================================== - # STEP 3 — Silent install - # ===================================================================== - Write-Info "Running silent install..." - $installLog = Join-Path $OutputDir "install.log" - $installArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', - "/DIR=$tempInstallPath", "/LOG=$installLog") - - try { - $proc = Start-Process -FilePath $foundInstaller -ArgumentList $installArgs ` - -Wait -PassThru -WindowStyle Hidden - $installExitCode = $proc.ExitCode - Add-Step "silent-install" "Passed" "Installer exited $installExitCode." @{ - installerPath = $foundInstaller - installDir = $tempInstallPath - logPath = $installLog - exitCode = $installExitCode - } - Write-Info "Install exit code: $installExitCode" - } - catch { - Add-Step "silent-install" "Failed" "Installer threw: $($_.Exception.Message)" - Write-Results -Verdict "FAIL" -ExitCode $EXIT_FAIL ` - -Notes "Silent install threw an exception: $($_.Exception.Message)" - exit $EXIT_FAIL - } - - # ===================================================================== - # STEP 4 — Verify post-install file presence - # ===================================================================== - $exePath = Join-Path $tempInstallPath "OpenClaw.Tray.WinUI.exe" - $hookScriptPath = Join-Path $tempInstallPath "Uninstall-LocalGateway.ps1" - - $exeExists = Test-Path -LiteralPath $exePath - $hookScriptExists = Test-Path -LiteralPath $hookScriptPath - - if (-not $exeExists -or -not $hookScriptExists) { - Add-Step "verify-post-install-files" "Failed" "Expected files missing post-install." @{ - "OpenClaw.Tray.WinUI.exe exists" = $exeExists - "Uninstall-LocalGateway.ps1 exists" = $hookScriptExists - } - Write-Results -Verdict "FAIL" -ExitCode $EXIT_FAIL ` - -Notes "Post-install file check failed. EXE=$exeExists Hook=$hookScriptExists" - exit $EXIT_FAIL - } - - Add-Step "verify-post-install-files" "Passed" "Both required files exist post-install." @{ - exePath = $exePath - hookScriptPath = $hookScriptPath - } - - # ===================================================================== - # STEP 5 — Locate unins000.exe - # ===================================================================== - $uninsExe = Join-Path $tempInstallPath "unins000.exe" - if (-not (Test-Path -LiteralPath $uninsExe)) { - # Inno may produce unins001.exe etc. if a previous install left a unins000. - $uninsExe = Get-ChildItem -LiteralPath $tempInstallPath -Filter "unins*.exe" ` - -ErrorAction SilentlyContinue | Sort-Object Name | Select-Object -First 1 | - ForEach-Object { $_.FullName } - if ([string]::IsNullOrEmpty($uninsExe)) { - Add-Step "locate-uninstaller" "Failed" "unins000.exe not found in $tempInstallPath" - Write-Results -Verdict "FAIL" -ExitCode $EXIT_FAIL -Notes "Inno uninstaller not found." - exit $EXIT_FAIL - } - } - Add-Step "locate-uninstaller" "Passed" "Uninstaller: $uninsExe" - - # ===================================================================== - # STEP 6 — Silent uninstall with log capture - # ===================================================================== - $uninstallLog = Join-Path $OutputDir "uninstall.log" - $uninstallArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', - "/LOG=$uninstallLog") - - Write-Info "Running silent uninstall..." - try { - $proc2 = Start-Process -FilePath $uninsExe -ArgumentList $uninstallArgs ` - -Wait -PassThru -WindowStyle Hidden - $uninstallExitCode = $proc2.ExitCode - Add-Step "silent-uninstall" "Passed" "Uninstaller exited $uninstallExitCode." @{ - uninsExe = $uninsExe - logPath = $uninstallLog - exitCode = $uninstallExitCode - } - Write-Info "Uninstall exit code: $uninstallExitCode" - } - catch { - Add-Step "silent-uninstall" "Failed" "Uninstaller threw: $($_.Exception.Message)" - Write-Results -Verdict "FAIL" -ExitCode $EXIT_FAIL ` - -Notes "Silent uninstall threw: $($_.Exception.Message)" - exit $EXIT_FAIL - } - - # ===================================================================== - # STEP 7 — Parse log: verify hook ran AND ordering is correct - # ===================================================================== - Write-Info "Parsing uninstall log for ordering evidence..." - $ordering = Test-UninstallLogOrdering -LogPath $uninstallLog - - # Save ordering analysis - $orderingPath = Join-Path $OutputDir "ordering-analysis.json" - $ordering | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $orderingPath -Encoding UTF8 - - $orderingStatus = if ($ordering.ordering_correct) { "Passed" } ` - elseif (-not $ordering.log_found) { "Warning" } ` - else { "Failed" } - - Add-Step "log-ordering-check" $orderingStatus $ordering.notes @{ - log_path = $uninstallLog - hook_line = $ordering.hook_line_index - hook_line_text = $ordering.hook_line_text - dir_delete_line = $ordering.dir_delete_line_index - dir_delete_line_text = $ordering.dir_delete_line_text - ordering_correct = $ordering.ordering_correct - analysis_file = $orderingPath - } - - # ===================================================================== - # STEP 7b — Supplemental: verify {app} dir was deleted after uninstall - # ===================================================================== - $appDirGone = -not (Test-Path -LiteralPath $tempInstallPath) - if ($appDirGone) { - Add-Step "verify-app-dir-deleted" "Passed" "{app} directory removed by uninstaller (expected)." - } - else { - Add-Step "verify-app-dir-deleted" "Warning" "{app} directory still exists after uninstall: $tempInstallPath" - } - - # ===================================================================== - # STEP 8 — WSL cleanup check - # ===================================================================== - Invoke-WslCleanupCheck - - # ===================================================================== - # STEP 9 — Final verdict - # ===================================================================== - - # Determine if the log ordering check was conclusive. - # If the log wasn't found or the hook was not in it, try a weaker check: - # look for evidence in the uninstall log that Uninstall-LocalGateway.ps1 was - # at least attempted (it may exit 0 quietly without verbose log entries). - $hookConfirmed = $ordering.hook_ran - - if (-not $hookConfirmed -and $ordering.log_found) { - # Secondary check: scan raw log for the script name anywhere - $rawLog = Get-Content -LiteralPath $uninstallLog -Raw -Encoding UTF8 -ErrorAction SilentlyContinue - if ($rawLog -match 'Uninstall-LocalGateway') { - Add-Step "secondary-hook-check" "Passed" "Secondary scan found 'Uninstall-LocalGateway' in uninstall log." - $hookConfirmed = $true - } - else { - Add-Step "secondary-hook-check" "Warning" "'Uninstall-LocalGateway' not found anywhere in uninstall log. The [UninstallRun] entry may not have been executed." - } - } - elseif (-not $ordering.log_found) { - Add-Step "secondary-hook-check" "Warning" "Cannot perform secondary check: uninstall log not found." - } - - # Determine pass/fail/skip - if (-not $ordering.log_found) { - # No log = can't confirm ordering; FAIL with guidance - $finalVerdict = "FAIL" - $notes = "Uninstall log not produced. Ensure Inno's /LOG= switch works for this installer version." - $exitCode = $EXIT_FAIL - } - elseif ($ordering.ordering_correct) { - $finalVerdict = "PASS" - $notes = $ordering.notes - $exitCode = $EXIT_PASS - } - elseif (-not $hookConfirmed) { - $finalVerdict = "FAIL" - $notes = "Hook not confirmed in log. [UninstallRun] entry may be missing or not triggered." - $exitCode = $EXIT_FAIL - } - else { - $finalVerdict = "FAIL" - $notes = $ordering.notes - $exitCode = $EXIT_FAIL - } - - $script:verdict = $finalVerdict - Write-Results -Verdict $finalVerdict -ExitCode $exitCode -Notes $notes - -} -catch { - $errMsg = $_.Exception.Message - Add-Step "unhandled-error" "Failed" $errMsg - Write-Host "ERROR: $errMsg" -ForegroundColor Red - Write-Results -Verdict "ERROR" -ExitCode $EXIT_ERROR -Notes $errMsg - $exitCode = $EXIT_ERROR -} -finally { - # Cleanup temp install directory unless -KeepTempDir or it was already removed by uninstall - if (-not $KeepTempDir -and -not [string]::IsNullOrEmpty($script:tempInstallPath)) { - if (Test-Path -LiteralPath $script:tempInstallPath) { - try { - Remove-Item -LiteralPath $script:tempInstallPath -Recurse -Force -ErrorAction SilentlyContinue - Write-Info "Temp install dir removed: $($script:tempInstallPath)" - } - catch { - Write-Info "Warning: could not remove temp dir: $($script:tempInstallPath) — $($_.Exception.Message)" - } - } - } - - # Also remove the parent temp base dir if it was auto-created and is now empty - if (-not $KeepTempDir -and -not [string]::IsNullOrEmpty($TempInstallDir)) { - if (Test-Path -LiteralPath $TempInstallDir) { - $remaining = @(Get-ChildItem -LiteralPath $TempInstallDir -ErrorAction SilentlyContinue) - if ($remaining.Count -eq 0) { - Remove-Item -LiteralPath $TempInstallDir -Force -ErrorAction SilentlyContinue - } - } - } -} - -exit $exitCode From c47575d2c2d868a0b5021825804b94b457a343aa Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 5 Jun 2026 16:23:19 -0700 Subject: [PATCH 05/38] Phase 3B: Remove Updatum (GitHub-Release-ZIP in-app updater) Updatum becomes dead under MSIX-primary publishing once in the .appinstaller XML (Phase 4) is the update mechanism. No in-app updater under MSIX, no manual "Check for updates" button. Removed: - Updatum NuGet PackageReference from OpenClaw.Tray.WinUI.csproj - App.xaml.cs: using Updatum, AppUpdater static field, BuildInitialUpdateInfo call, startup update-check gate, IAppCommands.CheckForUpdates impl, dispatcher case "checkupdates", HandleDeepLink wiring, and the entire #region Updates block (~460 lines). - IAppCommands.CheckForUpdates declaration. - DeepLinkHandler.cs: case "updates"/"update"/"check-updates"/"update-check" block + CheckForUpdates Func field. - AboutPage.xaml CheckUpdatesButton + AboutPage.xaml.cs OnCheckUpdatesClick. - HubWindow.xaml.cs orphan CheckForUpdatesAction property. - SettingsData.SkippedUpdateTag + 4 SettingsManager references. - Dialogs/UpdateDialog.cs (deleted file). - Dialogs/DownloadProgressDialog.cs (deleted file). - Update*, CheckUpdates*, DownloadProgress* resource keys from all 5 locale resw files (en-us, zh-tw, zh-cn, nl-nl, fr-fr; 18 keys per file). - ci.yml: Updatum auto-update ZIP comment + x64/arm64 "Create Release ZIP" steps + ZIP entries from release files: + Portable bullets from release body. Release will have no binary artifacts until Phase 4 MSIX pipeline. - Test fixtures: SettingsRoundTrip (4 SkippedUpdateTag refs), DeepLinkParser (2 InlineData rows + fixture init), TrayMenuWindowMarkup (2 Assert.Contains), AppRefactorContract (CheckForUpdatesAsync from AssertInOrder), LocalizationValidation (WindowTitle_Update + Update_OK invariants). - Docs: README.md + docs/SETUP.md (openclaw://check-updates row); docs/RELEASING.md (Portable ZIP Updatum block); docs/VERSIONING.md (Updatum Library reference). Kept: - UpdateCommandCenterInfo DTO in OpenClaw.Shared/Models.cs - public protocol type sent to external agent clients; default-initialized (Status="Unknown", CurrentVersion=null). AppStateSnapshot.LastUpdateInfo wiring stays. Validation: - ./build.ps1 green - Shared.Tests: 2049 passed, 29 skipped (unchanged) - Tray.Tests: 943 passed (was 945; -2 = DeepLinkParserTests InlineData rows) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 20 - README.md | 1 - docs/RELEASING.md | 11 +- docs/SETUP.md | 1 - docs/VERSIONING.md | 1 - src/OpenClaw.Shared/SettingsData.cs | 1 - src/OpenClaw.Tray.WinUI/App.xaml.cs | 75 +-- .../Dialogs/DownloadProgressDialog.cs | 43 -- .../Dialogs/UpdateDialog.cs | 122 ----- .../OpenClaw.Tray.WinUI.csproj | 1 - src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml | 2 - .../Pages/AboutPage.xaml.cs | 5 - .../Services/DeepLinkHandler.cs | 11 - .../Services/IAppCommands.cs | 1 - .../Services/SettingsManager.cs | 4 - .../Services/UpdateCoordinator.cs | 467 ------------------ .../Strings/en-us/Resources.resw | 78 +-- .../Strings/fr-fr/Resources.resw | 78 +-- .../Strings/nl-nl/Resources.resw | 78 +-- .../Strings/zh-cn/Resources.resw | 78 +-- .../Strings/zh-tw/Resources.resw | 78 +-- .../Windows/HubWindow.xaml.cs | 1 - .../AppRefactorContractTests.cs | 1 - .../DeepLinkParserTests.cs | 7 - .../LocalizationValidationTests.cs | 2 - .../SettingsRoundTripTests.cs | 4 - .../TrayMenuWindowMarkupTests.cs | 2 - 27 files changed, 68 insertions(+), 1105 deletions(-) delete mode 100644 src/OpenClaw.Tray.WinUI/Dialogs/DownloadProgressDialog.cs delete mode 100644 src/OpenClaw.Tray.WinUI/Dialogs/UpdateDialog.cs delete mode 100644 src/OpenClaw.Tray.WinUI/Services/UpdateCoordinator.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8209d0f2..160bf7473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -614,35 +614,15 @@ jobs: # checks still run. run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireAppLocalVCRuntime -SkipNativeLoadProbe - # Create ZIP files for Updatum auto-update (asset name must contain the RID). - # We ship both x64 and arm64 portables now that the build job produces a - # libsodium-compatible app-local VC runtime for both architectures (the - # arm64 leg sources its loose Microsoft.VC*.CRT DLLs from the VS install - # on the windows-11-arm runner; see src/Directory.Build.targets). - - name: Create x64 Release ZIP - run: | - Compress-Archive -Path artifacts/tray-win-x64/* -DestinationPath OpenClawTray-${{ needs.test.outputs.semVer }}-win-x64.zip - - - name: Create arm64 Release ZIP - run: | - Compress-Archive -Path artifacts/tray-win-arm64/* -DestinationPath OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip - - name: Create Release uses: softprops/action-gh-release@v3 with: generate_release_notes: true - files: | - OpenClawTray-${{ needs.test.outputs.semVer }}-win-x64.zip - OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip prerelease: ${{ contains(github.ref_name, '-') }} make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }} body: | ## OpenClaw Windows Hub ${{ github.ref_name }} - ### Downloads - - **Portable x64**: `OpenClawTray-${{ needs.test.outputs.semVer }}-win-x64.zip` - - **Portable ARM64**: `OpenClawTray-${{ needs.test.outputs.semVer }}-win-arm64.zip` - > MSIX installer artifacts arrive in a follow-up release once the > MSIX-primary distribution pipeline lands. diff --git a/README.md b/README.md index f8eb739a9..9ee63d02a 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,6 @@ OpenClaw registers the `openclaw://` URL scheme for automation and integration: | `openclaw://dashboard/skills` | Open Skills dashboard page | | `openclaw://dashboard/cron` | Open Cron dashboard page | | `openclaw://healthcheck` | Run a manual health check | -| `openclaw://check-updates` | Run a manual update check | | `openclaw://logs` | Open the current tray log file | | `openclaw://log-folder` | Open the logs folder | | `openclaw://config` | Open the config folder | diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 182dc6a3d..1ed6f2110 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -73,14 +73,9 @@ git tag -a vX.Y.Z-alpha.N -m "OpenClaw Windows Hub vX.Y.Z-alpha.N" git push origin vX.Y.Z-alpha.N ``` -For the current alpha flow, ship only: - -- Portable ZIP payloads for Updatum: - - `OpenClawTray--win-x64.zip` - - `OpenClawTray--win-arm64.zip` - -MSIX artifacts will become the primary distribution surface in a follow-up -phase; see the MSIX-primary publishing plan on the `user/kmahone/msix` branch. +For the current alpha flow, no binary artifacts are attached to the GitHub +release (the MSIX-primary distribution pipeline lands in a follow-up phase on +the `user/kmahone/msix` branch). The tag itself is the published artifact. ## Executable signing policy diff --git a/docs/SETUP.md b/docs/SETUP.md index df163b7eb..aeabbdb6f 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -108,7 +108,6 @@ OpenClaw Companion responds to `openclaw://` deep links, which can be invoked fr | `openclaw://activity` | Open the Activity page | | `openclaw://history` | Open the Activity page filtered to notification history | | `openclaw://healthcheck` | Run a manual health check | -| `openclaw://check-updates` | Run a manual update check | | `openclaw://logs` | Open the current tray log file | | `openclaw://log-folder` | Open the logs folder | | `openclaw://config` | Open the config folder | diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 2d8ba29cc..997d63687 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -92,5 +92,4 @@ For example: ## References - [Microsoft Docs: Assembly Versioning](https://learn.microsoft.com/en-us/dotnet/standard/assembly/versioning) -- [Updatum Library](https://github.com/sn4k3/Updatum) - [GitVersion Documentation](https://gitversion.net/) diff --git a/src/OpenClaw.Shared/SettingsData.cs b/src/OpenClaw.Shared/SettingsData.cs index 7fb8ac72b..ea26a61af 100644 --- a/src/OpenClaw.Shared/SettingsData.cs +++ b/src/OpenClaw.Shared/SettingsData.cs @@ -94,7 +94,6 @@ public record class SettingsData public bool? McpOnlyMode { get; set; } public string? PreferredGatewayId { get; set; } public bool HasSeenActivityStreamTip { get; set; } = false; - public string? SkippedUpdateTag { get; set; } public bool NotifyChatResponses { get; set; } = true; public bool PreferStructuredCategories { get; set; } = true; /// diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index edcf3cb6d..630796190 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -25,7 +25,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Updatum; using WinUIEx; using SetupCompletedEventArgs = OpenClaw.SetupEngine.UI.SetupCompletedEventArgs; using SetupWindow = OpenClaw.SetupEngine.UI.SetupWindow; @@ -34,12 +33,6 @@ namespace OpenClawTray; public partial class App : Application, OpenClawTray.Services.IAppCommands { - internal static readonly UpdatumManager AppUpdater = new("openclaw", "openclaw-windows-node") - { - FetchOnlyLatestRelease = true, - InstallUpdateSingleFileExecutableName = "OpenClaw.Tray.WinUI", - }; - private TrayIcon? _trayIcon; private GatewayConnectionManager? _connectionManager; private GatewayRegistry? _gatewayRegistry; @@ -102,8 +95,7 @@ public void EnsureSshTunnelStarted() _settings.SshTunnelHost, _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort, - includeBrowserProxyForward, - _settings.SshTunnelSshPort); + includeBrowserProxyForward); } /// @@ -138,7 +130,6 @@ public IntPtr GetHubWindowHandle() private Microsoft.UI.Dispatching.DispatcherQueue? _dispatcherQueue; private AppState? _appState; internal AppState? AppState => _appState; - private UpdateCoordinator? _updateCoordinator; private GatewayService? _gatewayService; private CancellationTokenSource? _deepLinkCts; private bool _isExiting; @@ -226,12 +217,6 @@ public App() Logger.Warn($"[App] Ignoring invalid OPENCLAW_LANGUAGE value: {langOverride}"); } - // Wire the GatewayHostAccess localization indirection to LocalizationHelper. - // The classifier defaults to identity (returns the resource key as-is) for unit-test - // contexts that lack a WinUI runtime; in-app we point it at the real resource lookup. - GatewayHostAccessLocalization.GetString = LocalizationHelper.GetString; - GatewayHostAccessLocalization.Format = (key, args) => LocalizationHelper.Format(key, args); - InitializeComponent(); s_runMarker.Check(); @@ -274,14 +259,12 @@ private static void WaitForRestartSourceIfRequested(string[] args) if (!process.HasExited) process.WaitForExit(TimeSpan.FromSeconds(60)); } - // slopwatch-ignore: SW003 Cleanup is best-effort; failure cannot improve caller state and the original outcome is preserved. catch (ArgumentException) { // The source process already exited. } catch (Exception ex) { - // slopwatch-ignore: SW003 Diagnostic logging fallback is best-effort and logging failure must not cascade. try { Logger.Warn($"Post-setup restart wait for PID {pid} failed: {ex.Message}"); } catch { } } } @@ -440,20 +423,6 @@ _dispatcherQueue is null // Central observable model + gateway event handler. _appState = new AppState(_dispatcherQueue); - _updateCoordinator = new UpdateCoordinator( - AppUpdater, - _appState, - _settings, - () => - { - XamlRoot? r = null; - if (_hubWindow != null && !_hubWindow.IsClosed) - r = (_hubWindow.Content as FrameworkElement)?.XamlRoot; - return r ?? (_keepAliveWindow?.Content as FrameworkElement)?.XamlRoot; - }, - refreshStatus: UpdateStatusDetailWindow, - exit: Exit); - _appState.UpdateInfo = UpdateCoordinator.BuildInitialInfo(); _gatewayService = new GatewayService(_appState, _dispatcherQueue!); _gatewayService.ConnectionStatusChanged += OnGatewayConnectionStatusChanged; _gatewayService.AuthenticationFailed += OnGatewayAuthenticationFailed; @@ -473,29 +442,15 @@ _dispatcherQueue is null // Register URI scheme on first run DeepLinkHandler.RegisterUriScheme(); - // Anchor the WinUI runtime so transient windows (UpdateDialog, - // setup wizard, etc.) don't terminate the process when closed. + // Anchor the WinUI runtime so transient windows (setup wizard, + // dialogs, etc.) don't terminate the process when closed. // WinUI 3 Desktop's default DispatcherShutdownMode is - // OnLastWindowClose — without this override, closing the - // UpdateDialog on the startup path (when it is the only window) - // would shut down the WinUI runtime mid-flight and kill the - // in-progress download/extraction. We still control shutdown + // OnLastWindowClose — without this override, closing a transient + // dialog on the startup path (when it is the only window) would + // shut down the WinUI runtime mid-flight. We control shutdown // explicitly via Application.Exit(). DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown; - // Check for updates before launching. Skip in test instances — no UI dialogs, - // no network calls, no startup delay. - if (DataDirOverride is null && - Environment.GetEnvironmentVariable("OPENCLAW_SKIP_UPDATE_CHECK") != "1") - { - var shouldLaunch = await _updateCoordinator.CheckForUpdatesAsync(); - if (!shouldLaunch) - { - Exit(); - return; - } - } - // Register toast activation handler ToastNotificationManagerCompat.OnActivated += OnToastActivated; @@ -514,7 +469,7 @@ _dispatcherQueue is null ShowSurfaceImprovementsTipIfNeeded(); // Initialize connection manager before setup flow. - _gatewayRegistry = new GatewayRegistry(SettingsManager.SettingsDirectoryPath, logger: new AppLogger()); + _gatewayRegistry = new GatewayRegistry(SettingsManager.SettingsDirectoryPath); _gatewayRegistry.Load(); var credentialResolver = new CredentialResolver(DeviceIdentityFileReader.Instance); var clientFactory = new GatewayClientFactory(); @@ -626,8 +581,7 @@ _dispatcherQueue is null // hosts. Fire-and-forget on a background task so a slow LxssManager at // cold logon never delays InitializeGatewayClient. The keepalive itself // runs detached from the tray — see WslDistroKeepAlive in LocalGatewaySetup.cs. - var wslKeepAlive = new WslGatewayKeepAliveService(() => _settings, () => _gatewayRegistry); - _ = Task.Run(wslKeepAlive.TryEnsureAsync); + _ = Task.Run(TryEnsureLocalGatewayKeepAliveAsync); InitializeGatewayClient(); // Pre-warm chat window (WebView2 init takes 1-3s, do it now so left-click is instant) @@ -936,7 +890,6 @@ private void OnTrayMenuItemClicked(object? sender, string action) case "history": ShowHub("channels"); break; case "activity": ShowHub("channels"); break; case "healthcheck": _ = RunHealthCheckAsync(userInitiated: true); break; - case "checkupdates": _ = _updateCoordinator!.CheckForUpdatesUserInitiatedAsync(); break; case "settings": ShowSettings(); break; case "setup": _ = ShowOnboardingAsync(); break; case "autostart": ToggleAutoStart(); break; @@ -1422,8 +1375,7 @@ private void InitializeGatewayClient(bool useBootstrapHandoffAuth = false) _settings.SshTunnelLocalPort, _settings.NodeBrowserProxyEnabled && SshTunnelCommandLine.CanForwardBrowserProxyPort( - _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort), - _settings.SshTunnelSshPort) + _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort)) : null, }; _gatewayRegistry.AddOrUpdate(record); @@ -1610,7 +1562,6 @@ private void TryMigrateLegacyGatewaySettings(string gatewayUrl, IOpenClawLogger _settings.UseSshTunnel, _settings.SshTunnelUser, _settings.SshTunnelHost, - _settings.SshTunnelSshPort, _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort, SettingsManager.SettingsDirectoryPath, @@ -3331,7 +3282,6 @@ void IAppCommands.Disconnect() } void IAppCommands.ShowVoiceOverlay() => ShowHub("voice"); void IAppCommands.ShowChat() => ShowChatWindow(); - void IAppCommands.CheckForUpdates() => _ = _updateCoordinator!.CheckForUpdatesUserInitiatedAsync(); void IAppCommands.ShowOnboarding() => _ = ShowOnboardingAsync(); void IAppCommands.ShowConnectionStatus() => ShowConnectionStatusWindow(); void IAppCommands.NotifySettingsSaved() => OnSettingsSaved(this, EventArgs.Empty); @@ -3614,7 +3564,6 @@ private void HandleDeepLink(string uri) OpenSettings = ShowSettings, OpenSetup = () => _ = ShowOnboardingAsync(), RunHealthCheck = () => RunHealthCheckAsync(userInitiated: true), - CheckForUpdates = _updateCoordinator!.CheckForUpdatesUserInitiatedAsync, OpenLogFile = OpenLogFile, OpenLogFolder = OpenLogFolder, OpenConfigFolder = OpenConfigFolder, @@ -3893,8 +3842,7 @@ _settings.SshTunnelRemotePort is < 1 or > 65535 || _settings.SshTunnelHost, _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort, - includeBrowserProxy, - _settings.SshTunnelSshPort); + includeBrowserProxy); DiagnosticsJsonlService.Write("tunnel.ensure_started", new { status = _sshTunnelService.Status.ToString(), @@ -3951,8 +3899,7 @@ private async Task OnSshTunnelExitedAsync(int exitCode) _settings.SshTunnelHost, _settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort, - restartBrowserProxy, - _settings.SshTunnelSshPort); + restartBrowserProxy); Logger.Info("SSH tunnel restarted successfully"); DiagnosticsJsonlService.Write("tunnel.restart_succeeded", new { diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/DownloadProgressDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/DownloadProgressDialog.cs deleted file mode 100644 index c66b2e466..000000000 --- a/src/OpenClaw.Tray.WinUI/Dialogs/DownloadProgressDialog.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; -using OpenClawTray.Helpers; -using Updatum; - -namespace OpenClawTray.Dialogs; - -public sealed class DownloadProgressDialog -{ - private Window? _window; - private readonly UpdatumManager? _updater; - - public DownloadProgressDialog(UpdatumManager updater) - { - _updater = updater; - } - - public void ShowAsync() - { - _window = new Window { Title = LocalizationHelper.GetString("WindowTitle_Downloading") }; - _window.SystemBackdrop = new MicaBackdrop(); - - var panel = new StackPanel { Padding = new Thickness(20) }; - var progressText = new TextBlock { Text = LocalizationHelper.GetString("Download_ProgressText"), Margin = new Thickness(0, 0, 0, 10) }; - var progressBar = new ProgressBar { IsIndeterminate = true }; - - panel.Children.Add(progressText); - panel.Children.Add(progressBar); - _window.Content = panel; - - // Size and center the window - _window.AppWindow.Resize(new global::Windows.Graphics.SizeInt32(400, 200)); - var displayArea = Microsoft.UI.Windowing.DisplayArea.Primary; - var centerX = (displayArea.WorkArea.Width - 400) / 2; - var centerY = (displayArea.WorkArea.Height - 200) / 2; - _window.AppWindow.Move(new global::Windows.Graphics.PointInt32(centerX, centerY)); - - _window.Activate(); - } - - public void Close() => _window?.Close(); -} diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/UpdateDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/UpdateDialog.cs deleted file mode 100644 index fb48e9f76..000000000 --- a/src/OpenClaw.Tray.WinUI/Dialogs/UpdateDialog.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; -using OpenClaw.Shared; -using OpenClawTray.Helpers; -using OpenClawTray.Services; -using System; -using System.Threading.Tasks; -using WinUIEx; - -namespace OpenClawTray.Dialogs; - -public enum UpdateDialogResult -{ - Download, - Skip, - RemindLater -} - -/// -/// Dialog showing available update with release notes. -/// Built directly in a WindowEx (no ContentDialog/XamlRoot issues). -/// -public sealed class UpdateDialog : WindowEx -{ - private readonly TaskCompletionSource _tcs = new(); - private UpdateDialogResult _result = UpdateDialogResult.RemindLater; - - public UpdateDialog(string version, string changelog) - { - Title = LocalizationHelper.GetString("WindowTitle_Update"); - this.SetWindowSize(560, 420); - this.CenterOnScreen(); - this.SetIcon("Assets\\openclaw.ico"); - SystemBackdrop = new MicaBackdrop(); - - var root = new Grid - { - Padding = new Thickness(32), - RowSpacing = 16 - }; - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - - // Header - var header = new TextBlock - { - Text = string.Format(LocalizationHelper.GetString("Update_VersionAvailable"), version), - Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"] - }; - Grid.SetRow(header, 0); - root.Children.Add(header); - - // Content - var content = new StackPanel { Spacing = 12 }; - - var currentVersion = AppVersionInfo.Version; - content.Children.Add(new TextBlock - { - Text = string.Format(LocalizationHelper.GetString("Update_CurrentVersion"), currentVersion), - Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"] - }); - - content.Children.Add(new TextBlock - { - Text = LocalizationHelper.GetString("Update_WhatsNew"), - FontWeight = Microsoft.UI.Text.FontWeights.SemiBold - }); - - content.Children.Add(new ScrollViewer - { - MaxHeight = 200, - Content = new TextBlock - { - Text = changelog, - TextWrapping = TextWrapping.Wrap - } - }); - - Grid.SetRow(content, 1); - root.Children.Add(content); - - // Buttons - var buttonPanel = new StackPanel - { - Orientation = Orientation.Horizontal, - HorizontalAlignment = HorizontalAlignment.Right, - Spacing = 8 - }; - - var skipButton = new Button { Content = LocalizationHelper.GetString("Update_SkipButton") }; - skipButton.Click += (s, e) => { Logger.Info("[Update] User clicked 'Skip'"); _result = UpdateDialogResult.Skip; Close(); }; - buttonPanel.Children.Add(skipButton); - - var laterButton = new Button { Content = LocalizationHelper.GetString("Update_RemindLaterButton") }; - laterButton.Click += (s, e) => { Logger.Info("[Update] User clicked 'Remind Later'"); _result = UpdateDialogResult.RemindLater; Close(); }; - buttonPanel.Children.Add(laterButton); - - var downloadButton = new Button - { - Content = LocalizationHelper.GetString("Update_DownloadButton"), - Style = (Style)Application.Current.Resources["AccentButtonStyle"] - }; - downloadButton.Click += (s, e) => { Logger.Info("[Update] User clicked 'Download'"); _result = UpdateDialogResult.Download; Close(); }; - buttonPanel.Children.Add(downloadButton); - - Grid.SetRow(buttonPanel, 2); - root.Children.Add(buttonPanel); - - Content = root; - Closed += (s, e) => _tcs.TrySetResult(_result); - - Logger.Info($"[Update] Update dialog shown for version {version}"); - } - - public Task ShowAsync() - { - Activate(); - return _tcs.Task; - } -} diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 00cc4d8de..162a9dbc3 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -86,7 +86,6 @@ - diff --git a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml index 58ac065d6..aab4cb02c 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/AboutPage.xaml @@ -79,8 +79,6 @@ Click="OnOpenConfigClick"/> private static readonly HashSet LatinScriptInvariantResourceKeys = new(StringComparer.Ordinal) { - "Update_OK", "Onboarding_IncompleteSetup_Close", "ChatPage_OK", "ConnectionPage_ViaSSH", diff --git a/tests/OpenClaw.Tray.Tests/SettingsRoundTripTests.cs b/tests/OpenClaw.Tray.Tests/SettingsRoundTripTests.cs index 7eb049397..0ccc5e998 100644 --- a/tests/OpenClaw.Tray.Tests/SettingsRoundTripTests.cs +++ b/tests/OpenClaw.Tray.Tests/SettingsRoundTripTests.cs @@ -53,7 +53,6 @@ public void RoundTrip_AllFields_Preserved() HubNavPaneOpen = false, TtsPiperVoiceId = "fr_FR-siwis-low", HasSeenActivityStreamTip = true, - SkippedUpdateTag = "v1.2.3", NotifyChatResponses = false, PreferStructuredCategories = true, UserRules = new List @@ -108,7 +107,6 @@ public void RoundTrip_AllFields_Preserved() Assert.Equal(original.HubNavPaneOpen, restored.HubNavPaneOpen); Assert.Equal(original.TtsPiperVoiceId, restored.TtsPiperVoiceId); Assert.Equal(original.HasSeenActivityStreamTip, restored.HasSeenActivityStreamTip); - Assert.Equal(original.SkippedUpdateTag, restored.SkippedUpdateTag); Assert.Equal(original.NotifyChatResponses, restored.NotifyChatResponses); Assert.Equal(original.PreferStructuredCategories, restored.PreferStructuredCategories); Assert.NotNull(restored.UserRules); @@ -173,7 +171,6 @@ public void MissingFields_UseDefaults() Assert.Null(settings.TtsElevenLabsModel); Assert.Null(settings.TtsElevenLabsVoiceId); Assert.False(settings.HasSeenActivityStreamTip); - Assert.Null(settings.SkippedUpdateTag); Assert.True(settings.NotifyChatResponses); Assert.True(settings.PreferStructuredCategories); // HubNavPaneOpen defaults to true (NavView starts expanded for new @@ -245,7 +242,6 @@ public void BackwardCompatibility_OldSettingsWithoutNewFields() Assert.Null(settings.TtsElevenLabsModel); Assert.Null(settings.TtsElevenLabsVoiceId); Assert.False(settings.HasSeenActivityStreamTip); - Assert.Null(settings.SkippedUpdateTag); Assert.True(settings.GlobalHotkeyEnabled); // HubNavPaneOpen wasn't in this older JSON shape; default true. Assert.True(settings.HubNavPaneOpen); diff --git a/tests/OpenClaw.Tray.Tests/TrayMenuWindowMarkupTests.cs b/tests/OpenClaw.Tray.Tests/TrayMenuWindowMarkupTests.cs index bfc5d85a1..ec237bc70 100644 --- a/tests/OpenClaw.Tray.Tests/TrayMenuWindowMarkupTests.cs +++ b/tests/OpenClaw.Tray.Tests/TrayMenuWindowMarkupTests.cs @@ -138,8 +138,6 @@ public void DeepLinkHandler_HasTrayUtilityEntryPoints() Assert.Contains(@"case ""healthcheck"":", source); Assert.Contains("RunHealthCheck", source); - Assert.Contains(@"case ""check-updates"":", source); - Assert.Contains("CheckForUpdates", source); Assert.Contains(@"case ""logs"":", source); Assert.Contains("OpenLogFile?.Invoke", source); } From c385ddf09695bc37372c6ebefd52209f1925bad3 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 5 Jun 2026 16:58:44 -0700 Subject: [PATCH 06/38] Fix: register toast COM activator in Package.appxmanifest Symptom (caught while smoke-testing the MSIX-primary build): asking the agent to show a Windows notification throws InvalidOperationException: Your app manifest must have a toastNotificationActivation extension with a valid ToastActivatorCLSID specified. at ToastNotificationManagerCompat.CreateToastNotifier() at ToastContentBuilder.Show() at ToastService.ShowToast(...) Root cause: Microsoft.Toolkit.Uwp.Notifications (which we still use for all toasts via App.OnToastActivated + interactive AddButton flows) has two code paths -- an unpackaged shortcut/COM-self-registration path and a packaged path that reads ToastActivatorCLSID from the appx manifest. Phase 2 of the MSIX-primary branch dropped -p:Unpackaged=true, so we now always hit the packaged path. With no manifest extension declared, the toolkit cannot find a CLSID and throws on first ShowToast call. Fix: declare a stable CLSID for the toast activator in the manifest: - Add desktop:Extension Category="windows.toastNotificationActivation" with ToastActivatorCLSID="EF9297B3-EEEB-4E50-8306-D1D118E04BC7". - Add the matching com:Extension/com:ComServer/com:ExeServer entry so the COM server is wired to OpenClaw.Tray.WinUI.exe with the conventional -ToastActivated arg. - Declare the desktop + com namespaces and include them in IgnorableNamespaces. The CommunityToolkit generates the actual activator type at runtime and binds it to this CLSID; no extra C# is needed. App.ToastActivation.cs already wires ToastNotificationManagerCompat.OnActivated to OnToastActivated and parses arguments via ToastArguments.Parse, so the button-click roundtrip works as soon as the CLSID is reachable. Validation: - ./build.ps1 green (manifest passes MakeAppx schema check). - Tray.Tests still pass (no source changes; manifest-only edit). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Tray.WinUI/Package.appxmanifest | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/Package.appxmanifest b/src/OpenClaw.Tray.WinUI/Package.appxmanifest index 6c3b5d32a..2cc8ea90a 100644 --- a/src/OpenClaw.Tray.WinUI/Package.appxmanifest +++ b/src/OpenClaw.Tray.WinUI/Package.appxmanifest @@ -3,8 +3,10 @@ + IgnorableNamespaces="uap desktop com rescap"> + + + + + + + + + + + From bf8c2477517ddf89dc379114f899e84fa39e9b5d Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 5 Jun 2026 17:04:24 -0700 Subject: [PATCH 07/38] build.ps1: hard-fail -PackageMsix when dev signing cert is missing Previously the precheck only warned and the post-build summary suggested `Add-AppxPackage -AllowUnsigned`. That command does not work for this MSIX package on stock Windows (AllowUnsigned only applies to a narrow set of developer-mode scenarios), so users following the suggestion would fail at install time. Now: missing cert => Write-Error + exit 1 in the preflight, with a clear message pointing at scripts\setup-dev-msix-cert.ps1. The post-build install hint always shows the signed Add-AppxPackage form (the unsigned branch is now unreachable). Docstring updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build.ps1 | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/build.ps1 b/build.ps1 index a8254c1a6..d1eeeddc5 100644 --- a/build.ps1 +++ b/build.ps1 @@ -25,9 +25,11 @@ In addition to the always-packaged loose-layout build, produce a .msix package file in src/OpenClaw.Tray.WinUI/AppPackages/. Requires the OpenClaw.Tray.WinUI project to be in the build set (Project=All, Tray, or - WinUI). If %LOCALAPPDATA%\OpenClawTray\dev-msix.pfx exists the .msix is - signed with that cert (run scripts\setup-dev-msix-cert.ps1 once to create - it); otherwise the .msix is unsigned. + WinUI). Requires %LOCALAPPDATA%\OpenClawTray\dev-msix.pfx (run + scripts\setup-dev-msix-cert.ps1 once to create it); the build hard-fails + if the cert is missing because MSIX packages cannot be installed unsigned + on stock Windows (Add-AppxPackage -AllowUnsigned only works under very + specific developer-mode conditions that do not cover this package). .EXAMPLE .\build.ps1 @@ -386,7 +388,9 @@ if ($Project -ne "Shared" -and $Project -ne "All" -and $toBuild -notcontains "Sh $toBuild = @("Shared") + $toBuild } -# -PackageMsix preflight: must include WinUI/Tray and (warn only) check PFX. +# -PackageMsix preflight: must include WinUI/Tray and dev signing cert must +# exist. Unsigned MSIX packages cannot be installed on stock Windows, so a +# missing cert is a hard fail rather than a warning. if ($PackageMsix) { $winUITargetIncluded = ($toBuild -contains "WinUI") -or ($toBuild -contains "Tray") if (-not $winUITargetIncluded) { @@ -398,10 +402,15 @@ if ($PackageMsix) { if (Test-Path $devPfx) { Write-Success "Dev MSIX signing cert found: $devPfx" } else { - Write-Warning "Dev MSIX signing cert not found at $devPfx" - Write-Info "The .msix will be unsigned and must be installed with: Add-AppxPackage -AllowUnsigned -Path " - Write-Info "To produce a signed .msix instead, run (elevated):" - Write-Info " .\scripts\setup-dev-msix-cert.ps1" + Write-Error "Dev MSIX signing cert not found at $devPfx" + Write-Host "" + Write-Host "An unsigned .msix cannot be installed on stock Windows" -ForegroundColor Yellow + Write-Host "(Add-AppxPackage -AllowUnsigned only works under narrow" -ForegroundColor Yellow + Write-Host "developer-mode conditions that do not cover this package)." -ForegroundColor Yellow + Write-Host "" + Write-Host "To create the dev signing cert, run (elevated):" -ForegroundColor Cyan + Write-Host " .\scripts\setup-dev-msix-cert.ps1" -ForegroundColor White + exit 1 } } @@ -463,12 +472,7 @@ if ($failCount -eq 0) { Write-Host "`nMSIX:" -ForegroundColor Cyan if ($producedMsix) { Write-Host " Path: $($producedMsix.FullName)" -ForegroundColor White - $devPfx = Join-Path $env:LOCALAPPDATA "OpenClawTray\dev-msix.pfx" - if (Test-Path $devPfx) { - Write-Host " Install: Add-AppxPackage -Path `"$($producedMsix.FullName)`"" -ForegroundColor White - } else { - Write-Host " Install: Add-AppxPackage -AllowUnsigned -Path `"$($producedMsix.FullName)`" (elevated)" -ForegroundColor White - } + Write-Host " Install: Add-AppxPackage -Path `"$($producedMsix.FullName)`"" -ForegroundColor White } else { Write-Warning "Could not locate produced .msix under $appPackagesDir" } From c04a7f668f8cf71e1edf4ac38c40f6afcffdf86b Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 09:33:49 -0700 Subject: [PATCH 08/38] cleanup --- build.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.ps1 b/build.ps1 index d1eeeddc5..7e218f161 100644 --- a/build.ps1 +++ b/build.ps1 @@ -404,10 +404,6 @@ if ($PackageMsix) { } else { Write-Error "Dev MSIX signing cert not found at $devPfx" Write-Host "" - Write-Host "An unsigned .msix cannot be installed on stock Windows" -ForegroundColor Yellow - Write-Host "(Add-AppxPackage -AllowUnsigned only works under narrow" -ForegroundColor Yellow - Write-Host "developer-mode conditions that do not cover this package)." -ForegroundColor Yellow - Write-Host "" Write-Host "To create the dev signing cert, run (elevated):" -ForegroundColor Cyan Write-Host " .\scripts\setup-dev-msix-cert.ps1" -ForegroundColor White exit 1 From 8b4bf80c3938972f30f5e74ef53f0f96d0f16b08 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 10:49:31 -0700 Subject: [PATCH 09/38] fix notifications in appxmanifest --- src/OpenClaw.Tray.WinUI/Package.appxmanifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/Package.appxmanifest b/src/OpenClaw.Tray.WinUI/Package.appxmanifest index 2cc8ea90a..de2197b55 100644 --- a/src/OpenClaw.Tray.WinUI/Package.appxmanifest +++ b/src/OpenClaw.Tray.WinUI/Package.appxmanifest @@ -72,7 +72,7 @@ - + From 83d161335873f32e03d0798f1874409722949ce6 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 11:01:25 -0700 Subject: [PATCH 10/38] Phase 4: AppInstaller stable feed + render/validate scripts + feed PR workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts the publishing-infrastructure pattern from PR #468 (treated as a reference design only, not cherry-picked) and adapts it to our WindowsAppSDKSelfContained=true MSIX. Because the WindowsAppRuntime is bundled inside the .msix, the AppInstaller feed has no block, no runtime-URI rendering, and no separate runtime MSIX release asset. What this commit adds - installer/openclaw-companion.appinstaller.template — 6 placeholders (VERSION, PUBLISHER, IDENTITY_NAME, PROCESSOR_ARCHITECTURE, MSIX_URI, APPINSTALLER_URI), AutomaticBackgroundTask-only UpdateSettings. - installer/appinstaller/openclaw-{x64,arm64}.appinstaller — bootstrap feed files at version 0.0.0.0; the appinstaller-feed-pr workflow rewrites these on each stable release tag. - installer/appinstaller/README.md — explains the stable-feed model. - scripts/render-appinstaller.ps1 — substitutes placeholders, asserts the rendered XML parses and contains no block. - scripts/validate-appinstaller-hosting.ps1 — Content-Type / Content- Length / Range checks against the hosted .appinstaller and .msix URLs, with -AllowGitHubContentTypes for raw.githubusercontent.com. - scripts/test-appinstaller-update.ps1 — local HttpListener-backed vN -> vN+1 upgrade smoke using PackageManager .AddPackageByAppInstallerFileAsync. - .github/workflows/appinstaller-feed-pr.yml — workflow_dispatch input takes a release tag, renders the two feed files, validates them, and opens a PR to advance the stable feed. Uses OpenClaw Foundation publisher; rejects pre-release tags; here-string PR body uses Set-Content -Value so the body renders as Markdown (fixes the 8-space-indent code-block bug in PR #468's workflow). - tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs — 8 test methods covering template shape, the new Template_HasNoDependenciesBlock invariant, the two bootstrap feed files, the validation script, the smoke script, and the feed-update workflow. - README.md — replaces the Phase-7 TODO placeholder with x64 and ARM64 Install links pointing at the raw GitHub .appinstaller URLs. What this commit does NOT add - No in-app "Check for updates" button or AppInstallerUpdateService. Windows AppInstaller's AutomaticBackgroundTask handles all polling at OS level under MSIX. - No Microsoft.WindowsAppRuntime.2 release asset or feed dependency — the runtime is bundled (WindowsAppSDKSelfContained=true). Validation - ./build.ps1: green. - Shared.Tests: 2049 passed / 29 skipped (matches baseline). - Tray.Tests: 957 passed (was 943; +14 effective test cases from the new file). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/appinstaller-feed-pr.yml | 166 +++++++++++++ README.md | 20 +- installer/appinstaller/README.md | 44 ++++ .../appinstaller/openclaw-arm64.appinstaller | 17 ++ .../appinstaller/openclaw-x64.appinstaller | 17 ++ .../openclaw-companion.appinstaller.template | 48 ++++ scripts/render-appinstaller.ps1 | 176 ++++++++++++++ scripts/test-appinstaller-update.ps1 | 175 ++++++++++++++ scripts/validate-appinstaller-hosting.ps1 | 221 ++++++++++++++++++ .../AppInstallerTemplateAssertionTests.cs | 200 ++++++++++++++++ 10 files changed, 1076 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/appinstaller-feed-pr.yml create mode 100644 installer/appinstaller/README.md create mode 100644 installer/appinstaller/openclaw-arm64.appinstaller create mode 100644 installer/appinstaller/openclaw-x64.appinstaller create mode 100644 installer/openclaw-companion.appinstaller.template create mode 100644 scripts/render-appinstaller.ps1 create mode 100644 scripts/test-appinstaller-update.ps1 create mode 100644 scripts/validate-appinstaller-hosting.ps1 create mode 100644 tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs diff --git a/.github/workflows/appinstaller-feed-pr.yml b/.github/workflows/appinstaller-feed-pr.yml new file mode 100644 index 000000000..af7169052 --- /dev/null +++ b/.github/workflows/appinstaller-feed-pr.yml @@ -0,0 +1,166 @@ +name: AppInstaller Feed PR + +on: + workflow_dispatch: + inputs: + tag: + description: Release tag whose signed MSIX assets should advance the stable AppInstaller feed + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + update-feed: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v6 + with: + ref: master + fetch-depth: 0 + + - name: Render stable AppInstaller feed files + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $ErrorActionPreference = 'Stop' + + $repo = '${{ github.repository }}' + $tag = '${{ inputs.tag }}'.Trim() + if ([string]::IsNullOrWhiteSpace($tag)) { + throw "tag input is required." + } + if (-not $tag.StartsWith('v', [StringComparison]::OrdinalIgnoreCase)) { + throw "Release tag must start with 'v'. Got '$tag'." + } + if ($tag.Contains('-')) { + throw "Pre-release AppInstaller feed updates are blocked until alpha channel policy is decided. Tag: $tag" + } + + $release = gh release view $tag --repo $repo --json tagName,isPrerelease,assets | ConvertFrom-Json + if ($release.isPrerelease) { + throw "Pre-release AppInstaller feed updates are blocked until alpha channel policy is decided. Tag: $tag" + } + + $versionText = $tag.Substring(1) + if ($versionText -notmatch '^\d+\.\d+\.\d+$') { + throw "Stable release tag must be vX.Y.Z so the MSIX/AppInstaller version can be rendered as X.Y.Z.0. Got '$tag'." + } + $version = "$versionText.0" + $publisher = 'CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US' + $identityName = 'OpenClaw.Companion' + $feedDir = 'installer\appinstaller' + New-Item -ItemType Directory -Force -Path $feedDir | Out-Null + + function Get-RequiredAsset { + param([Parameter(Mandatory)] [string] $Pattern) + $matches = @($release.assets | Where-Object { $_.name -like $Pattern }) + if ($matches.Count -ne 1) { + $available = ($release.assets | ForEach-Object { $_.name }) -join ', ' + throw "Expected exactly one release asset matching '$Pattern' for $tag; found $($matches.Count). Assets: $available" + } + return $matches[0] + } + + function Get-ReleaseAssetUri { + param([Parameter(Mandatory)] [string] $AssetName) + $escapedName = [Uri]::EscapeDataString($AssetName) + return "https://github.com/$repo/releases/download/$tag/$escapedName" + } + + $x64Asset = Get-RequiredAsset -Pattern "OpenClawCompanion-$versionText-win-x64.msix" + $arm64Asset = Get-RequiredAsset -Pattern "OpenClawCompanion-$versionText-win-arm64.msix" + $x64Uri = Get-ReleaseAssetUri -AssetName $x64Asset.name + $arm64Uri = Get-ReleaseAssetUri -AssetName $arm64Asset.name + + $rawBase = "https://raw.githubusercontent.com/$repo/master/installer/appinstaller" + $x64FeedPath = Join-Path $feedDir 'openclaw-x64.appinstaller' + $arm64FeedPath = Join-Path $feedDir 'openclaw-arm64.appinstaller' + + .\scripts\render-appinstaller.ps1 ` + -Version $version ` + -Publisher $publisher ` + -IdentityName $identityName ` + -ProcessorArchitecture x64 ` + -MsixUri $x64Uri ` + -AppInstallerUri "$rawBase/openclaw-x64.appinstaller" ` + -OutputPath $x64FeedPath + + .\scripts\render-appinstaller.ps1 ` + -Version $version ` + -Publisher $publisher ` + -IdentityName $identityName ` + -ProcessorArchitecture arm64 ` + -MsixUri $arm64Uri ` + -AppInstallerUri "$rawBase/openclaw-arm64.appinstaller" ` + -OutputPath $arm64FeedPath + + .\scripts\validate-appinstaller-hosting.ps1 ` + -AppInstallerPath $x64FeedPath ` + -MsixUri $x64Uri ` + -AllowGitHubContentTypes + .\scripts\validate-appinstaller-hosting.ps1 ` + -AppInstallerPath $arm64FeedPath ` + -MsixUri $arm64Uri ` + -AllowGitHubContentTypes + + $branch = "automation/appinstaller-feed-$($tag -replace '[^A-Za-z0-9._-]', '-')" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git checkout -B $branch + git add $feedDir + + $changes = git status --short -- $feedDir + if ([string]::IsNullOrWhiteSpace($changes)) { + Write-Host "Stable AppInstaller feed already points at $tag; no PR needed." + exit 0 + } + + git commit -m "chore(msix): update AppInstaller feed for $tag" ` + -m "Advance the stable Windows AppInstaller feed to the signed MSIX assets from $tag." ` + -m "Merging this PR advances the auto-update source for installed MSIX clients." + git push --force-with-lease origin $branch + + $bodyPath = Join-Path $env:RUNNER_TEMP 'appinstaller-feed-pr.md' + $body = @" +Updates the stable Windows AppInstaller feed files for ``$tag``. + +Merging this PR advances installed MSIX clients that poll the stable feed: + +- x64 feed: ``$rawBase/openclaw-x64.appinstaller`` +- ARM64 feed: ``$rawBase/openclaw-arm64.appinstaller`` +- x64 MSIX: ``$x64Uri`` +- ARM64 MSIX: ``$arm64Uri`` + +Validation performed: + +- Rendered both feed files from ``scripts/render-appinstaller.ps1`` +- Parsed local AppInstaller XML before publishing +- Validated GitHub release MSIX headers with candidate GitHub content-type compatibility enabled +- Blocked pre-release/alpha feed updates until channel policy is decided + +Note: the MSIX is built with ``WindowsAppSDKSelfContained=true``, so the feed +intentionally omits any ```` block — Windows installs the runtime +bundled inside the .msix and never fetches a separate framework package. +"@ + Set-Content -Path $bodyPath -Value $body -Encoding UTF8 + + $existingPr = gh pr list --repo $repo --base master --head $branch --json number --jq '.[0].number' + if ([string]::IsNullOrWhiteSpace($existingPr)) { + gh pr create ` + --repo $repo ` + --base master ` + --head $branch ` + --title "chore(msix): update AppInstaller feed for $tag" ` + --body-file $bodyPath + } + else { + gh pr edit $existingPr ` + --repo $repo ` + --title "chore(msix): update AppInstaller feed for $tag" ` + --body-file $bodyPath + } diff --git a/README.md b/README.md index 9ee63d02a..6525b49cb 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,18 @@ This monorepo contains the Windows hub, shared client libraries, and CLI utiliti > > **Managed WSL gateway?** Local setup creates a locked-down app-owned `OpenClawGateway` distro. See [docs/WSL_GATEWAY_ADMIN.md](docs/WSL_GATEWAY_ADMIN.md) for editing `openclaw.json` as the `openclaw` user and using root for protected-file administration. -Direct downloads from the latest OpenClaw release: - - -- MSIX installer downloads coming soon — see [docs/SETUP.md](docs/SETUP.md). +Install via Windows AppInstaller (auto-updates from the stable feed): + +- **Install (x64)** — [openclaw-x64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller) +- **Install (ARM64)** — [openclaw-arm64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-arm64.appinstaller) + +Click the link for your machine architecture; Windows opens the App Installer +UI, prompts for consent, then installs the signed MSIX. Future updates are +delivered automatically by Windows via the same feed URL — no in-app "Check +for updates" button needed. + +> See [docs/SETUP.md](docs/SETUP.md) for step-by-step guidance and what to do +> if the install link opens as plain text in your browser. ### Prerequisites - Windows 10 (20H2+) or Windows 11 diff --git a/installer/appinstaller/README.md b/installer/appinstaller/README.md new file mode 100644 index 000000000..42433fb94 --- /dev/null +++ b/installer/appinstaller/README.md @@ -0,0 +1,44 @@ +# Windows AppInstaller stable feed + +This directory is the source-controlled stable update feed for the OpenClaw +Companion MSIX channel. + +Installed MSIX packages poll these architecture-specific raw GitHub URLs: + +- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller` +- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-arm64.appinstaller` + +The checked-in feed files are bootstrap placeholders at version `0.0.0.0` so +the raw URLs exist before the first signed MSIX embeds them. End users never +install these placeholders directly — Windows AppInstaller checks them in the +background after the user installs from a real release. + +## Release flow + +Release builds do **not** push these files directly to `master`. After a +successful stable release tag: + +1. `.github/workflows/appinstaller-feed-pr.yml` is triggered (manually via + workflow_dispatch with the release tag). +2. The workflow renders the per-architecture feed files from the matching + signed `.msix` release assets via `scripts/render-appinstaller.ps1`. +3. The rendered files are validated via `scripts/validate-appinstaller-hosting.ps1` + against the hosted GitHub release assets. +4. A pull request is opened against `master` with the regenerated XML. +5. Merging the PR is the human gate that advances installed clients to the + new version. + +Git history is the audit trail for which release each feed file points at. + +## Pre-release / alpha channel + +Alpha/pre-release feed updates are blocked until maintainers choose a channel +strategy. Do not hand-edit the stable feed files to point at alpha packages — +auto-updating all stable users to a pre-release build is a one-way trip. + +## Self-contained WindowsAppSDK + +OpenClaw Companion is built with `WindowsAppSDKSelfContained=true`, so the +WindowsAppRuntime is packaged inside each `.msix`. The feed files therefore +emit no `` block — Windows does not need to download a separate +framework package at install time. diff --git a/installer/appinstaller/openclaw-arm64.appinstaller b/installer/appinstaller/openclaw-arm64.appinstaller new file mode 100644 index 000000000..982eb0717 --- /dev/null +++ b/installer/appinstaller/openclaw-arm64.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/installer/appinstaller/openclaw-x64.appinstaller b/installer/appinstaller/openclaw-x64.appinstaller new file mode 100644 index 000000000..6612bcb65 --- /dev/null +++ b/installer/appinstaller/openclaw-x64.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/installer/openclaw-companion.appinstaller.template b/installer/openclaw-companion.appinstaller.template new file mode 100644 index 000000000..315239f12 --- /dev/null +++ b/installer/openclaw-companion.appinstaller.template @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/scripts/render-appinstaller.ps1 b/scripts/render-appinstaller.ps1 new file mode 100644 index 000000000..8f5c42df3 --- /dev/null +++ b/scripts/render-appinstaller.ps1 @@ -0,0 +1,176 @@ +<# +.SYNOPSIS + Renders installer/openclaw-companion.appinstaller.template into a release-ready + AppInstaller XML by substituting the {{TOKEN}} placeholders. + +.DESCRIPTION + Used by .github/workflows/appinstaller-feed-pr.yml after a stable release tag + to regenerate installer/appinstaller/openclaw-{x64,arm64}.appinstaller from + the signed MSIX release assets. Also runnable locally to validate template + renders before tagging a release. + + The rendered AppInstaller XML must validate against the AppInstaller schema + (http://schemas.microsoft.com/appx/appinstaller/2018). We assert via XML + load rather than schema validation because the schema isn't shipped with the + Windows SDK on most runners. + + The OpenClaw Companion MSIX is built with WindowsAppSDKSelfContained=true, + so the rendered AppInstaller intentionally has NO block. + Windows does not need to fetch a separate WindowsAppRuntime package. + +.PARAMETER Version + 4-part version string (e.g. "0.5.3.0"). Must match the MSIX . + Windows AppInstaller's update detector compares versions as 4-part values, so + a 1-3 part value produces "no update available" forever even though it parses. + +.PARAMETER Publisher + Publisher subject from the MSIX manifest. Must match the signing cert + Subject DN exactly. Example: + "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US" + +.PARAMETER ProcessorArchitecture + MSIX processor architecture for this AppInstaller file. Must be x64 or arm64. + +.PARAMETER IdentityName + MSIX package identity for the MainPackage element. Stable releases use + OpenClaw.Companion. + +.PARAMETER MsixUri + Absolute https:// URL of the matching architecture .msix release asset. + +.PARAMETER AppInstallerUri + Absolute https:// URL of THIS rendered .appinstaller file on the stable + channel (e.g. + https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller). + Embedded inside the AppInstaller so Windows AppInstaller knows where to poll + for future updates. + +.PARAMETER OutputPath + Destination path for the rendered .appinstaller file. + +.PARAMETER AllowHttpForLocalTest + Allows http:// loopback URIs for local AppInstaller smoke tests. Production + release rendering must omit this switch and use https:// URLs. + +.EXAMPLE + ./scripts/render-appinstaller.ps1 ` + -Version 0.5.3.0 ` + -Publisher 'CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US' ` + -IdentityName OpenClaw.Companion ` + -ProcessorArchitecture x64 ` + -MsixUri https://github.com/openclaw/openclaw-windows-node/releases/download/v0.5.3/OpenClawCompanion-0.5.3-win-x64.msix ` + -AppInstallerUri https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller ` + -OutputPath installer/appinstaller/openclaw-x64.appinstaller +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string] $Version, + [Parameter(Mandatory)] [string] $Publisher, + [string] $IdentityName = 'OpenClaw.Companion', + [Parameter(Mandatory)] [ValidateSet('x64', 'arm64')] [string] $ProcessorArchitecture, + [Parameter(Mandatory)] [string] $MsixUri, + [Parameter(Mandatory)] [string] $AppInstallerUri, + [Parameter(Mandatory)] [string] $OutputPath, + [switch] $AllowHttpForLocalTest +) + +$ErrorActionPreference = 'Stop' + +$parts = $Version.Split('.') +if ($parts.Length -ne 4) { + throw "Version must be 4-part (X.Y.Z.W). Got: '$Version'" +} +foreach ($p in $parts) { + $parsed = 0 + if (-not [int]::TryParse($p, [ref]$parsed)) { + throw "Version segment '$p' is not an integer." + } +} + +if ([string]::IsNullOrWhiteSpace($IdentityName)) { + throw "IdentityName must not be empty." +} + +foreach ($pair in @( + @{ Name = 'MsixUri'; Value = $MsixUri }, + @{ Name = 'AppInstallerUri'; Value = $AppInstallerUri } + )) { + $u = $null + if (-not [Uri]::TryCreate($pair.Value, 'Absolute', [ref]$u)) { + throw "$($pair.Name) must be an absolute URL. Got: '$($pair.Value)'" + } + + $isAllowedHttpLoopback = $AllowHttpForLocalTest -and $u.Scheme -eq 'http' -and $u.IsLoopback + if ($u.Scheme -ne 'https' -and -not $isAllowedHttpLoopback) { + throw "$($pair.Name) must be an absolute https:// URL. Got: '$($pair.Value)'" + } +} + +$repoRoot = Split-Path -Parent $PSScriptRoot +$templatePath = Join-Path $repoRoot 'installer\openclaw-companion.appinstaller.template' +if (-not (Test-Path $templatePath)) { + throw "Template not found: $templatePath" +} + +$template = Get-Content $templatePath -Raw + +# Simple string substitution — inputs are not regex patterns and we don't want +# regex-metacharacter surprises from values like a publisher subject with +# literal commas/quotes or a URI with percent-encoded characters. +$rendered = $template +$rendered = $rendered.Replace('{{VERSION}}', $Version) +$rendered = $rendered.Replace('{{PUBLISHER}}', $Publisher) +$rendered = $rendered.Replace('{{IDENTITY_NAME}}', $IdentityName) +$rendered = $rendered.Replace('{{PROCESSOR_ARCHITECTURE}}', $ProcessorArchitecture) +$rendered = $rendered.Replace('{{MSIX_URI}}', $MsixUri) +$rendered = $rendered.Replace('{{APPINSTALLER_URI}}', $AppInstallerUri) +if ($rendered -match '\{\{[A-Z0-9_]+\}\}') { + throw "Rendered XML still contains unresolved template token(s): $($Matches[0])" +} + +# Validate the rendered XML parses. A bad template / bad substitution surfaces +# here instead of at deploy time when Windows refuses to install. +[xml]$xml = $rendered +if ($xml.AppInstaller.Version -ne $Version) { + throw "Rendered XML has Version '$($xml.AppInstaller.Version)' but expected '$Version'. Substitution failure." +} +$mainPackage = $xml.AppInstaller.MainPackage +if ($null -eq $mainPackage) { + throw "Rendered XML must contain exactly one MainPackage element." +} +if ($mainPackage.Publisher -ne $Publisher) { + throw "Rendered XML has Publisher '$($mainPackage.Publisher)' but expected '$Publisher'." +} +if ($mainPackage.Name -ne $IdentityName) { + throw "Rendered XML has MainPackage Name '$($mainPackage.Name)' but expected '$IdentityName'." +} +if ($mainPackage.Version -ne $Version) { + throw "Rendered XML has MainPackage Version '$($mainPackage.Version)' but expected '$Version'." +} +if ($mainPackage.ProcessorArchitecture -ne $ProcessorArchitecture) { + throw "Rendered XML has ProcessorArchitecture '$($mainPackage.ProcessorArchitecture)' but expected '$ProcessorArchitecture'." +} +if ($mainPackage.Uri -ne $MsixUri) { + throw "Rendered XML has package Uri '$($mainPackage.Uri)' but expected '$MsixUri'." +} + +# We do not emit a block (WindowsAppSDKSelfContained=true), so +# guard against accidentally re-introducing one in the template. +if ($null -ne $xml.AppInstaller.Dependencies) { + throw "Rendered XML must not contain a block (MSIX is self-contained)." +} + +$outDir = Split-Path -Parent $OutputPath +if ($outDir -and -not (Test-Path $outDir)) { + New-Item -ItemType Directory -Force -Path $outDir | Out-Null +} +Set-Content -Path $OutputPath -Value $rendered -Encoding UTF8 + +Write-Host "Rendered AppInstaller: $OutputPath" +Write-Host " Version: $Version" +Write-Host " Publisher: $Publisher" +Write-Host " Identity: $IdentityName" +Write-Host " Architecture: $ProcessorArchitecture" +Write-Host " MSIX URI: $MsixUri" +Write-Host " AppInstaller URI: $AppInstallerUri" diff --git a/scripts/test-appinstaller-update.ps1 b/scripts/test-appinstaller-update.ps1 new file mode 100644 index 000000000..6d43d0425 --- /dev/null +++ b/scripts/test-appinstaller-update.ps1 @@ -0,0 +1,175 @@ +<# +.SYNOPSIS + Simulates a non-Store .appinstaller upgrade by hosting two signed MSIX + versions on a local HTTP server and walking the install vN -> publish vN+1 + -> trigger upgrade flow end-to-end. + +.DESCRIPTION + The point of this script is to catch regressions in the .appinstaller XML + and the PackageManager.AddPackageByAppInstallerFileAsync wiring without + needing a real GitHub release / stable-feed PR cycle. Run before a release + tag goes out; if it fails, the same failure will reach every user who + installs from the stable architecture-specific AppInstaller URL. + + Steps: + 1. Launch a tiny HTTP server (HttpListener) on localhost:$Port that serves + the two MSIX files plus a rendered .appinstaller pointing at vN+1. + 2. Render an "old" .appinstaller pointing at vN and install it (records + the source URL with Windows AppInstaller). + 3. Re-render the .appinstaller in place pointing at vN+1. + 4. Invoke PackageManager.AddPackageByAppInstallerFileAsync against the + local URL — same call the OS performs in the AutomaticBackgroundTask. + 5. Assert Get-AppxPackage reports the new Version. + 6. Tear down. + + Requires signed MSIX packages — Add-AppxPackage / AddPackageByAppInstaller + refuse unsigned packages outside a developer-mode loopback. Use the dev + signing cert from scripts/setup-dev-msix-cert.ps1 to produce vN and vN+1. + +.PARAMETER MsixVnPath + Path to the "older" signed .msix (used as the seed install). + +.PARAMETER MsixVn1Path + Path to the "newer" signed .msix (used as the upgrade target). + +.PARAMETER VnVersion + 4-part version of the older .msix (e.g. 0.5.3.0). + +.PARAMETER Vn1Version + 4-part version of the newer .msix (e.g. 0.5.4.0). + +.PARAMETER Publisher + Publisher subject that must match BOTH MSIX manifests. + +.EXAMPLE + ./scripts/test-appinstaller-update.ps1 ` + -MsixVnPath .\OpenClawCompanion-0.5.3-win-x64.msix -VnVersion 0.5.3.0 ` + -MsixVn1Path .\OpenClawCompanion-0.5.4-win-x64.msix -Vn1Version 0.5.4.0 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string] $MsixVnPath, + [Parameter(Mandatory)] [string] $VnVersion, + [Parameter(Mandatory)] [string] $MsixVn1Path, + [Parameter(Mandatory)] [string] $Vn1Version, + [string] $Publisher = 'CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US', + [int] $Port = 8765 +) + +$ErrorActionPreference = 'Stop' + +foreach ($p in @($MsixVnPath, $MsixVn1Path)) { + if (-not (Test-Path $p)) { throw "MSIX not found: $p" } +} + +$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "openclaw-appinstaller-test-$(Get-Random)" +New-Item -ItemType Directory -Force -Path $tmp | Out-Null + +try { + Copy-Item $MsixVnPath (Join-Path $tmp 'vN.msix') + Copy-Item $MsixVn1Path (Join-Path $tmp 'vNplus1.msix') + + $baseUri = "http://127.0.0.1:$Port" + $repoRoot = Split-Path -Parent $PSScriptRoot + + function Render-AppInstaller { + param([string]$Version, [string]$MsixFileName, [string]$OutputPath) + & "$repoRoot\scripts\render-appinstaller.ps1" ` + -Version $Version ` + -Publisher $Publisher ` + -ProcessorArchitecture x64 ` + -MsixUri "$baseUri/$MsixFileName" ` + -AppInstallerUri "$baseUri/openclaw.appinstaller" ` + -OutputPath $OutputPath ` + -AllowHttpForLocalTest + } + + Render-AppInstaller -Version $VnVersion -MsixFileName 'vN.msix' -OutputPath (Join-Path $tmp 'openclaw.appinstaller') + + # Spin up exactly one HttpListener in a background job. Binding the same + # prefix in both parent and job would fail before AppInstaller is exercised. + $listenerJob = Start-Job -ScriptBlock { + param($prefix, $root) + $l = [System.Net.HttpListener]::new() + $l.Prefixes.Add("$prefix/") + $l.Start() + while ($l.IsListening) { + $ctx = $l.GetContext() + $name = [System.IO.Path]::GetFileName($ctx.Request.Url.LocalPath) + $path = Join-Path $root $name + if (Test-Path $path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + $ctx.Response.ContentType = if ($name.EndsWith('.appinstaller')) { 'application/appinstaller' } else { 'application/octet-stream' } + $ctx.Response.ContentLength64 = $bytes.Length + $ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length) + } else { + $ctx.Response.StatusCode = 404 + } + $ctx.Response.Close() + } + } -ArgumentList $baseUri, $tmp + + $listenerReady = $false + for ($i = 0; $i -lt 20; $i++) { + if ($listenerJob.State -eq 'Failed') { + Receive-Job $listenerJob -Keep | Out-String | Write-Error + throw "AppInstaller test HTTP listener failed to start." + } + + try { + Invoke-WebRequest "$baseUri/openclaw.appinstaller" -UseBasicParsing -TimeoutSec 2 | Out-Null + $listenerReady = $true + break + } catch { + Start-Sleep -Milliseconds 250 + } + } + if (-not $listenerReady) { + throw "AppInstaller test HTTP listener did not serve $baseUri/openclaw.appinstaller." + } + Write-Host "Listening on $baseUri/" -ForegroundColor Cyan + + try { + # Step 2: install vN via the .appinstaller URL. + Write-Host "Installing vN via $baseUri/openclaw.appinstaller ..." -ForegroundColor Cyan + Add-AppxPackage -AppInstallerFile "$baseUri/openclaw.appinstaller" -ForceApplicationShutdown + $pkg = Get-AppxPackage -Name 'OpenClaw.Companion*' | Select-Object -First 1 + if ($pkg.Version -ne $VnVersion) { + throw "Expected vN install to land version $VnVersion, got $($pkg.Version)" + } + Write-Host " vN installed: $($pkg.Version)" -ForegroundColor Green + + # Step 3: re-render the .appinstaller in place pointing at vN+1. + Render-AppInstaller -Version $Vn1Version -MsixFileName 'vNplus1.msix' -OutputPath (Join-Path $tmp 'openclaw.appinstaller') + + # Step 4: trigger the upgrade via PackageManager (same call the OS uses). + Write-Host "Triggering upgrade to vN+1 via PackageManager.AddPackageByAppInstallerFileAsync ..." -ForegroundColor Cyan + Add-Type -AssemblyName 'Windows.Management.Deployment.PackageManager, ContentType=WindowsRuntime' + $pm = [Windows.Management.Deployment.PackageManager,Windows.Management.Deployment,ContentType=WindowsRuntime]::new() + $op = $pm.AddPackageByAppInstallerFileAsync( + [Uri]"$baseUri/openclaw.appinstaller", + [Windows.Management.Deployment.AddPackageByAppInstallerOptions]::None, + $pm.GetDefaultPackageVolume()) + $result = $op.AsTask().GetAwaiter().GetResult() + if (-not $result.IsRegistered) { + throw "Upgrade failed: $($result.ErrorText) (HRESULT 0x$('{0:X8}' -f $result.ExtendedErrorCode.HResult))" + } + Write-Host " PackageManager reported IsRegistered=$($result.IsRegistered)" -ForegroundColor Green + + # Step 5: assert. + $pkg = Get-AppxPackage -Name 'OpenClaw.Companion*' | Select-Object -First 1 + if ($pkg.Version -ne $Vn1Version) { + throw "Expected upgrade to land version $Vn1Version, got $($pkg.Version)" + } + Write-Host "vN+1 verified at $($pkg.Version)" -ForegroundColor Green + + Write-Host "`nAppInstaller upgrade simulation: PASS" -ForegroundColor Green + } + finally { + if ($listenerJob) { Stop-Job $listenerJob -ErrorAction SilentlyContinue; Remove-Job $listenerJob -Force -ErrorAction SilentlyContinue } + } +} +finally { + if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } +} diff --git a/scripts/validate-appinstaller-hosting.ps1 b/scripts/validate-appinstaller-hosting.ps1 new file mode 100644 index 000000000..6d72d978d --- /dev/null +++ b/scripts/validate-appinstaller-hosting.ps1 @@ -0,0 +1,221 @@ +<# +.SYNOPSIS + Validates a hosted AppInstaller XML and its referenced MSIX URL before + promoting a release. + +.DESCRIPTION + Windows AppInstaller is strict about hosted metadata and package assets. + This script checks the stable .appinstaller URL (or a local file), parses + its MainPackage URI when -MsixUri is not provided, then validates the MSIX + endpoint for content-type, content-length, and range-request support. + + Intended for release operators before promoting + installer/appinstaller/openclaw-{x64,arm64}.appinstaller to the stable + feed location, and for the appinstaller-feed-pr workflow. + +.PARAMETER AppInstallerUri + Stable hosted .appinstaller URL, e.g. + https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller. + +.PARAMETER MsixUri + Optional MSIX URL. When omitted, the script fetches AppInstallerUri (or + reads AppInstallerPath) and uses the MainPackage Uri attribute. + +.PARAMETER AppInstallerPath + Optional local .appinstaller file to parse instead of fetching AppInstallerUri. + Used by the feed-update PR workflow before the rendered file is merged to + master at the stable raw GitHub location. + +.PARAMETER AllowGitHubContentTypes + Compatibility switch for GitHub-hosted release assets. GitHub release + downloads serve MSIX files as application/octet-stream and raw .appinstaller + files as text/plain. This switch keeps strict validation as the default + while allowing the GitHub-hosted production flow. + +.EXAMPLE + ./scripts/validate-appinstaller-hosting.ps1 ` + -AppInstallerUri https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller ` + -AllowGitHubContentTypes +#> + +[CmdletBinding()] +param( + [Uri] $AppInstallerUri, + [string] $AppInstallerPath, + [Uri] $MsixUri, + [switch] $AllowGitHubContentTypes +) + +$ErrorActionPreference = 'Stop' + +function Get-HeaderValue { + param( + [Parameter(Mandatory)] $Response, + [Parameter(Mandatory)] [string] $Name + ) + + $value = $Response.Headers[$Name] + if ($value -is [array]) { return $value[0] } + return $value +} + +function Invoke-Head { + param([Parameter(Mandatory)] [Uri] $Uri) + + try { + return Invoke-WebRequest -Uri $Uri -Method Head -MaximumRedirection 5 -UseBasicParsing + } + catch { + throw "HEAD $Uri failed: $($_.Exception.Message)" + } +} + +function Assert-HttpsUri { + param( + [Parameter(Mandatory)] [Uri] $Uri, + [Parameter(Mandatory)] [string] $Description + ) + + if ($Uri.Scheme -ne 'https') { + throw "$Description must use https: $Uri" + } +} + +function Assert-ContentType { + param( + [Parameter(Mandatory)] $Response, + [Parameter(Mandatory)] [Uri] $Uri, + [Parameter(Mandatory)] [string] $Expected, + [string[]] $AlsoAllowed = @() + ) + + $contentType = Get-HeaderValue -Response $Response -Name 'Content-Type' + $allowed = @($Expected) + $AlsoAllowed + foreach ($candidate in $allowed) { + if (-not [string]::IsNullOrWhiteSpace($contentType) -and + $contentType.StartsWith($candidate, [StringComparison]::OrdinalIgnoreCase)) { + Write-Host " Content-Type OK: $contentType" + return + } + } + + if ([string]::IsNullOrWhiteSpace($contentType) -or $allowed.Count -eq 1) { + throw "$Uri returned Content-Type '$contentType'; expected '$Expected'." + } + throw "$Uri returned Content-Type '$contentType'; expected one of: $($allowed -join ', ')." +} + +function Get-MainPackageUri { + param([Parameter(Mandatory)] [xml] $AppInstallerXml) + + $namespaceManager = [System.Xml.XmlNamespaceManager]::new($AppInstallerXml.NameTable) + $namespaceManager.AddNamespace('ai', 'http://schemas.microsoft.com/appx/appinstaller/2018') + $mainPackage = $AppInstallerXml.SelectSingleNode('/ai:AppInstaller/ai:MainPackage', $namespaceManager) + if ($null -eq $mainPackage) { + $mainPackage = $AppInstallerXml.SelectSingleNode('/AppInstaller/MainPackage') + } + + $mainPackageUri = if ($null -eq $mainPackage) { $null } else { $mainPackage.GetAttribute('Uri') } + if ([string]::IsNullOrWhiteSpace($mainPackageUri)) { + throw "AppInstaller XML does not contain a MainPackage Uri." + } + + return [Uri]$mainPackageUri +} + +function Assert-ContentLength { + param( + [Parameter(Mandatory)] $Response, + [Parameter(Mandatory)] [Uri] $Uri + ) + + $contentLength = Get-HeaderValue -Response $Response -Name 'Content-Length' + if ([string]::IsNullOrWhiteSpace($contentLength)) { + throw "$Uri did not return Content-Length." + } + $parsed = 0L + if (-not [long]::TryParse($contentLength, [ref]$parsed)) { + throw "$Uri returned non-numeric Content-Length '$contentLength'." + } + Write-Host " Content-Length OK: $contentLength" +} + +function Assert-MsixRangeRequest { + param([Parameter(Mandatory)] [Uri] $Uri) + + try { + $response = Invoke-WebRequest -Uri $Uri ` + -Method Get ` + -Headers @{ Range = 'bytes=0-0' } ` + -MaximumRedirection 5 ` + -UseBasicParsing + } + catch { + throw "Range GET $Uri failed: $($_.Exception.Message)" + } + + if ($response.StatusCode -ne 206) { + throw "$Uri did not honor range request. Expected HTTP 206, got HTTP $($response.StatusCode)." + } + + $contentRange = Get-HeaderValue -Response $response -Name 'Content-Range' + if ([string]::IsNullOrWhiteSpace($contentRange)) { + throw "$Uri returned HTTP 206 but omitted Content-Range." + } + Write-Host " Range request OK: $contentRange" +} + +if ([string]::IsNullOrWhiteSpace($AppInstallerPath) -and $null -eq $AppInstallerUri) { + throw "Provide either -AppInstallerUri or -AppInstallerPath." +} + +if (-not [string]::IsNullOrWhiteSpace($AppInstallerPath)) { + if (-not (Test-Path $AppInstallerPath)) { + throw "AppInstallerPath not found: $AppInstallerPath" + } + + Write-Host "Validating local AppInstaller XML: $AppInstallerPath" + [xml]$appInstallerXml = Get-Content -Path $AppInstallerPath -Raw + if ($null -eq $MsixUri) { + $MsixUri = Get-MainPackageUri -AppInstallerXml $appInstallerXml + Write-Host "Discovered MSIX URI from local AppInstaller: $MsixUri" + } +} +else { + Write-Host "Validating AppInstaller hosting: $AppInstallerUri" + Assert-HttpsUri -Uri $AppInstallerUri -Description 'AppInstallerUri' + $appInstallerHead = Invoke-Head -Uri $AppInstallerUri + $allowedAppInstallerTypes = if ($AllowGitHubContentTypes -and + $AppInstallerUri.Host.Equals('raw.githubusercontent.com', [StringComparison]::OrdinalIgnoreCase)) { + @('text/plain') + } else { + @() + } + Assert-ContentType -Response $appInstallerHead -Uri $AppInstallerUri -Expected 'application/appinstaller' -AlsoAllowed $allowedAppInstallerTypes + if (-not ($AllowGitHubContentTypes -and + $AppInstallerUri.Host.Equals('raw.githubusercontent.com', [StringComparison]::OrdinalIgnoreCase))) { + Assert-ContentLength -Response $appInstallerHead -Uri $AppInstallerUri + } + + if ($null -eq $MsixUri) { + $appInstallerBody = Invoke-WebRequest -Uri $AppInstallerUri -Method Get -MaximumRedirection 5 -UseBasicParsing + [xml]$appInstallerXml = $appInstallerBody.Content + $MsixUri = Get-MainPackageUri -AppInstallerXml $appInstallerXml + Write-Host "Discovered MSIX URI from AppInstaller: $MsixUri" + } +} + +Write-Host "Validating MSIX hosting: $MsixUri" +Assert-HttpsUri -Uri $MsixUri -Description 'MsixUri' +$msixHead = Invoke-Head -Uri $MsixUri +$allowedMsixTypes = if ($AllowGitHubContentTypes -and + $MsixUri.Host.Equals('github.com', [StringComparison]::OrdinalIgnoreCase)) { + @('application/octet-stream') +} else { + @() +} +Assert-ContentType -Response $msixHead -Uri $MsixUri -Expected 'application/msix' -AlsoAllowed $allowedMsixTypes +Assert-ContentLength -Response $msixHead -Uri $MsixUri +Assert-MsixRangeRequest -Uri $MsixUri + +Write-Host "AppInstaller hosting validation passed." -ForegroundColor Green diff --git a/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs new file mode 100644 index 000000000..58e7c2757 --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs @@ -0,0 +1,200 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace OpenClaw.Tray.Tests; + +/// +/// Structural assertions on the AppInstaller template and the publishing +/// infrastructure that renders it. The template is rendered by CI and parsed +/// by Windows AppInstaller; even a single attribute typo silently breaks +/// auto-update for every user who installed via the stable feed link, with +/// no in-app surface to notice. +/// +/// We deliberately don't ship an in-app "Check for updates" affordance under +/// MSIX — Windows AppInstaller's AutomaticBackgroundTask handles polling at +/// the OS level. So these tests pin only the publishing-infrastructure +/// contract: template shape, bootstrap feed files, validation scripts, and +/// the feed-update workflow. +/// +public sealed class AppInstallerTemplateAssertionTests +{ + private static string GetRepositoryRoot() + { + var envRepoRoot = Environment.GetEnvironmentVariable("OPENCLAW_REPO_ROOT"); + if (!string.IsNullOrWhiteSpace(envRepoRoot) && Directory.Exists(envRepoRoot)) + return envRepoRoot; + + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + if ((Directory.Exists(Path.Combine(directory.FullName, ".git")) || + File.Exists(Path.Combine(directory.FullName, ".git"))) && + File.Exists(Path.Combine(directory.FullName, "README.md"))) + return directory.FullName; + directory = directory.Parent; + } + + throw new InvalidOperationException( + "Could not find repository root. Set OPENCLAW_REPO_ROOT to the repo path."); + } + + private static string LoadTemplate() => + File.ReadAllText(Path.Combine( + GetRepositoryRoot(), "installer", "openclaw-companion.appinstaller.template")); + + [Fact] + public void Template_IsWellFormedXml() + { + // Parse with placeholders intact — XML parsing tolerates {{TOKEN}} as + // attribute *values* because they're just strings. + var doc = XDocument.Parse(LoadTemplate()); + Assert.Equal("AppInstaller", doc.Root!.Name.LocalName); + Assert.Equal("http://schemas.microsoft.com/appx/appinstaller/2018", + doc.Root.Name.NamespaceName); + } + + [Theory] + [InlineData("{{VERSION}}")] + [InlineData("{{PUBLISHER}}")] + [InlineData("{{IDENTITY_NAME}}")] + [InlineData("{{PROCESSOR_ARCHITECTURE}}")] + [InlineData("{{MSIX_URI}}")] + [InlineData("{{APPINSTALLER_URI}}")] + public void Template_DeclaresExpectedPlaceholder(string token) + { + // scripts/render-appinstaller.ps1 substitutes exactly these tokens. + // If you add a new placeholder here, also add a -Replace in the script + // AND a CI step parameter. If you remove one, the renderer silently + // ships the literal {{TOKEN}} string to AppInstaller which fails to parse. + Assert.Contains(token, LoadTemplate()); + } + + [Fact] + public void Template_UsesQuietBackgroundUpdateSettingsOnly() + { + var doc = XDocument.Parse(LoadTemplate()); + XNamespace ns = "http://schemas.microsoft.com/appx/appinstaller/2018"; + + Assert.Empty(doc.Descendants(ns + "OnLaunch")); + Assert.Empty(doc.Descendants(ns + "ForceUpdateFromAnyVersion")); + + var backgroundTasks = doc.Descendants(ns + "AutomaticBackgroundTask").ToArray(); + Assert.Single(backgroundTasks); + } + + [Fact] + public void Template_UsesArchitectureSpecificMainPackage() + { + var doc = XDocument.Parse(LoadTemplate()); + XNamespace ns = "http://schemas.microsoft.com/appx/appinstaller/2018"; + Assert.Empty(doc.Descendants(ns + "MainBundle")); + + var mainPackage = doc.Descendants(ns + "MainPackage").Single(); + Assert.Equal("{{IDENTITY_NAME}}", (string?)mainPackage.Attribute("Name")); + Assert.Equal("{{PROCESSOR_ARCHITECTURE}}", (string?)mainPackage.Attribute("ProcessorArchitecture")); + Assert.Equal("{{MSIX_URI}}", (string?)mainPackage.Attribute("Uri")); + } + + [Fact] + public void Template_HasNoDependenciesBlock() + { + // The MSIX is built with WindowsAppSDKSelfContained=true, so the + // WindowsAppRuntime is bundled inside the .msix payload. The + // AppInstaller XML therefore must NOT declare a block — + // a stale block here would either fail-to-resolve at + // install time, or worse, silently pull an extra framework package + // the app doesn't need. + var doc = XDocument.Parse(LoadTemplate()); + XNamespace ns = "http://schemas.microsoft.com/appx/appinstaller/2018"; + + Assert.Empty(doc.Descendants(ns + "Dependencies")); + Assert.Empty(doc.Descendants(ns + "Package")); + } + + [Fact] + public void StableFeedFiles_ExistAsBootstrapPlaceholders() + { + foreach (var (fileName, arch) in new[] + { + ("openclaw-x64.appinstaller", "x64"), + ("openclaw-arm64.appinstaller", "arm64") + }) + { + var path = Path.Combine(GetRepositoryRoot(), "installer", "appinstaller", fileName); + Assert.True(File.Exists(path), $"Missing stable feed bootstrap file: {fileName}"); + + var doc = XDocument.Load(path); + XNamespace ns = "http://schemas.microsoft.com/appx/appinstaller/2018"; + Assert.Equal("0.0.0.0", (string?)doc.Root!.Attribute("Version")); + Assert.Equal($"https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/{fileName}", + (string?)doc.Root.Attribute("Uri")); + + var mainPackage = doc.Descendants(ns + "MainPackage").Single(); + Assert.Equal("OpenClaw.Companion", (string?)mainPackage.Attribute("Name")); + Assert.Equal("0.0.0.0", (string?)mainPackage.Attribute("Version")); + Assert.Equal(arch, (string?)mainPackage.Attribute("ProcessorArchitecture")); + } + } + + [Fact] + public void HostingValidationScript_ChecksMimeLengthAndRange() + { + var script = File.ReadAllText(Path.Combine( + GetRepositoryRoot(), "scripts", "validate-appinstaller-hosting.ps1")); + + Assert.Contains("application/appinstaller", script); + Assert.Contains("application/msix", script); + Assert.Contains("AppInstallerPath", script); + Assert.Contains("AllowGitHubContentTypes", script); + Assert.Contains("application/octet-stream", script); + Assert.Contains("Scheme -ne 'https'", script); + Assert.Contains("Content-Length", script); + Assert.Contains("Range = 'bytes=0-0'", script); + Assert.Contains("StatusCode -ne 206", script); + } + + [Fact] + public void AppInstallerUpdateSmokeScript_BindsSingleHttpListenerAndSelfChecks() + { + var script = File.ReadAllText(Path.Combine( + GetRepositoryRoot(), "scripts", "test-appinstaller-update.ps1")); + + // The listener lives only inside Start-Job; the parent must not + // open a second HttpListener on the same prefix. + Assert.DoesNotContain("$listener = [System.Net.HttpListener]::new()", script); + Assert.Contains("Invoke-WebRequest \"$baseUri/openclaw.appinstaller\"", script); + Assert.Contains("$listenerJob.State -eq 'Failed'", script); + } + + [Fact] + public void FeedUpdateWorkflow_OpensMaintainerPrAndBlocksPrereleaseFeeds() + { + var workflow = File.ReadAllText(Path.Combine( + GetRepositoryRoot(), ".github", "workflows", "appinstaller-feed-pr.yml")); + + Assert.Contains("workflow_dispatch", workflow); + Assert.Contains("pull-requests: write", workflow); + Assert.Contains("contents: write", workflow); + Assert.Contains("Pre-release AppInstaller feed updates are blocked", workflow); + Assert.Contains("installer\\appinstaller", workflow); + Assert.Contains("openclaw-x64.appinstaller", workflow); + Assert.Contains("openclaw-arm64.appinstaller", workflow); + Assert.Contains("gh pr create", workflow); + Assert.Contains("--base master", workflow); + Assert.Contains("validate-appinstaller-hosting.ps1", workflow); + Assert.Contains("-AllowGitHubContentTypes", workflow); + Assert.Contains("OpenClawCompanion-$versionText-win-x64.msix", workflow); + Assert.Contains("OpenClawCompanion-$versionText-win-arm64.msix", workflow); + + // MSIX is self-contained — the workflow must not fetch or pass a + // separate WindowsAppRuntime asset. + Assert.DoesNotContain("Microsoft.WindowsAppRuntime", workflow); + Assert.DoesNotContain("WindowsAppRuntimeUri", workflow); + + // Stable feed only — no wildcard alpha/staging file globbing. + Assert.DoesNotContain("OpenClawCompanion-*-win-x64.msix", workflow); + Assert.DoesNotContain("OpenClawCompanion-*-win-arm64.msix", workflow); + } +} From 2aa4d3de775e7b80837b4127fb241582885a1031 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 11:15:47 -0700 Subject: [PATCH 11/38] Phase 4: adopt MSBuild default MSIX naming for release assets The Phase 4 appinstaller feed workflow expected release assets named 'OpenClawCompanion-<3partVersion>-win-.msix', which would have required a rename step in the Phase 5 release job (Option A). Switching to Option B: match whatever MSBuild emits by default for GenerateAppxPackageOnBuild=true, i.e. 'OpenClaw.Companion_<4partVersion>_.msix'. * appinstaller-feed-pr.yml: Get-RequiredAsset patterns updated to use the 4-part $version (not the 3-part $versionText), dotted identity, and underscore separators. Uses ${version} to keep _ from being absorbed into the variable name. * AppInstallerTemplateAssertionTests: matching Assert.Contains / Assert.DoesNotContain updates so the pin reflects the new convention. Inside the rendered .appinstaller, uses the actual GitHub release asset name, so AppInstaller behavior is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/appinstaller-feed-pr.yml | 4 ++-- .../AppInstallerTemplateAssertionTests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/appinstaller-feed-pr.yml b/.github/workflows/appinstaller-feed-pr.yml index af7169052..4d94da434 100644 --- a/.github/workflows/appinstaller-feed-pr.yml +++ b/.github/workflows/appinstaller-feed-pr.yml @@ -72,8 +72,8 @@ jobs: return "https://github.com/$repo/releases/download/$tag/$escapedName" } - $x64Asset = Get-RequiredAsset -Pattern "OpenClawCompanion-$versionText-win-x64.msix" - $arm64Asset = Get-RequiredAsset -Pattern "OpenClawCompanion-$versionText-win-arm64.msix" + $x64Asset = Get-RequiredAsset -Pattern "OpenClaw.Companion_${version}_x64.msix" + $arm64Asset = Get-RequiredAsset -Pattern "OpenClaw.Companion_${version}_arm64.msix" $x64Uri = Get-ReleaseAssetUri -AssetName $x64Asset.name $arm64Uri = Get-ReleaseAssetUri -AssetName $arm64Asset.name diff --git a/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs index 58e7c2757..5575618db 100644 --- a/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs +++ b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs @@ -185,8 +185,8 @@ public void FeedUpdateWorkflow_OpensMaintainerPrAndBlocksPrereleaseFeeds() Assert.Contains("--base master", workflow); Assert.Contains("validate-appinstaller-hosting.ps1", workflow); Assert.Contains("-AllowGitHubContentTypes", workflow); - Assert.Contains("OpenClawCompanion-$versionText-win-x64.msix", workflow); - Assert.Contains("OpenClawCompanion-$versionText-win-arm64.msix", workflow); + Assert.Contains("OpenClaw.Companion_${version}_x64.msix", workflow); + Assert.Contains("OpenClaw.Companion_${version}_arm64.msix", workflow); // MSIX is self-contained — the workflow must not fetch or pass a // separate WindowsAppRuntime asset. @@ -194,7 +194,7 @@ public void FeedUpdateWorkflow_OpensMaintainerPrAndBlocksPrereleaseFeeds() Assert.DoesNotContain("WindowsAppRuntimeUri", workflow); // Stable feed only — no wildcard alpha/staging file globbing. - Assert.DoesNotContain("OpenClawCompanion-*-win-x64.msix", workflow); - Assert.DoesNotContain("OpenClawCompanion-*-win-arm64.msix", workflow); + Assert.DoesNotContain("OpenClaw.Companion_*_x64.msix", workflow); + Assert.DoesNotContain("OpenClaw.Companion_*_arm64.msix", workflow); } } From c0b9cc5f37d200f90472be60f3a020ab3860d1b1 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 11:19:57 -0700 Subject: [PATCH 12/38] Phase 5: sign and publish MSIX release artifacts Unpause the build-msix matrix job and wire its outputs into the release job so that tagged builds produce signed .msix artifacts attached to the GitHub Release. - ci.yml build-msix: drop `if: false` paused gate and `continue-on-error: true` placeholder; the job is now load-bearing. - ci.yml release: add `build-msix` to `needs:` plus matching `needs.build-msix.result == 'success'` guard. - Download the per-arch `openclaw-msix-win-{x64,arm64}` artifacts. - Sign each .msix in place using `azure/artifact-signing-action@v2` with `files-folder-filter: msix` (mirrors the existing exe signing pattern: same endpoint, signing account, certificate profile, OIDC auth via azure/login). - Attach both signed .msix files to the release via `files:` and rewrite the release body so it points users at the AppInstaller links in the README (primary install path) and notes the .msix assets as a direct-install fallback. Validation: - ./build.ps1 green - Shared.Tests: 2049 passed / 29 skipped (baseline) - Tray.Tests: 957 passed (baseline) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 63 +++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 160bf7473..9db015d06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -424,9 +424,7 @@ jobs: build-msix: needs: [test, e2etests] - if: false # Paused; will be unpaused when MSIX-primary release pipeline lands (Phase 4). runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }} - continue-on-error: true strategy: fail-fast: false matrix: @@ -518,8 +516,8 @@ jobs: path: ${{ steps.find-msix.outputs.msix_path }} release: - needs: [repo-hygiene, test, e2etests, build] - if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.e2etests.result == 'success' && needs.build.result == 'success' && !cancelled() + needs: [repo-hygiene, test, e2etests, build, build-msix] + if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.e2etests.result == 'success' && needs.build.result == 'success' && needs.build-msix.result == 'success' && !cancelled() runs-on: windows-latest environment: release-signing permissions: @@ -542,6 +540,18 @@ jobs: name: openclaw-tray-win-arm64 path: artifacts/tray-win-arm64 + - name: Download win-x64 MSIX artifact + uses: actions/download-artifact@v8 + with: + name: openclaw-msix-win-x64 + path: artifacts/msix-win-x64 + + - name: Download win-arm64 MSIX artifact + uses: actions/download-artifact@v8 + with: + name: openclaw-msix-win-arm64 + path: artifacts/msix-win-arm64 + - name: Disable NuGet source mapping for signing shell: pwsh run: | @@ -595,6 +605,32 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 + - name: Sign x64 Release MSIX Package + uses: azure/artifact-signing-action@v2 + with: + endpoint: https://eus.codesigning.azure.net/ + signing-account-name: openclaw + certificate-profile-name: openclaw + files-folder: artifacts/msix-win-x64 + files-folder-filter: msix + files-folder-depth: 1 + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Sign ARM64 Release MSIX Package + uses: azure/artifact-signing-action@v2 + with: + endpoint: https://eus.codesigning.azure.net/ + signing-account-name: openclaw + certificate-profile-name: openclaw + files-folder: artifacts/msix-win-arm64 + files-folder-filter: msix + files-folder-depth: 1 + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: Verify x64 Release Executable Signing Policy shell: pwsh run: .\scripts\Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-x64 -RequireSignedOpenClaw @@ -620,22 +656,27 @@ jobs: generate_release_notes: true prerelease: ${{ contains(github.ref_name, '-') }} make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }} + files: | + artifacts/msix-win-x64/*.msix + artifacts/msix-win-arm64/*.msix body: | ## OpenClaw Windows Hub ${{ github.ref_name }} - > MSIX installer artifacts arrive in a follow-up release once the - > MSIX-primary distribution pipeline lands. + ### Install + + OpenClaw is distributed as an MSIX package via Windows AppInstaller. + Use the install links in the [README](https://github.com/openclaw/openclaw-windows-node#install) + for the recommended path — Windows handles silent auto-updates from there. + + The `.msix` files attached below are the underlying signed packages. + Advanced users can install them directly with `Add-AppxPackage `. ### Features - 🦞 System tray integration with gateway status - ✅ Code-signed with Azure Artifact Signing + - 🔄 Silent auto-updates via Windows AppInstaller ### Requirements - Windows 10 version 1903 or later - [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) - OpenClaw gateway running locally - - ### Quick Start - 1. Extract the portable ZIP and run `OpenClaw.Tray.WinUI.exe` - 2. Launch from system tray - 3. Right-click tray icon → Settings to configure From ccb934391d9f04ec3915ccf357c2d848fb61d7ff Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 11:29:08 -0700 Subject: [PATCH 13/38] Phase 6: retire loose-tray build pipeline After Phase 3A removed Inno and Phase 5 wired the MSIX into the release, the `build` job's loose unpackaged-tray artifact ceased to be shipped to any user. The release job was still downloading it, signing the loose exe, and running signature/native-dep validation against bits that were then discarded. This commit removes that dead infrastructure and preserves the most valuable validation against what we actually ship. ci.yml changes: - Delete the `build` job in its entirety (loose `dotnet publish`, Test-ReleaseNativeDependencies on publish/, GitVersion verify, Upload Tray Artifact -> openclaw-tray-{rid}). - Drop `build` from `release.needs:` and from the corresponding `needs.build.result == 'success'` guard. - In `release`, remove: 2 tray-artifact download steps, 2 stage exe for signing steps, 2 sign-loose-exe steps, and 4 verify steps (Test-ReleaseExecutableSignatures and Test-ReleaseNativeDependencies against artifacts/tray-win-*). - In `build-msix`, add a new `Verify MSIX Package Contents` step that runs immediately after the .msix is produced. It extracts the .msix (a zip), confirms `OpenClaw.Tray.WinUI.dll` is present with the correct GitVersion ProductVersion, and runs the existing Test-ReleaseNativeDependencies.ps1 against the extracted payload so the libsodium / VC++ runtime presence canary continues to fire -- but now against the actual shipped MSIX bits, not a phantom unpackaged build. Net diff: -108 lines from ci.yml; pipeline shape simplified from six jobs to five (release no longer depends on a parallel build job whose output it never used). Validation: - ./build.ps1 green - Shared.Tests: 2049 passed / 29 skipped (baseline) - Tray.Tests: 957 passed (baseline) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 170 +++++++-------------------------------- 1 file changed, 31 insertions(+), 139 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9db015d06..9c153f47b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -354,74 +354,6 @@ jobs: TestResults/E2E/ if-no-files-found: warn - build: - needs: [test, e2etests] - runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }} - strategy: - matrix: - rid: [win-x64, win-arm64] - - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Cache NuGet packages - continue-on-error: true - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }} - restore-keys: nuget-${{ runner.os }}- - - - name: Restore WinUI Tray App - run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} - - - name: Build WinUI Tray App (Release) - run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} - - - name: Publish WinUI Tray App - run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish - - - name: Verify x64 Native Runtime Payload - if: matrix.rid == 'win-x64' - shell: pwsh - run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath publish -RequireAppLocalVCRuntime - - - name: Verify ARM64 Native Runtime Payload - if: matrix.rid == 'win-arm64' - shell: pwsh - # -SkipNativeLoadProbe: an ARM64 runner CAN LoadLibrary ARM64 DLLs, but - # libsodium pulls in a long native dependency chain that may not all be - # in the payload here (WindowsAppSDK pieces, etc.). The probe is for x64 - # parity; signature + presence is what we actually care about. - run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath publish -RequireAppLocalVCRuntime -SkipNativeLoadProbe - - - name: Verify GitVersion assembly metadata - shell: pwsh - run: | - $expected = "${{ needs.test.outputs.semVer }}" - $assemblyPath = Resolve-Path "publish\OpenClaw.Tray.WinUI.dll" - $metadata = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($assemblyPath) - if ([string]::IsNullOrWhiteSpace($metadata.ProductVersion)) { - throw "OpenClaw.Tray.WinUI.dll is missing ProductVersion metadata." - } - $actual = $metadata.ProductVersion -replace '\+.*$', '' - if ($actual -ne $expected) { - throw "ProductVersion '$actual' did not match GitVersion SemVer '$expected'." - } - - - name: Upload Tray Artifact - uses: actions/upload-artifact@v7 - with: - name: openclaw-tray-${{ matrix.rid }} - path: publish/ - build-msix: needs: [test, e2etests] runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }} @@ -509,6 +441,35 @@ jobs: echo "msix_path=$($msix.FullName)" >> $env:GITHUB_OUTPUT echo "msix_name=$($msix.Name)" >> $env:GITHUB_OUTPUT + - name: Verify MSIX Package Contents + shell: pwsh + # Extract the .msix (it's a zip) and validate the actual payload that + # ships to users: GitVersion metadata on the tray .dll and the libsodium + # / VC++ runtime dependencies required by NSec.Cryptography. + run: | + $msixPath = "${{ steps.find-msix.outputs.msix_path }}" + $extractDir = Join-Path $env:RUNNER_TEMP "msix-contents-${{ matrix.rid }}" + if (Test-Path $extractDir) { Remove-Item -Recurse -Force $extractDir } + Expand-Archive -Path $msixPath -DestinationPath $extractDir -Force + + $assemblyPath = Join-Path $extractDir "OpenClaw.Tray.WinUI.dll" + if (-not (Test-Path $assemblyPath)) { + throw "OpenClaw.Tray.WinUI.dll missing from MSIX payload at $assemblyPath" + } + $expected = "${{ needs.test.outputs.semVer }}" + $metadata = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($assemblyPath) + if ([string]::IsNullOrWhiteSpace($metadata.ProductVersion)) { + throw "OpenClaw.Tray.WinUI.dll inside MSIX is missing ProductVersion metadata." + } + $actual = $metadata.ProductVersion -replace '\+.*$', '' + if ($actual -ne $expected) { + throw "MSIX-internal ProductVersion '$actual' did not match GitVersion SemVer '$expected'." + } + + $skipProbeArgs = @{} + if ("${{ matrix.rid }}" -eq "win-arm64") { $skipProbeArgs["SkipNativeLoadProbe"] = $true } + & .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath $extractDir -RequireAppLocalVCRuntime @skipProbeArgs + - name: Upload MSIX Artifact uses: actions/upload-artifact@v7 with: @@ -516,8 +477,8 @@ jobs: path: ${{ steps.find-msix.outputs.msix_path }} release: - needs: [repo-hygiene, test, e2etests, build, build-msix] - if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.e2etests.result == 'success' && needs.build.result == 'success' && needs.build-msix.result == 'success' && !cancelled() + needs: [repo-hygiene, test, e2etests, build-msix] + if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.e2etests.result == 'success' && needs.build-msix.result == 'success' && !cancelled() runs-on: windows-latest environment: release-signing permissions: @@ -528,18 +489,6 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Download win-x64 tray artifact - uses: actions/download-artifact@v8 - with: - name: openclaw-tray-win-x64 - path: artifacts/tray-win-x64 - - - name: Download win-arm64 tray artifact - uses: actions/download-artifact@v8 - with: - name: openclaw-tray-win-arm64 - path: artifacts/tray-win-arm64 - - name: Download win-x64 MSIX artifact uses: actions/download-artifact@v8 with: @@ -567,44 +516,6 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Stage x64 OpenClaw Executables for Signing - shell: pwsh - run: | - New-Item -ItemType Directory -Path signing-input-x64 -Force | Out-Null - New-Item -ItemType HardLink -Path signing-input-x64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-x64\OpenClaw.Tray.WinUI.exe | Out-Null - - - name: Stage ARM64 OpenClaw Executables for Signing - shell: pwsh - run: | - New-Item -ItemType Directory -Path signing-input-arm64 -Force | Out-Null - New-Item -ItemType HardLink -Path signing-input-arm64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-arm64\OpenClaw.Tray.WinUI.exe | Out-Null - - - name: Sign x64 OpenClaw Executables - uses: azure/artifact-signing-action@v2 - with: - endpoint: https://eus.codesigning.azure.net/ - signing-account-name: openclaw - certificate-profile-name: openclaw - files-folder: signing-input-x64 - files-folder-filter: exe - files-folder-depth: 1 - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - - name: Sign ARM64 OpenClaw Executables - uses: azure/artifact-signing-action@v2 - with: - endpoint: https://eus.codesigning.azure.net/ - signing-account-name: openclaw - certificate-profile-name: openclaw - files-folder: signing-input-arm64 - files-folder-filter: exe - files-folder-depth: 1 - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - name: Sign x64 Release MSIX Package uses: azure/artifact-signing-action@v2 with: @@ -631,25 +542,6 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - - name: Verify x64 Release Executable Signing Policy - shell: pwsh - run: .\scripts\Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-x64 -RequireSignedOpenClaw - - - name: Verify ARM64 Release Executable Signing Policy - shell: pwsh - run: .\scripts\Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireSignedOpenClaw - - - name: Verify x64 Release Native Dependencies - shell: pwsh - run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-x64 -RequireAppLocalVCRuntime - - - name: Verify ARM64 Release Native Dependencies - shell: pwsh - # -SkipNativeLoadProbe: this release job runs on the x64 windows-latest - # runner and cannot LoadLibrary an ARM64 DLL. The signature and presence - # checks still run. - run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireAppLocalVCRuntime -SkipNativeLoadProbe - - name: Create Release uses: softprops/action-gh-release@v3 with: From 57b9ccc00594ecc23c41a7bdf63b71c68f3fea6a Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 11:53:33 -0700 Subject: [PATCH 14/38] chore(msix): update stale master->main branch references Phase 4 AppInstaller infrastructure was authored against master but the default branch was renamed to main (commit 37b0ea67). The stale refs would cause silent failure: AppInstaller polling would forever fetch frozen XML from the now-inactive origin/master branch, and the feed-update PR workflow would target a branch that doesn't accept commits. Fixed: - installer/appinstaller/openclaw-{x64,arm64}.appinstaller: -> main - installer/appinstaller/README.md: hosting URLs + prose -> main - README.md: end-user install links -> main - .github/workflows/appinstaller-feed-pr.yml: ref/raw URL/PR --base -> main - scripts/render-appinstaller.ps1: docstring URLs -> main - scripts/validate-appinstaller-hosting.ps1: docstring -> main - tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs: assertions tracking the above Also swept pre-existing master refs that the rename PR (37b0ea67) missed: - docs/RELEASING.md, docs/VERSIONING.md: prerelease prose - tests/.../LocalizationValidationTests.cs: comment - src/.../ConnectionPage.xaml.cs: comment Left alone (per main's rename PR intent): dual [main, master] CI triggers, GitVersion ^(master|main)$ regex, third-party URLs, and all 'Master switch/toggle/control' UX terminology (unrelated to git branch). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/appinstaller-feed-pr.yml | 8 ++++---- README.md | 4 ++-- docs/RELEASING.md | 2 +- docs/VERSIONING.md | 6 +++--- installer/appinstaller/README.md | 8 ++++---- installer/appinstaller/openclaw-arm64.appinstaller | 2 +- installer/appinstaller/openclaw-x64.appinstaller | 2 +- scripts/render-appinstaller.ps1 | 4 ++-- scripts/validate-appinstaller-hosting.ps1 | 6 +++--- src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs | 2 +- .../AppInstallerTemplateAssertionTests.cs | 4 ++-- tests/OpenClaw.Tray.Tests/LocalizationValidationTests.cs | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/appinstaller-feed-pr.yml b/.github/workflows/appinstaller-feed-pr.yml index 4d94da434..82bcd9d6e 100644 --- a/.github/workflows/appinstaller-feed-pr.yml +++ b/.github/workflows/appinstaller-feed-pr.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: master + ref: main fetch-depth: 0 - name: Render stable AppInstaller feed files @@ -77,7 +77,7 @@ jobs: $x64Uri = Get-ReleaseAssetUri -AssetName $x64Asset.name $arm64Uri = Get-ReleaseAssetUri -AssetName $arm64Asset.name - $rawBase = "https://raw.githubusercontent.com/$repo/master/installer/appinstaller" + $rawBase = "https://raw.githubusercontent.com/$repo/main/installer/appinstaller" $x64FeedPath = Join-Path $feedDir 'openclaw-x64.appinstaller' $arm64FeedPath = Join-Path $feedDir 'openclaw-arm64.appinstaller' @@ -149,11 +149,11 @@ bundled inside the .msix and never fetches a separate framework package. "@ Set-Content -Path $bodyPath -Value $body -Encoding UTF8 - $existingPr = gh pr list --repo $repo --base master --head $branch --json number --jq '.[0].number' + $existingPr = gh pr list --repo $repo --base main --head $branch --json number --jq '.[0].number' if ([string]::IsNullOrWhiteSpace($existingPr)) { gh pr create ` --repo $repo ` - --base master ` + --base main ` --head $branch ` --title "chore(msix): update AppInstaller feed for $tag" ` --body-file $bodyPath diff --git a/README.md b/README.md index 6525b49cb..6da61fb2e 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ This monorepo contains the Windows hub, shared client libraries, and CLI utiliti Install via Windows AppInstaller (auto-updates from the stable feed): -- **Install (x64)** — [openclaw-x64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller) -- **Install (ARM64)** — [openclaw-arm64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-arm64.appinstaller) +- **Install (x64)** — [openclaw-x64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-x64.appinstaller) +- **Install (ARM64)** — [openclaw-arm64.appinstaller](https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-arm64.appinstaller) Click the link for your machine architecture; Windows opens the App Installer UI, prompts for consent, then installs the signed MSIX. Future updates are diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 1ed6f2110..56a652fa1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -217,7 +217,7 @@ Only tag when `HEAD == origin/main`. - Do not add csproj `` release fallbacks; product versions come from GitVersion/tag history. - Release versions come from the tag (`vX.Y.Z` or `vX.Y.Z-alpha.N`). -- Untagged `master` builds are prerelease builds. After `vX.Y.Z-alpha.N`, an +- Untagged `main` builds are prerelease builds. After `vX.Y.Z-alpha.N`, an untagged commit may resolve to the next alpha prerelease, for example `X.Y.Z-alpha.(N+1)`. - CI computes GitVersion outputs for artifact naming, while product builds use diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 997d63687..eb86a8dba 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -22,12 +22,12 @@ Tagged releases must resolve to the exact tag SemVer: - `vX.Y.Z` -> `X.Y.Z` - `vX.Y.Z-alpha.N` -> `X.Y.Z-alpha.N` -Untagged `master` checkouts are still prerelease builds. After an alpha tag, +Untagged `main` checkouts are still prerelease builds. After an alpha tag, GitVersion advances to the next alpha prerelease until another tag pins the -version. For example, after `v0.6.0-alpha.5`, an untagged commit on `master` +version. For example, after `v0.6.0-alpha.5`, an untagged commit on `main` may resolve to `0.6.0-alpha.6`. -`GitVersion.yml` intentionally gives the `master`/`main` branch the `alpha` +`GitVersion.yml` intentionally gives the `main`/`master` branch the `alpha` label so alpha tags are treated as exact version sources. Do not remove that label unless the release train stops using alpha tags. diff --git a/installer/appinstaller/README.md b/installer/appinstaller/README.md index 42433fb94..4577886ed 100644 --- a/installer/appinstaller/README.md +++ b/installer/appinstaller/README.md @@ -5,8 +5,8 @@ Companion MSIX channel. Installed MSIX packages poll these architecture-specific raw GitHub URLs: -- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller` -- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-arm64.appinstaller` +- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-x64.appinstaller` +- `https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-arm64.appinstaller` The checked-in feed files are bootstrap placeholders at version `0.0.0.0` so the raw URLs exist before the first signed MSIX embeds them. End users never @@ -15,7 +15,7 @@ background after the user installs from a real release. ## Release flow -Release builds do **not** push these files directly to `master`. After a +Release builds do **not** push these files directly to `main`. After a successful stable release tag: 1. `.github/workflows/appinstaller-feed-pr.yml` is triggered (manually via @@ -24,7 +24,7 @@ successful stable release tag: signed `.msix` release assets via `scripts/render-appinstaller.ps1`. 3. The rendered files are validated via `scripts/validate-appinstaller-hosting.ps1` against the hosted GitHub release assets. -4. A pull request is opened against `master` with the regenerated XML. +4. A pull request is opened against `main` with the regenerated XML. 5. Merging the PR is the human gate that advances installed clients to the new version. diff --git a/installer/appinstaller/openclaw-arm64.appinstaller b/installer/appinstaller/openclaw-arm64.appinstaller index 982eb0717..d208640c3 100644 --- a/installer/appinstaller/openclaw-arm64.appinstaller +++ b/installer/appinstaller/openclaw-arm64.appinstaller @@ -2,7 +2,7 @@ + Uri="https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-arm64.appinstaller"> + Uri="https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-x64.appinstaller"> diff --git a/scripts/validate-appinstaller-hosting.ps1 b/scripts/validate-appinstaller-hosting.ps1 index 6d72d978d..ae508036e 100644 --- a/scripts/validate-appinstaller-hosting.ps1 +++ b/scripts/validate-appinstaller-hosting.ps1 @@ -15,7 +15,7 @@ .PARAMETER AppInstallerUri Stable hosted .appinstaller URL, e.g. - https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller. + https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-x64.appinstaller. .PARAMETER MsixUri Optional MSIX URL. When omitted, the script fetches AppInstallerUri (or @@ -24,7 +24,7 @@ .PARAMETER AppInstallerPath Optional local .appinstaller file to parse instead of fetching AppInstallerUri. Used by the feed-update PR workflow before the rendered file is merged to - master at the stable raw GitHub location. + main at the stable raw GitHub location. .PARAMETER AllowGitHubContentTypes Compatibility switch for GitHub-hosted release assets. GitHub release @@ -34,7 +34,7 @@ .EXAMPLE ./scripts/validate-appinstaller-hosting.ps1 ` - -AppInstallerUri https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/openclaw-x64.appinstaller ` + -AppInstallerUri https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/openclaw-x64.appinstaller ` -AllowGitHubContentTypes #> diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs index 1f6108597..6a475927a 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs @@ -1739,7 +1739,7 @@ private static string ActionInProgressLabel(WslGatewayControlAction action) /// /// Handler for both the Welcome and Cockpit "Install local WSL gateway" /// buttons. Hands off to the hub's OpenSetupAction (which the App wires - /// to the V2 onboarding flow) — same wiring master added on the legacy + /// to the V2 onboarding flow) — same wiring main added on the legacy /// ConnectionPage; the cards live in different slots in the rebuilt /// page but the user-facing behavior is identical. /// diff --git a/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs index 5575618db..29c3c04d2 100644 --- a/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs +++ b/tests/OpenClaw.Tray.Tests/AppInstallerTemplateAssertionTests.cs @@ -128,7 +128,7 @@ public void StableFeedFiles_ExistAsBootstrapPlaceholders() var doc = XDocument.Load(path); XNamespace ns = "http://schemas.microsoft.com/appx/appinstaller/2018"; Assert.Equal("0.0.0.0", (string?)doc.Root!.Attribute("Version")); - Assert.Equal($"https://raw.githubusercontent.com/openclaw/openclaw-windows-node/master/installer/appinstaller/{fileName}", + Assert.Equal($"https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/{fileName}", (string?)doc.Root.Attribute("Uri")); var mainPackage = doc.Descendants(ns + "MainPackage").Single(); @@ -182,7 +182,7 @@ public void FeedUpdateWorkflow_OpensMaintainerPrAndBlocksPrereleaseFeeds() Assert.Contains("openclaw-x64.appinstaller", workflow); Assert.Contains("openclaw-arm64.appinstaller", workflow); Assert.Contains("gh pr create", workflow); - Assert.Contains("--base master", workflow); + Assert.Contains("--base main", workflow); Assert.Contains("validate-appinstaller-hosting.ps1", workflow); Assert.Contains("-AllowGitHubContentTypes", workflow); Assert.Contains("OpenClaw.Companion_${version}_x64.msix", workflow); diff --git a/tests/OpenClaw.Tray.Tests/LocalizationValidationTests.cs b/tests/OpenClaw.Tray.Tests/LocalizationValidationTests.cs index 35ba0f704..060190d66 100644 --- a/tests/OpenClaw.Tray.Tests/LocalizationValidationTests.cs +++ b/tests/OpenClaw.Tray.Tests/LocalizationValidationTests.cs @@ -198,7 +198,7 @@ public class LocalizationValidationTests "ConfigPage_ConfigUnavailable", "ConfigPage_ConfigIsReadOnly", // ConnectionPage gateway terminal controls — surfaced after PR #597 - // landed in master. Seeded English-only across all 5 locales using the + // landed in main. Seeded English-only across all 5 locales using the // same deferred-translation pattern as the AgentEventsPage / SkillsPage // / CronPage entries above. The Description_Format key takes the WSL // distro name as {0} and is formatted in ConnectionPage.xaml.cs. From 98f03c77034937f0e4ec01f0a7550c3e5d249383 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 12:12:41 -0700 Subject: [PATCH 15/38] ci(msix): run GitVersion in build-msix so MSIX payload carries SemVer build-msix invokes VS MSBuild without first running GitVersion, so the GitVersion.MsBuild package can't compute version metadata and the WinUI .dll inside the MSIX falls back to AssemblyVersion 1.0.0. The Verify MSIX Package Contents step then correctly fails: MSIX-internal ProductVersion '1.0.0' did not match GitVersion SemVer '0.6.4-PullRequest732.54'. Mirror the gitversion/setup + gitversion/execute steps from the test job before Build MSIX Package. The execute action exports GitVersion_* environment variables that GitVersion.MsBuild picks up to inject Assembly/File/InformationalVersion, satisfying the verify assertion. fetch-depth: 0 is already set on the checkout, so GitVersion can read git history. The existing manifest-patch step (which sets Appx package identity version) is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c153f47b..dc0206914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -396,6 +396,15 @@ jobs: - name: Setup MSBuild uses: microsoft/setup-msbuild@v3 + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v4 + with: + versionSpec: '6.4.x' + + - name: Determine Version + id: gitversion + uses: gittools/actions/gitversion/execute@v4 + - name: Restore run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} From c4f5d62ec4cb3ba8f4a5b84e85b9083d2c4f56e0 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 12:14:41 -0700 Subject: [PATCH 16/38] ci(msix): TEMP unblock build-msix from needs:[test, e2etests] for fast PR iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-msix only consumed needs.test.outputs.{semVer,majorMinorPatch}, which are now produced locally by the gitversion/execute step inside the job. e2etests was a pure gate (no outputs). Swap manifest-patch + verify references from needs.test.outputs.* to steps.gitversion.outputs.*, then drop the needs:[] list so build-msix starts in parallel with test/e2etests on every push. REVERT before merge — release job still gates on all four (line 491), so production releases are unaffected by this temporary change; the only effect is faster PR feedback. Restore 'needs: [test, e2etests]' on build-msix and switch the two version refs back to needs.test.outputs.*. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc0206914..ddc7b5095 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,7 +355,9 @@ jobs: if-no-files-found: warn build-msix: - needs: [test, e2etests] + # TEMP: needs:[test, e2etests] removed for faster iteration on this PR. + # Restore before merging — release job depends on all four. + needs: [] runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }} strategy: fail-fast: false @@ -411,7 +413,7 @@ jobs: - name: Patch MSIX manifest metadata shell: pwsh run: | - $version = "${{ needs.test.outputs.majorMinorPatch }}.0" + $version = "${{ steps.gitversion.outputs.majorMinorPatch }}.0" $isAlpha = "${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, '-') }}" -eq "true" $identityName = if ($isAlpha) { "OpenClaw.Companion.Alpha" } else { "OpenClaw.Companion" } $displayName = if ($isAlpha) { "OpenClaw Companion Alpha" } else { "OpenClaw Companion" } @@ -465,7 +467,7 @@ jobs: if (-not (Test-Path $assemblyPath)) { throw "OpenClaw.Tray.WinUI.dll missing from MSIX payload at $assemblyPath" } - $expected = "${{ needs.test.outputs.semVer }}" + $expected = "${{ steps.gitversion.outputs.semVer }}" $metadata = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($assemblyPath) if ([string]::IsNullOrWhiteSpace($metadata.ProductVersion)) { throw "OpenClaw.Tray.WinUI.dll inside MSIX is missing ProductVersion metadata." From 91f6f3f26d19d02a848b63ae4660701ffbe73d38 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 12:44:46 -0700 Subject: [PATCH 17/38] ci(msix): use 'dotnet publish' so VC++ runtime is copied into the MSIX payload Test-ReleaseNativeDependencies.ps1 -RequireAppLocalVCRuntime fails after the MSIX-payload extract because vcruntime140.dll is missing next to libsodium.dll: Missing app-local vcruntime140.dll next to libsodium.dll. Root cause: the CopyOpenClawVCRuntime* targets in src/Directory.Build.targets only ship a current VS-resolved runtime via CopyOpenClawVCRuntimeToPublish (AfterTargets=Publish). The pre-fix CI step ran 'msbuild /t:Build', which only triggers CopyOpenClawVCRuntimeToOutput - x64-only AND sourced from the stale 14.29 NuGet that the comment explicitly warns is too old for onnxruntime >= 1.20. Worse, the WinUI single-project AppX packaging task collects payload from publish output, not from the build TargetDir, so even the stale copies wouldn't end up inside the MSIX. Switching to 'dotnet publish ... -p:PackageMsix=true' matches what build.ps1 does locally and what produces working MSIX packages on dev machines. publish triggers CopyOpenClawVCRuntimeToPublish (VS-install resolution -> current 14.40+ DLLs) and ValidateOpenClawVCRuntimePublished, which guarantees vcruntime140.dll lands in PublishDir before the AppX packager collects payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddc7b5095..34fe612ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -428,16 +428,17 @@ jobs: - name: Build MSIX Package run: > - msbuild src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj - /p:Configuration=Release - /p:RuntimeIdentifier=${{ matrix.rid }} - /p:Platform=${{ matrix.platform }} - /p:PackageMsix=true - /p:GenerateAppxPackageOnBuild=true - /p:AppxPackageSigningEnabled=false - /p:AppxBundle=Never - /p:UapAppxPackageBuildMode=SideloadOnly - /p:AppxPackageDir=AppPackages\ + dotnet publish src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj + -c Release + -r ${{ matrix.rid }} + --self-contained + -p:Platform=${{ matrix.platform }} + -p:PackageMsix=true + -p:GenerateAppxPackageOnBuild=true + -p:AppxPackageSigningEnabled=false + -p:AppxBundle=Never + -p:UapAppxPackageBuildMode=SideloadOnly + -p:AppxPackageDir=AppPackages\ - name: Find MSIX Package id: find-msix From b51787e2599e36e43294bc079d742c7ab7734cd6 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Tue, 9 Jun 2026 12:58:56 -0700 Subject: [PATCH 18/38] build(msix): register VC++ runtime as ResolvedFileToPublish so it lands in the MSIX Even after switching CI to 'dotnet publish ... -p:PackageMsix=true', the verify step still failed with: Missing app-local vcruntime140.dll next to libsodium.dll. Reproduced locally: publish dir had all VS-resolved VC++ runtime DLLs, but only libsodium.dll and vcruntime140_cor3.dll (the .NET self-contained one) made it into the .msix payload. Root cause: the WinUI single-project AppX packaging target collects payload from @(PackagingOutputs), which for publish builds is populated from PublishItemsOutputGroupOutput - i.e. the .NET SDK's ResolvedFileToPublish collection - NOT from arbitrary files sitting in PublishDir. The old CopyOpenClawVCRuntimeToPublish target ran AfterTargets='Publish' and copied files into PublishDir, which was enough for the loose-tray Inno pipeline (now retired) but invisible to the AppX packager. Fix: add AddOpenClawVCRuntimeToPublishItems target that runs BeforeTargets='ComputeFilesToPublish' and registers the resolved VC++ runtime files as ResolvedFileToPublish items. The .NET SDK publish flow then: 1. Copies them to PublishDir naturally (still covers any loose-publish use). 2. Emits them in PublishItemsOutputGroupOutput. 3. -> PackagingOutputs. 4. -> Inside the .msix. Also narrow VC Redist version selection. A VS install can carry side-by-side versions (e.g. 14.42 / 14.44 / 14.51). The previous glob 'MSVC\*\\Microsoft.VC*.CRT\vcruntime140*.dll' matched every version, which the .NET SDK rejects under publish with NETSDK1152 (duplicate publish output files with the same relative path). Now we resolve the single highest version directory once via vswhere + a powershell sort, then glob just that directory. Kept CopyOpenClawVCRuntimeToPublish and ValidateOpenClawVCRuntimePublished as defensive safety nets that fire after publish. Validated: - Local 'dotnet publish ... -p:PackageMsix=true' for win-x64 produced an .msix containing vcruntime140.dll v14.51.36231.0 (well above the 14.38 floor). - 'Test-ReleaseNativeDependencies.ps1 -RequireAppLocalVCRuntime' against the extracted MSIX payload now reports 'Release native dependency policy passed.' - ./build.ps1 succeeded. - Shared tests: 2130 passed / 29 skipped. - Tray tests: 975 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Directory.Build.targets | 48 +++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 515b25e14..bd5972415 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -44,6 +44,21 @@ + + + + + + + - - + + - - + + + Text="No $(OpenClawVCRuntimeArch) VC++ Runtime DLLs were found under '$(_OpenClawVCRedistVersionRoot)\$(OpenClawVCRuntimeArch)\Microsoft.VC*.CRT\'. Install the C++ Redistributable Update Visual Studio component." /> @@ -98,6 +113,29 @@ SkipUnchangedFiles="true" /> + + + + + %(Filename)%(Extension) + PreserveNewest + + + + From 2516bd38923463dd5f997eb954c7658d7288549f Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 09:46:18 -0700 Subject: [PATCH 19/38] build(msix): version MSIX from GitVersion instead of the SDK default 1.0.0 Every .msix produced by 'build.ps1 -PackageMsix' and the CI build-msix job shipped tagged as 1.0.0.0 (Identity/@Version) regardless of the real release version, even though the assembly's AssemblyInformationalVersion was correct. Two cooperating bugs in the GitVersion -> MSIX chain: 1. GitVersion.MsBuild only updates the MSBuild Version property when the opt-in true is set (GitVersion.MsBuild.targets line 72). The repo never opted in, so Version stayed at the .NET SDK default '1.0.0'. 2. GitVersion's GetVersion target uses BeforeTargets=GitVersionTargetsBefore, which is empty in this repo. That means GetVersion never runs as an implicit side effect of any standard target - including BeforeBuild. SyncAppxManifestVersionTarget was hooked to BeforeBuild and read Version, so even if (1) were fixed, it would still read '1.0.0' because GetVersion had not run yet. (Some downstream target chain does eventually pull GetVersion in - that is why the produced DLL's AssemblyInformationalVersion matched GitVersion. But by then the manifest is already patched and packaged.) Fix: - Set UpdateVersionProperties=true in src/Directory.Build.props so GetVersion writes Version = GitVersion_SemVer. - Add DependsOnTargets=GetVersion to SyncAppxManifestVersionTarget so GetVersion runs first and Version is populated before the manifest is patched. After the fix, local dotnet publish -p:PackageMsix=true produces OpenClaw.Tray.WinUI_0.6.4.0_x64.msix with Identity/@Version=0.6.4.0 and the DLL's ProductVersion matching GitVersion_SemVer exactly, which is what the build-msix CI verify step asserts. Validated: - ./build.ps1 succeeded. - Shared tests: 2130 passed / 29 skipped. - Tray tests: 975 passed. - Local MSIX build (-p:PackageMsix=true) produced 0.6.4.0 file + manifest. - Test-ReleaseNativeDependencies.ps1 -RequireAppLocalVCRuntime against the extracted MSIX payload still reports 'Release native dependency policy passed.' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Directory.Build.props | 12 ++++++++++++ src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 30fd2cd1a..20e91bd8b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,6 +10,18 @@ all + + + true diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 162a9dbc3..46c860c37 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -193,7 +193,17 @@ - + + From 68dac18da9841f3cd42c8e362b69c0722c029362 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 10:19:38 -0700 Subject: [PATCH 20/38] ci(msix): TEMP disable GitHub Release creation; upload signed MSIX as artifact instead Lets us validate the Azure Trusted Signing leg of the release job on a throwaway alpha tag without publishing a public GitHub Release. The Create Release step (softprops/action-gh-release@v3) is left commented in place so the revert is a trivial uncomment+delete of the two new upload-artifact steps. Signal flow during the test: push tag v0.6.4-alpha.testN -> repo-hygiene / test / e2etests / build-msix -> release job: download unsigned MSIX artifacts, azure/login, sign with azure/artifact-signing-action@v2, upload signed MSIX as openclaw-msix-signed-win-{x64,arm64} artifacts. Must be reverted before merging to main. Tracked in session todo revert-release-create-temp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 79 +++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34fe612ff..a9da15d57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -554,33 +554,54 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - - name: Create Release - uses: softprops/action-gh-release@v3 + # TEMP: GitHub Release creation disabled so we can validate the Azure + # Trusted Signing pipeline on a throwaway tag without publishing a public + # Release. Signed MSIX artifacts are surfaced as workflow artifacts only. + # Restore the "Create Release" step below before merging this branch. + - name: Upload Signed x64 MSIX (TEMP - signing pipeline test) + uses: actions/upload-artifact@v7 + with: + name: openclaw-msix-signed-win-x64 + path: artifacts/msix-win-x64/*.msix + if-no-files-found: error + overwrite: true + + - name: Upload Signed ARM64 MSIX (TEMP - signing pipeline test) + uses: actions/upload-artifact@v7 with: - generate_release_notes: true - prerelease: ${{ contains(github.ref_name, '-') }} - make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }} - files: | - artifacts/msix-win-x64/*.msix - artifacts/msix-win-arm64/*.msix - body: | - ## OpenClaw Windows Hub ${{ github.ref_name }} - - ### Install - - OpenClaw is distributed as an MSIX package via Windows AppInstaller. - Use the install links in the [README](https://github.com/openclaw/openclaw-windows-node#install) - for the recommended path — Windows handles silent auto-updates from there. - - The `.msix` files attached below are the underlying signed packages. - Advanced users can install them directly with `Add-AppxPackage `. - - ### Features - - 🦞 System tray integration with gateway status - - ✅ Code-signed with Azure Artifact Signing - - 🔄 Silent auto-updates via Windows AppInstaller - - ### Requirements - - Windows 10 version 1903 or later - - [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) - - OpenClaw gateway running locally + name: openclaw-msix-signed-win-arm64 + path: artifacts/msix-win-arm64/*.msix + if-no-files-found: error + overwrite: true + + # TEMP REVERT: re-enable this step before merging. + # - name: Create Release + # uses: softprops/action-gh-release@v3 + # with: + # generate_release_notes: true + # prerelease: ${{ contains(github.ref_name, '-') }} + # make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }} + # files: | + # artifacts/msix-win-x64/*.msix + # artifacts/msix-win-arm64/*.msix + # body: | + # ## OpenClaw Windows Hub ${{ github.ref_name }} + # + # ### Install + # + # OpenClaw is distributed as an MSIX package via Windows AppInstaller. + # Use the install links in the [README](https://github.com/openclaw/openclaw-windows-node#install) + # for the recommended path — Windows handles silent auto-updates from there. + # + # The `.msix` files attached below are the underlying signed packages. + # Advanced users can install them directly with `Add-AppxPackage `. + # + # ### Features + # - 🦞 System tray integration with gateway status + # - ✅ Code-signed with Azure Artifact Signing + # - 🔄 Silent auto-updates via Windows AppInstaller + # + # ### Requirements + # - Windows 10 version 1903 or later + # - [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) + # - OpenClaw gateway running locally From 9f195d68f293ca5e6a0bd93d26c7f90ae3af1c9d Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 10:30:44 -0700 Subject: [PATCH 21/38] ci(msix): TEMP skip Verify tag version output for msixtest signing rehearsal tags The Verify tag version output step asserts GitVersion's computed SemVer exactly matches the tag minus its 'v' prefix. GitVersion only honors a tag as an exact version source when the tag's prerelease label is compatible with the branch's auto-derived label. On user/kmahone/msix the branch label is user-kmahone-msix, so v0.6.4-msixtest.1 is silently ignored and GitVersion falls back to 0.6.4-user-kmahone-msix.1. For a signing-pipeline rehearsal we do not care about strict SemVer/tag agreement - the MSIX-internal ProductVersion comes from GitVersion regardless, and the msixtest token is only there to make the tag/artifact name self-describing. Skip the verify step when the tag name contains 'msixtest'. Tracked in session todo revert-verify-tag-msixtest-skip; restore the unconditional gate before merging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9da15d57..1ae3b88a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,12 @@ jobs: uses: gittools/actions/gitversion/execute@v4 - name: Verify tag version output - if: startsWith(github.ref, 'refs/tags/v') + # TEMP: skip the strict tag/SemVer match for msixtest signing-pipeline + # rehearsal tags. GitVersion ignores the tag's prerelease label when it + # does not match the branch's auto-derived label, so on a feature branch + # `v0.6.4-msixtest.1` resolves to `0.6.4-.`. Revert + # the contains-check below before merging this branch to main. + if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, 'msixtest') shell: pwsh run: | $expected = "${{ github.ref_name }}" -replace '^v', '' From 9777606606dc7aad7f04f3edb0ae17cc2e1b4d26 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 11:33:34 -0700 Subject: [PATCH 22/38] ci(msix): TEMP AppInstaller silent-update rehearsal infrastructure Lets us validate the end-to-end MSIX path on a feature branch without disturbing the production AppInstaller feed: push tag vX.Y.Z-msixtest.N -> CI builds signs and publishes a prerelease GitHub Release -> branch-only openclaw-msixtest-x64.appinstaller feed points at it -> ms-appinstaller: link installs, raw.githubusercontent.com feed bump triggers silent auto-update on subsequent Add-AppxPackage poll. Changes: 1. ci.yml build-msix Patch MSIX manifest metadata step: when the tag matches ^v\d+\.\d+\.\d+-msixtest\.(\d+)$, set Identity/@Version=X.Y.Z.N (using the tag's prerelease counter as the 4th part). Without this, successive msixtest tags would all map to X.Y.Z.0 and AppInstaller would no-op the second install. 2. ci.yml build-msix Build MSIX Package step: pass -p:Version=X.Y.Z.N when the override is active so SyncAppxManifestVersionTarget does not clobber the manifest version back to GitVersion's SemVer-derived .0 default. 3. ci.yml release job: for msixtest tags, publish a real (prerelease, not-latest) GitHub Release so AppInstaller can fetch the signed .msix from a stable HTTPS URL (workflow artifacts require auth and cannot be used). Non-msixtest tags still skip Release creation (TEMP carryover); the original Create Release step is left commented in place for a clean revert. 4. New installer/appinstaller/openclaw-msixtest-x64.appinstaller. Branch-only test feed, pinned to v0.6.4-msixtest.3 initially. Self-Uri points at the raw.githubusercontent.com URL on user/kmahone/msix. OnLaunch is set to HoursBetweenUpdateChecks=0 UpdateBlocksActivation=true ShowPrompt=true to make the rehearsal visible and synchronous; the production feed will keep ShowPrompt=false. All TEMP edits and the new feed file must be reverted before merging to main. Tracked in session todos revert-msixtest-version-override, revert-msixtest-prerelease-create, and delete-msixtest-appinstaller-file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 73 +++++++++++++++++-- .../openclaw-msixtest-x64.appinstaller | 51 +++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 installer/appinstaller/openclaw-msixtest-x64.appinstaller diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ae3b88a4..cbe073ce6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -416,12 +416,34 @@ jobs: run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} - name: Patch MSIX manifest metadata + id: patch-manifest shell: pwsh run: | $version = "${{ steps.gitversion.outputs.majorMinorPatch }}.0" $isAlpha = "${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, '-') }}" -eq "true" $identityName = if ($isAlpha) { "OpenClaw.Companion.Alpha" } else { "OpenClaw.Companion" } $displayName = if ($isAlpha) { "OpenClaw Companion Alpha" } else { "OpenClaw Companion" } + + # TEMP: for msixtest signing/AppInstaller rehearsal tags, use the tag's + # prerelease counter (the .N in v0.6.4-msixtest.N) as the 4th version + # part so successive test tags produce monotonically increasing MSIX + # Identity/@Version values (e.g. 0.6.4.3 then 0.6.4.4). Without this, + # both tags would resolve to 0.6.4.0 and AppInstaller would no-op the + # second install. Remove this block when the msixtest tag-verify TEMP + # skip in the test job is reverted. + $msixtestVersion = "" + $refName = "${{ github.ref_name }}" + if ($refName -match '^v\d+\.\d+\.\d+-msixtest\.(\d+)$') { + $msixtestVersion = "${{ steps.gitversion.outputs.majorMinorPatch }}.$([int]$Matches[1])" + $version = $msixtestVersion + Write-Host "TEMP msixtest override: MSIX Identity/@Version = $version" + } + # Exported so the Build step can pass -p:Version=; required because + # SyncAppxManifestVersionTarget (OpenClaw.Tray.WinUI.csproj) re-derives + # the manifest Identity/@Version from $(Version)=$(GitVersion_SemVer) + # at build time and would otherwise clobber our .N override back to .0. + "msixtest_version=$msixtestVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + $manifest = "src/OpenClaw.Tray.WinUI/Package.appxmanifest" [xml]$xml = Get-Content $manifest $xml.Package.Identity.Name = $identityName @@ -432,6 +454,11 @@ jobs: Write-Host "Patched MSIX manifest to identity $identityName, display name '$displayName', version $version" - name: Build MSIX Package + # TEMP: the trailing format(...) appends -p:Version=0.6.4.N only for + # msixtest rehearsal tags so SyncAppxManifestVersionTarget writes the + # same 4-part literal back into the manifest at build time. Otherwise + # SyncAppxManifestVersionTarget would derive 0.6.4.0 from GitVersion's + # SemVer and clobber the Patch step's override. run: > dotnet publish src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj -c Release @@ -444,6 +471,7 @@ jobs: -p:AppxBundle=Never -p:UapAppxPackageBuildMode=SideloadOnly -p:AppxPackageDir=AppPackages\ + ${{ steps.patch-manifest.outputs.msixtest_version && format('-p:Version={0}', steps.patch-manifest.outputs.msixtest_version) || '' }} - name: Find MSIX Package id: find-msix @@ -559,11 +587,17 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - # TEMP: GitHub Release creation disabled so we can validate the Azure - # Trusted Signing pipeline on a throwaway tag without publishing a public - # Release. Signed MSIX artifacts are surfaced as workflow artifacts only. - # Restore the "Create Release" step below before merging this branch. - - name: Upload Signed x64 MSIX (TEMP - signing pipeline test) + # TEMP: GitHub Release creation is constrained while we validate the + # signing + AppInstaller auto-update pipeline. + # - For msixtest rehearsal tags (vX.Y.Z-msixtest.N): publish a real + # prerelease GitHub Release so AppInstaller can fetch the signed .msix + # from a stable HTTPS URL. AppInstaller cannot read GitHub workflow + # artifacts (they require auth). + # - For any other tag: do NOT publish a Release; just upload as workflow + # artifacts. We do not want to publish a production Release from this + # feature branch by mistake. + # Restore the unconditional "Create Release" behavior before merging. + - name: Upload Signed x64 MSIX (TEMP) uses: actions/upload-artifact@v7 with: name: openclaw-msix-signed-win-x64 @@ -571,7 +605,7 @@ jobs: if-no-files-found: error overwrite: true - - name: Upload Signed ARM64 MSIX (TEMP - signing pipeline test) + - name: Upload Signed ARM64 MSIX (TEMP) uses: actions/upload-artifact@v7 with: name: openclaw-msix-signed-win-arm64 @@ -579,7 +613,32 @@ jobs: if-no-files-found: error overwrite: true - # TEMP REVERT: re-enable this step before merging. + - name: Create Prerelease (TEMP - msixtest tags only) + if: ${{ contains(github.ref_name, 'msixtest') }} + uses: softprops/action-gh-release@v3 + with: + prerelease: true + make_latest: false + files: | + artifacts/msix-win-x64/*.msix + artifacts/msix-win-arm64/*.msix + body: | + ## OpenClaw Windows Hub ${{ github.ref_name }} + + **THROWAWAY TEST RELEASE — DO NOT INSTALL.** + + This release exists only to validate the Azure Trusted Signing leg + of the release pipeline and the AppInstaller silent-update flow on + the `user/kmahone/msix` branch. It will be deleted along with its + tag once testing is complete. + + The signed `.msix` files attached below are addressed by the + temporary AppInstaller feed at + `installer/appinstaller/openclaw-msixtest-x64.appinstaller` on the + branch. + + # TEMP REVERT: re-enable this step (and remove the two TEMP blocks above) + # before merging. # - name: Create Release # uses: softprops/action-gh-release@v3 # with: diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller new file mode 100644 index 000000000..ae504c147 --- /dev/null +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -0,0 +1,51 @@ + + + + + + + + + + + From f4e29dd646aa0661f050f07d89cdd8858aacac8d Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 11:55:10 -0700 Subject: [PATCH 23/38] ci(msix): pivot rehearsal to SemVer-patch bump (0.6.4 -> 0.6.5), drop 4th-part override The previous attempt to encode the rehearsal counter in the MSIX Identity's 4th version part (X.Y.Z.N) did not produce the expected MSIX version in CI - run for v0.6.4-msixtest.3 still emitted OpenClaw.Tray.WinUI_0.6.4.0_x64.msix instead of _0.6.4.3_. Rather than debug that path, switch to bumping the SemVer patch part for the rehearsal: v0.6.4-msixtest.3 -> MSIX 0.6.4.0 (install rehearsal) v0.6.5-msixtest.1 -> MSIX 0.6.5.0 (update rehearsal) This uses the existing majorMinorPatch + .0 logic in the Patch MSIX manifest metadata step, no overrides needed. The msixtest counter is now only there to differentiate rehearsal runs of the same 3-part SemVer. Changes: - ci.yml: drop TEMP msixtest 4th-part override in Patch MSIX manifest metadata step, drop the id: patch-manifest output, drop the conditional -p:Version= in Build MSIX Package step. revert-msixtest-version-override todo is now obsolete (marked done in session SQL). - openclaw-msixtest-x64.appinstaller: re-pin Version and MainPackage Version to 0.6.4.0, MainPackage Uri to the existing v0.6.4-msixtest.3 release asset OpenClaw.Tray.WinUI_0.6.4.0_x64.msix. Header comment updated with the new bump procedure. Reuses the existing v0.6.4-msixtest.3 release as the install-rehearsal source (it is already signed 0.6.4.0). Next push will be v0.6.5-msixtest.1 for the update rehearsal. Still TEMP-tracked for revert before merge: revert-msix-needs-temp (build-msix needs:[]) revert-verify-tag-msixtest-skip (Verify tag version output skip) revert-msixtest-prerelease-create (release-job msixtest-only prerelease) delete-msixtest-appinstaller-file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 28 -------------- .../openclaw-msixtest-x64.appinstaller | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbe073ce6..910248306 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -416,34 +416,12 @@ jobs: run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }} - name: Patch MSIX manifest metadata - id: patch-manifest shell: pwsh run: | $version = "${{ steps.gitversion.outputs.majorMinorPatch }}.0" $isAlpha = "${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, '-') }}" -eq "true" $identityName = if ($isAlpha) { "OpenClaw.Companion.Alpha" } else { "OpenClaw.Companion" } $displayName = if ($isAlpha) { "OpenClaw Companion Alpha" } else { "OpenClaw Companion" } - - # TEMP: for msixtest signing/AppInstaller rehearsal tags, use the tag's - # prerelease counter (the .N in v0.6.4-msixtest.N) as the 4th version - # part so successive test tags produce monotonically increasing MSIX - # Identity/@Version values (e.g. 0.6.4.3 then 0.6.4.4). Without this, - # both tags would resolve to 0.6.4.0 and AppInstaller would no-op the - # second install. Remove this block when the msixtest tag-verify TEMP - # skip in the test job is reverted. - $msixtestVersion = "" - $refName = "${{ github.ref_name }}" - if ($refName -match '^v\d+\.\d+\.\d+-msixtest\.(\d+)$') { - $msixtestVersion = "${{ steps.gitversion.outputs.majorMinorPatch }}.$([int]$Matches[1])" - $version = $msixtestVersion - Write-Host "TEMP msixtest override: MSIX Identity/@Version = $version" - } - # Exported so the Build step can pass -p:Version=; required because - # SyncAppxManifestVersionTarget (OpenClaw.Tray.WinUI.csproj) re-derives - # the manifest Identity/@Version from $(Version)=$(GitVersion_SemVer) - # at build time and would otherwise clobber our .N override back to .0. - "msixtest_version=$msixtestVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - $manifest = "src/OpenClaw.Tray.WinUI/Package.appxmanifest" [xml]$xml = Get-Content $manifest $xml.Package.Identity.Name = $identityName @@ -454,11 +432,6 @@ jobs: Write-Host "Patched MSIX manifest to identity $identityName, display name '$displayName', version $version" - name: Build MSIX Package - # TEMP: the trailing format(...) appends -p:Version=0.6.4.N only for - # msixtest rehearsal tags so SyncAppxManifestVersionTarget writes the - # same 4-part literal back into the manifest at build time. Otherwise - # SyncAppxManifestVersionTarget would derive 0.6.4.0 from GitVersion's - # SemVer and clobber the Patch step's override. run: > dotnet publish src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj -c Release @@ -471,7 +444,6 @@ jobs: -p:AppxBundle=Never -p:UapAppxPackageBuildMode=SideloadOnly -p:AppxPackageDir=AppPackages\ - ${{ steps.patch-manifest.outputs.msixtest_version && format('-p:Version={0}', steps.patch-manifest.outputs.msixtest_version) || '' }} - name: Find MSIX Package id: find-msix diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller index ae504c147..75d471b2c 100644 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -4,18 +4,28 @@ AppInstaller silent-update on the user/kmahone/msix branch. Delete this file before merging the branch to main. + Version scheme: rehearsal bumps the PATCH part of the SemVer in the tag + (0.6.4 -> 0.6.5 -> ...) because GitVersion+SemVer tags are only 3 parts. + The MSIX Identity/@Version comes from build-msix Patch step as + majorMinorPatch + ".0" (so v0.6.4-msixtest.N -> 0.6.4.0). The trailing + `-msixtest.N` counter only differentiates rehearsal runs of the same + 3-part version; we usually push a new patch number per rehearsal. + How to use: - 1. Push the FIRST signing-test tag, e.g. `git push origin v0.6.4-msixtest.3`. - CI publishes a prerelease GitHub Release with the signed MSIX named - `OpenClaw.Tray.WinUI_0.6.4.3_x64.msix`. - 2. Install via ms-appinstaller link: + 1. Install rehearsal. Pin Version below to 0.6.4.0 (the existing + v0.6.4-msixtest.3 release asset is the install source). Then: start ms-appinstaller:?source=https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-msixtest-x64.appinstaller - 3. To rehearse the auto-update path: bump the two Version="0.6.4.N" - attributes below and the MainPackage/@Uri release-tag segment to N+1, - commit the edit to this branch, then push a new tag - `v0.6.4-msixtest.`. After CI publishes the new release, run: - Add-AppxPackage -AppInstallerFile https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-msixtest-x64.appinstaller -ForceApplicationShutdown - Then verify the installed Identity Version advanced: + 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.5-msixtest.1) and + wait for CI to publish its release. Bump all 4 placeholders below: + + + Commit + push the edit. Then either: + a. Quit the installed Alpha tray and relaunch from Start menu — the + prompt should appear, click Update. + b. Or force from PowerShell: + Add-AppxPackage -AppInstallerFile -ForceApplicationShutdown + Verify the installed Identity Version advanced: Get-AppxPackage OpenClaw.Companion.Alpha | Select Name, Version Identity Name is OpenClaw.Companion.Alpha (msixtest tags contain '-', so the @@ -34,18 +44,19 @@ --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.4-msixtest.3/OpenClaw.Tray.WinUI_0.6.4.0_x64.msix" /> + From 8d40aaa7e4f3a451ec51ba05c1da48989e00caad Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Wed, 10 Jun 2026 12:53:28 -0700 Subject: [PATCH 24/38] msixtest: bump AppInstaller schema to 2021 to fix LogHr 8007000D "data is invalid" on install The 2018 schema can choke on validators in newer Windows AppInstaller builds. Switching to the 2021 namespace makes Windows use the newer parser, which accepts the same element set we already use (MainPackage, OnLaunch, AutomaticBackgroundTask). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- installer/appinstaller/openclaw-msixtest-x64.appinstaller | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller index 75d471b2c..c8f061e9d 100644 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -43,7 +43,7 @@ keep this false for silent tray-app updates). --> From 81eed1bb632e737ccd007af886a3a17e78bf7ac5 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Thu, 11 Jun 2026 12:39:51 -0700 Subject: [PATCH 25/38] ci(msix): TEMP embed AppInstaller in MSIX payload to bypass external-feed install issues Local-file install of the standalone openclaw-msixtest-x64.appinstaller fails with LogHr 8007000D on the second-pass parse after AppInstaller refetches from raw.githubusercontent.com. Two underlying gaps in the external-feed approach we discovered: 1. raw.githubusercontent.com serves .appinstaller as text/plain instead of application/appinstaller. AppInstaller's strict parser rejects the redirected fetch. 2. ms-appinstaller://?source= protocol has been disabled by default on consumer devices since December 2023. Docs: https://learn.microsoft.com/en-us/windows/msix/app-installer/installing-windows10-apps-web https://learn.microsoft.com/en-us/windows/msix/app-installer/how-to-embed-an-appinstaller-file Embedded approach sidesteps both: ship openclaw-update.appinstaller inside the MSIX payload, reference it from the manifest's . Install path becomes 'download MSIX, double-click' - the .appinstaller is parsed locally from the extracted payload, no HTTP fetch, no MIME issue. Windows registers the embedded file's as the canonical update-poll endpoint for the background update task. Changes: - src/OpenClaw.Tray.WinUI/Package.appxmanifest: add xmlns:uap13, add uap13 to IgnorableNamespaces, add in . - src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller: new file, identical content to the on-disk installer/appinstaller/openclaw-msixtest-x64.appinstaller for the rehearsal (pinned to v0.6.4-msixtest.3 release, 0.6.4.0). - src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj: add so the file ships in the MSIX payload. Local validation: - dotnet publish -p:PackageMsix=true succeeds. - MSIX payload contains openclaw-update.appinstaller (1270 bytes). - AppxManifest.xml carries the uap13:AutoUpdate Properties block. - Add-AppxPackage installed OpenClaw.Companion 0.6.4.0 in 406 ms with no schema errors - accepted by the Windows AppX deployment engine. Still TEMP-tracked for revert before merge: revert-embedded-appinstaller-manifest revert-embedded-appinstaller-content ... plus the earlier msixtest reverts. Not yet known: whether the background update poll (after install) hits the same raw.githubusercontent.com MIME issue as the install-time parse. That's the next thing to test once a higher-version MSIX is published. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OpenClaw.Tray.WinUI.csproj | 6 +++++ src/OpenClaw.Tray.WinUI/Package.appxmanifest | 14 +++++++++-- .../openclaw-update.appinstaller | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 46c860c37..307ba1041 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -97,6 +97,12 @@ + + diff --git a/src/OpenClaw.Tray.WinUI/Package.appxmanifest b/src/OpenClaw.Tray.WinUI/Package.appxmanifest index de2197b55..e753d203d 100644 --- a/src/OpenClaw.Tray.WinUI/Package.appxmanifest +++ b/src/OpenClaw.Tray.WinUI/Package.appxmanifest @@ -3,10 +3,11 @@ + IgnorableNamespaces="uap uap13 desktop com rescap"> + + + diff --git a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller new file mode 100644 index 000000000..e7e0bb8e5 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller @@ -0,0 +1,24 @@ + + + + + + + + + + + From e9de9698f1a1503dc35bc9a1adcee9a96c78ca4e Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Thu, 11 Jun 2026 12:57:50 -0700 Subject: [PATCH 26/38] msixtest: bump rehearsal feeds to 0.6.5.0 / v0.6.5-msixtest.1 for update rehearsal Phase 2 of the embedded-AppInstaller rehearsal. With v0.6.4-msixtest.4 installed, bump both feed copies to 0.6.5.0 pointing at the next test release. Order matters - push feed bump first so raw.githubusercontent.com is already serving the bumped XML by the time CI publishes the new MSIX release; otherwise the OnLaunch poll would see the new XML and 404 on MainPackage Uri. - installer/appinstaller/openclaw-msixtest-x64.appinstaller: what raw.githubusercontent.com serves to the update poller. This is the URL Windows registered at install time via the embedded copy. - src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller: kept in lockstep for consistency; only affects future installs (existing installs read Uri once at install time, then poll that URL forever). Next: push tag v0.6.5-msixtest.1 to produce the matching release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- installer/appinstaller/openclaw-msixtest-x64.appinstaller | 8 ++++---- src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller index c8f061e9d..854550c38 100644 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -13,7 +13,7 @@ How to use: 1. Install rehearsal. Pin Version below to 0.6.4.0 (the existing - v0.6.4-msixtest.3 release asset is the install source). Then: + v0.6.5-msixtest.1 release asset is the install source). Then: start ms-appinstaller:?source=https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-msixtest-x64.appinstaller 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.5-msixtest.1) and wait for CI to publish its release. Bump all 4 placeholders below: @@ -44,15 +44,15 @@ --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.5-msixtest.1/OpenClaw.Tray.WinUI_0.6.5.0_x64.msix" /> diff --git a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller index e7e0bb8e5..5c563c76f 100644 --- a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller +++ b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller @@ -7,15 +7,15 @@ time to know what URL to poll). --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.5-msixtest.1/OpenClaw.Tray.WinUI_0.6.5.0_x64.msix" /> From 441e19ce2817b664b0e71cc657412a35d3dc5d22 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Thu, 11 Jun 2026 14:17:04 -0700 Subject: [PATCH 27/38] msixtest: bump rehearsal feeds to 0.6.6 with UpdateBlocksActivation=false Phase 3 of the embedded-AppInstaller rehearsal: test non-blocking update behavior. ShowPrompt stays true (per user preference) so the update dialog still surfaces; UpdateBlocksActivation=false means the app launches immediately with the OLD version while the update stages in the background. Quit and relaunch applies the new version. The currently-installed 0.6.5.0 has embedded UpdateBlocksActivation=true, so it will use blocking behavior to install 0.6.6.0 (one more time). 0.6.6.0 has the new non-blocking settings embedded. A subsequent bump (Phase 4: 0.6.7-msixtest.1) is then needed to exercise the non-blocking path from 0.6.6 to 0.6.7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../openclaw-msixtest-x64.appinstaller | 18 +++++++++--------- .../openclaw-update.appinstaller | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller index 854550c38..72ec6a5fe 100644 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -13,13 +13,13 @@ How to use: 1. Install rehearsal. Pin Version below to 0.6.4.0 (the existing - v0.6.5-msixtest.1 release asset is the install source). Then: + v0.6.6-msixtest.1 release asset is the install source). Then: start ms-appinstaller:?source=https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-msixtest-x64.appinstaller - 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.5-msixtest.1) and + 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.6-msixtest.1) and wait for CI to publish its release. Bump all 4 placeholders below: - - + + Commit + push the edit. Then either: a. Quit the installed Alpha tray and relaunch from Start menu — the prompt should appear, click Update. @@ -44,18 +44,18 @@ --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.6-msixtest.1/OpenClaw.Tray.WinUI_0.6.6.0_x64.msix" /> - + diff --git a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller index 5c563c76f..94c01ce81 100644 --- a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller +++ b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller @@ -7,18 +7,18 @@ time to know what URL to poll). --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.6-msixtest.1/OpenClaw.Tray.WinUI_0.6.6.0_x64.msix" /> - + From 4df7d070b6df725d58ebe7294824b7a22d03eeda Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Thu, 11 Jun 2026 14:34:50 -0700 Subject: [PATCH 28/38] msixtest: bump rehearsal feeds to 0.6.7 to exercise non-blocking update from 0.6.6 --- .../openclaw-msixtest-x64.appinstaller | 16 ++++++++-------- .../openclaw-update.appinstaller | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller index 72ec6a5fe..3c5d1a505 100644 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ b/installer/appinstaller/openclaw-msixtest-x64.appinstaller @@ -13,13 +13,13 @@ How to use: 1. Install rehearsal. Pin Version below to 0.6.4.0 (the existing - v0.6.6-msixtest.1 release asset is the install source). Then: + v0.6.7-msixtest.1 release asset is the install source). Then: start ms-appinstaller:?source=https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-msixtest-x64.appinstaller - 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.6-msixtest.1) and + 2. Update rehearsal. Push a higher-patch tag (e.g. v0.6.7-msixtest.1) and wait for CI to publish its release. Bump all 4 placeholders below: - - + + Commit + push the edit. Then either: a. Quit the installed Alpha tray and relaunch from Start menu — the prompt should appear, click Update. @@ -44,15 +44,15 @@ --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.7-msixtest.1/OpenClaw.Tray.WinUI_0.6.7.0_x64.msix" /> diff --git a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller index 94c01ce81..021423dc8 100644 --- a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller +++ b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller @@ -7,15 +7,15 @@ time to know what URL to poll). --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.7-msixtest.1/OpenClaw.Tray.WinUI_0.6.7.0_x64.msix" /> From 4820e71754c858967a5072a2f2f1721fc06e7f33 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Thu, 11 Jun 2026 17:43:15 -0700 Subject: [PATCH 29/38] msix(channels): add Alpha polled-feed files and embedded-AppInstaller source files Commit 1 of the three-channel embedded-AppInstaller production rollout. Pure additions; no behavior changes yet. Four new committed XML files in installer/appinstaller/: - openclaw-alpha-x64.appinstaller, openclaw-alpha-arm64.appinstaller Polled-feed bootstrap files for the Alpha channel, mirroring the existing Stable bootstrap pattern (openclaw-{x64,arm64}.appinstaller). Version=0.0.0.0 placeholders; bumped by the operator-driven feed-bump workflow after each Alpha release. - openclaw-update.stable.appinstaller, openclaw-update.alpha.appinstaller Source files for the AppInstaller content shipped INSIDE each MSIX as openclaw-update.appinstaller (referenced from Package.appxmanifest's ). Hardcoded channel-specific identity + polled-feed URL; {{VERSION}}, {{ARCH}}, {{MAIN_PACKAGE_URI}} placeholders patched by MSBuild XmlPoke at build time in Commit 2. settings pinned to ShowPrompt=true / UpdateBlocksActivation=false / 24h interval per the locked-in design. See three-channel-msix-plan.md (session state) for the full design. Nothing in the build pipeline references these files yet; that wiring lands in Commit 2 alongside the csproj XmlPoke target and the manifest cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../openclaw-alpha-arm64.appinstaller | 17 ++++++++ .../openclaw-alpha-x64.appinstaller | 17 ++++++++ .../openclaw-update.alpha.appinstaller | 31 ++++++++++++++ .../openclaw-update.stable.appinstaller | 42 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 installer/appinstaller/openclaw-alpha-arm64.appinstaller create mode 100644 installer/appinstaller/openclaw-alpha-x64.appinstaller create mode 100644 installer/appinstaller/openclaw-update.alpha.appinstaller create mode 100644 installer/appinstaller/openclaw-update.stable.appinstaller diff --git a/installer/appinstaller/openclaw-alpha-arm64.appinstaller b/installer/appinstaller/openclaw-alpha-arm64.appinstaller new file mode 100644 index 000000000..b31df42ef --- /dev/null +++ b/installer/appinstaller/openclaw-alpha-arm64.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller new file mode 100644 index 000000000..342020e26 --- /dev/null +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/installer/appinstaller/openclaw-update.alpha.appinstaller b/installer/appinstaller/openclaw-update.alpha.appinstaller new file mode 100644 index 000000000..017204e9b --- /dev/null +++ b/installer/appinstaller/openclaw-update.alpha.appinstaller @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/installer/appinstaller/openclaw-update.stable.appinstaller b/installer/appinstaller/openclaw-update.stable.appinstaller new file mode 100644 index 000000000..ba7bd1151 --- /dev/null +++ b/installer/appinstaller/openclaw-update.stable.appinstaller @@ -0,0 +1,42 @@ + + + + + + + + + + + From 82461ecdf111b2fc58366bd73ac584d15157d0a5 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 09:33:19 -0700 Subject: [PATCH 30/38] msix(channels): wire MSIX builds to Stable/Alpha/Dev channels via OpenClawChannel property Commit 2 of the three-channel embedded-AppInstaller production rollout. Wires the channel logic into the WinUI csproj and build.ps1; consumes the per-channel embedded-AppInstaller source files added in Commit 1. Changes ======= .gitignore Ignore the generated src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller (output of the RenderEmbeddedAppInstaller MSBuild target; per-build payload). src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj - Top-level OpenClawChannel property, defaulting to Dev. CI overrides with -p:OpenClawChannel=Stable|Alpha based on the tag pattern. - Extended the existing SyncAppxManifestVersion inline Roslyn task with three new parameters - IdentityName, DisplayName, RemoveAutoUpdate. The task now rewrites Identity/@Name, Properties/DisplayName, and VisualElements/@DisplayName in addition to Identity/@Version; for Dev it also strips the uap13:AutoUpdate element so Dev MSIXs do not poll for updates. - New RenderEmbeddedAppInstaller target (Stable/Alpha only). Copies the per-channel source file installer/appinstaller/openclaw-update.{stable,alpha}.appinstaller to the WinUI project as openclaw-update.appinstaller, then XmlPokes placeholder values (Version, ProcessorArchitecture, MainPackage Uri, and the feed self-Uri for arm64). - Conditional picks up the rendered file for non-Dev channels and ships it inside the MSIX payload. src/OpenClaw.Tray.WinUI/Package.appxmanifest - Identity/@Version baseline restored to 1.0.0.0 (was left at 0.6.4.0 by the rehearsal). - TEMP rehearsal comments rewritten as production language. The uap13:AutoUpdate element stays committed (Stable default; Dev builds strip it). build.ps1 - New -OpenClawChannel Dev|Alpha|Stable parameter (default Dev) and -OpenClawReleaseTag parameter. Threaded through to dotnet publish via -p:OpenClawChannel and -p:OpenClawReleaseTag. - Preflight: -OpenClawChannel Stable|Alpha requires -OpenClawReleaseTag. Dev with a tag warns. Channel/tag without -PackageMsix warns. Deletions (rehearsal artifacts now superseded by the production wiring): - installer/appinstaller/openclaw-msixtest-x64.appinstaller - src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller (now generated) Local validation ================ - ./build.ps1: green. - Shared.Tests: 2199 passed, 29 skipped. - Tray.Tests: 991 passed. - Dev MSIX via ./build.ps1 -Project WinUI -PackageMsix: Identity OpenClaw.Companion.Dev, no AutoUpdate, no embedded file. Confirmed. - Alpha MSIX via ./build.ps1 -Project WinUI -PackageMsix -OpenClawChannel Alpha -OpenClawReleaseTag v0.6.4-alpha.1: Identity OpenClaw.Companion.Alpha, embedded openclaw-update.appinstaller with alpha feed Uri and release-asset Uri. Confirmed. - Stable MSIX via -OpenClawChannel Stable -OpenClawReleaseTag v0.6.7: Identity OpenClaw.Companion, embedded file with stable feed Uri. Confirmed. - Preflight: missing tag for Alpha/Stable hard-fails. Confirmed. Resolves session todos: - delete-msixtest-appinstaller-file - revert-embedded-appinstaller-manifest (Package.appxmanifest now in production shape) - revert-embedded-appinstaller-content (no more committed openclaw-update.appinstaller) Next: Commit 3 wires CI (.github/workflows/ci.yml) to pass -p:OpenClawChannel based on the tag pattern and reverts the remaining TEMP rehearsal CI edits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 4 + build.ps1 | 61 +++++- .../openclaw-msixtest-x64.appinstaller | 62 ------ .../OpenClaw.Tray.WinUI.csproj | 197 ++++++++++++++++-- src/OpenClaw.Tray.WinUI/Package.appxmanifest | 23 +- .../openclaw-update.appinstaller | 24 --- 6 files changed, 254 insertions(+), 117 deletions(-) delete mode 100644 installer/appinstaller/openclaw-msixtest-x64.appinstaller delete mode 100644 src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller diff --git a/.gitignore b/.gitignore index 97d6a0cbc..06802f77d 100644 --- a/.gitignore +++ b/.gitignore @@ -361,3 +361,7 @@ visual-test-output/ msix-validation-evidence/ packaging-test-output/ uninstall-validation-output/ + +# Generated by RenderEmbeddedAppInstaller MSBuild target for Stable/Alpha MSIXs; +# committed sources live in installer/appinstaller/openclaw-update.{stable,alpha}.appinstaller. +src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller diff --git a/build.ps1 b/build.ps1 index 7e218f161..c48428719 100644 --- a/build.ps1 +++ b/build.ps1 @@ -31,10 +31,34 @@ on stock Windows (Add-AppxPackage -AllowUnsigned only works under very specific developer-mode conditions that do not cover this package). +.PARAMETER OpenClawChannel + Which MSIX release channel to build (only meaningful with -PackageMsix): + + Dev - Local builds. Identity OpenClaw.Companion.Dev, no embedded + AppInstaller, no auto-update. Default for -PackageMsix. + + Alpha - Tester builds. Identity OpenClaw.Companion.Alpha, embedded + AppInstaller pointing at the alpha polled feed on main. CI uses + this for tags matching vX.Y.Z-alpha.N. + + Stable - Production builds. Identity OpenClaw.Companion, embedded + AppInstaller pointing at the stable polled feed on main. CI uses + this for tags matching vX.Y.Z. + + Stable and Alpha require -OpenClawReleaseTag so the embedded AppInstaller's + MainPackage Uri can reference the matching GitHub release. + +.PARAMETER OpenClawReleaseTag + Git tag corresponding to this MSIX, e.g. v0.6.4 or v0.6.4-alpha.1. Required + when -OpenClawChannel is Stable or Alpha. Used by the embedded AppInstaller's + MainPackage Uri to reference the right GitHub release asset. + .EXAMPLE .\build.ps1 .\build.ps1 -Project WinUI -Configuration Release .\build.ps1 -Project WinUI -PackageMsix + .\build.ps1 -Project WinUI -PackageMsix -OpenClawChannel Alpha -OpenClawReleaseTag v0.6.4-alpha.1 + .\build.ps1 -Project WinUI -PackageMsix -OpenClawChannel Stable -OpenClawReleaseTag v0.6.4 .\build.ps1 -CheckOnly #> @@ -49,7 +73,12 @@ param( [switch]$NoTrustRepository, - [switch]$PackageMsix + [switch]$PackageMsix, + + [ValidateSet("Dev", "Alpha", "Stable")] + [string]$OpenClawChannel = "Dev", + + [string]$OpenClawReleaseTag ) $ErrorActionPreference = "Stop" @@ -321,8 +350,21 @@ function Build-Project($name, $path, $useRid = $false, $publishMsix = $false) { if ($publishMsix) { # MSIX file production: dotnet publish so the self-contained layout MSIX # tooling packages matches what end-users install. -p:PackageMsix=true - # turns on GenerateAppxPackageOnBuild in the csproj. - $dotnetArgs = @("publish", $path, "-c", $Configuration, "-r", $rid, "--self-contained", "-p:PackageMsix=true") + # turns on GenerateAppxPackageOnBuild in the csproj. The csproj's + # SyncAppxManifestVersionTarget reads $(OpenClawChannel) to pick the + # Identity Name / DisplayName / embedded-AppInstaller behavior; default + # Dev produces an OpenClaw.Companion.Dev MSIX with no auto-update. + $dotnetArgs = @( + "publish", $path, + "-c", $Configuration, + "-r", $rid, + "--self-contained", + "-p:PackageMsix=true", + "-p:OpenClawChannel=$OpenClawChannel" + ) + if ($OpenClawReleaseTag) { + $dotnetArgs += "-p:OpenClawReleaseTag=$OpenClawReleaseTag" + } } elseif ($useRid) { # WinUI requires runtime identifier for self-contained WebView2 support. $dotnetArgs = @("build", $path, "-c", $Configuration, "-r", $rid) @@ -398,6 +440,17 @@ if ($PackageMsix) { exit 1 } + # Stable and Alpha embed an AppInstaller that references a specific GitHub + # release asset — without a tag we can't compute that URI. + if ($OpenClawChannel -in @("Stable", "Alpha") -and -not $OpenClawReleaseTag) { + Write-Error "-OpenClawChannel $OpenClawChannel requires -OpenClawReleaseTag (e.g. v0.6.4-alpha.1 or v0.6.4)" + exit 1 + } + if ($OpenClawChannel -eq "Dev" -and $OpenClawReleaseTag) { + Write-Warning "-OpenClawReleaseTag '$OpenClawReleaseTag' is ignored for Dev channel (Dev MSIXs do not embed an AppInstaller)" + } + Write-Info "MSIX channel: $OpenClawChannel$(if ($OpenClawReleaseTag) { " (release tag: $OpenClawReleaseTag)" })" + $devPfx = Join-Path $env:LOCALAPPDATA "OpenClawTray\dev-msix.pfx" if (Test-Path $devPfx) { Write-Success "Dev MSIX signing cert found: $devPfx" @@ -408,6 +461,8 @@ if ($PackageMsix) { Write-Host " .\scripts\setup-dev-msix-cert.ps1" -ForegroundColor White exit 1 } +} elseif ($OpenClawChannel -ne "Dev" -or $OpenClawReleaseTag) { + Write-Warning "-OpenClawChannel/-OpenClawReleaseTag are only meaningful with -PackageMsix; ignoring." } for ($i = 0; $i -lt $toBuild.Count; $i++) { diff --git a/installer/appinstaller/openclaw-msixtest-x64.appinstaller b/installer/appinstaller/openclaw-msixtest-x64.appinstaller deleted file mode 100644 index 3c5d1a505..000000000 --- a/installer/appinstaller/openclaw-msixtest-x64.appinstaller +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 307ba1041..7f78f68cb 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -41,6 +41,13 @@ MSIX true + + + Dev @@ -97,12 +104,13 @@ - - + + @@ -122,9 +130,12 @@ Package.appxmanifest carries a baseline Version in source (e.g. 1.0.0.0) so the always-packaged dev path (WinAppCLI loose-layout register, VS F5) can use the in-source manifest as-is. The real release version is patched in only when actually producing a .msix file (PackageMsix=true), - so default `dotnet build` runs leave the file untouched and git stays clean. CI's manifest patch - step (.github/workflows/ci.yml) only writes Identity/Name and DisplayName; Version is written - by this target from the GitVersion-derived $(Version). + so default `dotnet build` runs leave the file untouched and git stays clean. + + This task ALSO patches Identity Name and DisplayName based on $(OpenClawChannel) + (Stable / Alpha / Dev) and conditionally removes the element + for Dev MSIX builds. See the "Channel" PropertyGroup below for the channel + -> identity/display-name mapping. Uses a regex string-replace (not XmlPoke) to preserve the manifest's hand-formatted layout. The MSIX Identity/Version must be a 4-part version (X.Y.Z.W) with no pre-release suffix; @@ -134,6 +145,9 @@ + + + @@ -143,21 +157,69 @@ Log.LogError("SyncAppxManifestVersion: '" + FourPartVersion + "' is not a valid 4-part numeric MSIX version."); return false; } + // Validate identity name shape (matches all current channels: OpenClaw.Companion[.Alpha|.Dev]). + if (!System.Text.RegularExpressions.Regex.IsMatch(IdentityName, @"^[A-Za-z][A-Za-z0-9\.\-]*$")) { + Log.LogError("SyncAppxManifestVersion: '" + IdentityName + "' is not a valid AppX Identity Name."); + return false; + } var text = System.IO.File.ReadAllText(ManifestPath); - // Tolerate attribute-order, whitespace around '=', single or double quotes, and case - // variations on the Version attribute name. Capture the opening and closing quote - // characters so we re-emit them unchanged. - var regex = new System.Text.RegularExpressions.Regex( + + // 1) Identity/@Version. + var versionRegex = new System.Text.RegularExpressions.Regex( "(]*?\\bVersion\\s*=\\s*[\"'])[^\"']+([\"'])", System.Text.RegularExpressions.RegexOptions.IgnoreCase); - if (!regex.IsMatch(text)) { + if (!versionRegex.IsMatch(text)) { Log.LogError("SyncAppxManifestVersion: could not find Identity/@Version in " + ManifestPath); return false; } - var updated = regex.Replace(text, "${1}" + FourPartVersion + "$2", 1); - if (updated == text) { - return true; + text = versionRegex.Replace(text, "${1}" + FourPartVersion + "$2", 1); + + // 2) Identity/@Name. + var nameRegex = new System.Text.RegularExpressions.Regex( + "(]*?\\bName\\s*=\\s*[\"'])[^\"']+([\"'])", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (!nameRegex.IsMatch(text)) { + Log.LogError("SyncAppxManifestVersion: could not find Identity/@Name in " + ManifestPath); + return false; + } + text = nameRegex.Replace(text, "${1}" + IdentityName + "$2", 1); + + // 3) Properties/DisplayName (element text content). Anchored to the + // direct child of so we don't accidentally also rewrite + // PublisherDisplayName below it. Singleline so '.' would match newlines, + // but we limit with non-greedy and require the closing tag on the same + // text run via [^<]*. + var propsDisplayNameRegex = new System.Text.RegularExpressions.Regex( + "(]*>\\s*)[^<]*()", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (!propsDisplayNameRegex.IsMatch(text)) { + Log.LogError("SyncAppxManifestVersion: could not find Properties/DisplayName in " + ManifestPath); + return false; + } + text = propsDisplayNameRegex.Replace(text, "${1}" + DisplayName + "$2", 1); + + // 4) Application/VisualElements/@DisplayName. + var veDisplayNameRegex = new System.Text.RegularExpressions.Regex( + "(]*?\\bDisplayName\\s*=\\s*[\"'])[^\"']+([\"'])", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (!veDisplayNameRegex.IsMatch(text)) { + Log.LogError("SyncAppxManifestVersion: could not find VisualElements/@DisplayName in " + ManifestPath); + return false; + } + text = veDisplayNameRegex.Replace(text, "${1}" + DisplayName + "$2", 1); + + // 5) Optionally strip ... for Dev MSIXs. + // The (?s) flag lets '.' match newlines so the multi-line element is captured. + // Eats one leading newline/whitespace too so we don't leave a blank line behind. + if (RemoveAutoUpdate) { + var autoUpdateRegex = new System.Text.RegularExpressions.Regex( + @"(?s)\s*]*>.*?\s*", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (autoUpdateRegex.IsMatch(text)) { + text = autoUpdateRegex.Replace(text, System.Environment.NewLine + " ", 1); + } + // It's OK if the element wasn't there (e.g. earlier Dev build already stripped it). } // Clear any read-only attribute (some non-Git SCMs mark files read-only) so the @@ -176,7 +238,7 @@ // sure the temp file never lingers (it isn't in .gitignore and would otherwise show // up in `git status` if Replace throws something other than FileNotFoundException). var tempPath = ManifestPath + ".sync-tmp"; - System.IO.File.WriteAllText(tempPath, updated); + System.IO.File.WriteAllText(tempPath, text); try { try { System.IO.File.Replace(tempPath, ManifestPath, destinationBackupFileName: null); @@ -193,7 +255,11 @@ } } catch { /* best-effort cleanup */ } } - Log.LogMessage(MessageImportance.High, "Synced Package.appxmanifest Identity/@Version to " + FourPartVersion); + Log.LogMessage(MessageImportance.High, + "Synced Package.appxmanifest: Identity Name=" + IdentityName + + ", Version=" + FourPartVersion + + ", DisplayName=" + DisplayName + + (RemoveAutoUpdate ? ", AutoUpdate removed (Dev)" : "")); ]]> @@ -214,6 +280,28 @@ this, the target would silently skip and the baseline version in the source manifest would ship in the .msix package. --> + + + + <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Stable'">OpenClaw.Companion + <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Alpha'">OpenClaw.Companion.Alpha + <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Dev'">OpenClaw.Companion.Dev + + <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Stable'">OpenClaw Companion + <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Alpha'">OpenClaw Companion Alpha + <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Dev'">OpenClaw Companion Dev + + <_OpenClawRemoveAutoUpdate Condition="'$(OpenClawChannel)' == 'Dev'">true + <_OpenClawRemoveAutoUpdate Condition="'$(_OpenClawRemoveAutoUpdate)' == ''">false + + + + <_AppxManifestPath>$(MSBuildThisFileDirectory)Package.appxmanifest <_StrippedVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(Version)', '[-+].*$', '')) @@ -223,7 +311,76 @@ <_AppxManifestVersion Condition="'$(_VersionDotCount)' == '1'">$(_StrippedVersion).0.0 - + + + + + + + + + + <_OpenClawEmbedArch Condition="'$(RuntimeIdentifier)' == 'win-arm64'">arm64 + <_OpenClawEmbedArch Condition="'$(_OpenClawEmbedArch)' == '' And '$(Platform)' == 'ARM64'">arm64 + <_OpenClawEmbedArch Condition="'$(_OpenClawEmbedArch)' == ''">x64 + + <_OpenClawEmbedSource>$(OpenClawRepoRoot)installer\appinstaller\openclaw-update.$(OpenClawChannel.ToLowerInvariant()).appinstaller + <_OpenClawEmbedOutput>$(MSBuildThisFileDirectory)openclaw-update.appinstaller + + <_OpenClawFeedFilename Condition="'$(OpenClawChannel)' == 'Stable'">openclaw-$(_OpenClawEmbedArch).appinstaller + <_OpenClawFeedFilename Condition="'$(OpenClawChannel)' == 'Alpha'">openclaw-alpha-$(_OpenClawEmbedArch).appinstaller + <_OpenClawFeedUri>https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/$(_OpenClawFeedFilename) + + <_OpenClawMainPackageUri>https://github.com/openclaw/openclaw-windows-node/releases/download/$(OpenClawReleaseTag)/OpenClaw.Tray.WinUI_$(_AppxManifestVersion)_$(_OpenClawEmbedArch).msix + + + + + + + + + <_OpenClawAppInstallerNamespaces><Namespace Prefix="ai" Uri="http://schemas.microsoft.com/appx/appinstaller/2018" /> + + + + + + + + + + docs/VERSIONING.md. + + Identity Name and DisplayName below default to the Stable channel. + SyncAppxManifestVersionTarget rewrites them per $(OpenClawChannel) + (Stable / Alpha / Dev). For Dev builds the same target also strips the + embedded-AppInstaller element from the Properties block below so Dev + MSIXs do not poll for updates. --> + Version="1.0.0.0" /> OpenClaw Companion OpenClaw Foundation Assets\StoreLogo.png - + diff --git a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller b/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller deleted file mode 100644 index 021423dc8..000000000 --- a/src/OpenClaw.Tray.WinUI/openclaw-update.appinstaller +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - From 484c5417f1cd64aa3de2e0a4020a08caa6290fd2 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 10:41:23 -0700 Subject: [PATCH 31/38] msix(channels): rename OpenClawChannel/OpenClawReleaseTag -> ReleaseChannel/ReleaseTag Rename the MSBuild property and matching build.ps1 parameter to drop the OpenClaw prefix. The names are scoped enough to be unambiguous without it and shorter to type. OpenClawChannel -> ReleaseChannel OpenClawReleaseTag -> ReleaseTag Mechanical rename across 3 files (50 total swaps): src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj, src/OpenClaw.Tray.WinUI/Package.appxmanifest, build.ps1. The internal csproj-local properties (_OpenClawIdentityName, _OpenClawDisplayName, _OpenClawRemoveAutoUpdate, _OpenClawArch, _OpenClawEmbedArch, etc.) keep their _OpenClaw prefix since they are private to the csproj and the convention is that private MSBuild properties are prefixed with _. No behavior change. build.ps1 -Project WinUI -PackageMsix -ReleaseChannel Alpha -ReleaseTag v0.6.4-alpha.1 produces the same MSIX it did before this commit under the old parameter names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build.ps1 | 38 ++++++++-------- .../OpenClaw.Tray.WinUI.csproj | 44 +++++++++---------- src/OpenClaw.Tray.WinUI/Package.appxmanifest | 10 ++--- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/build.ps1 b/build.ps1 index c48428719..fd54edec3 100644 --- a/build.ps1 +++ b/build.ps1 @@ -31,7 +31,7 @@ on stock Windows (Add-AppxPackage -AllowUnsigned only works under very specific developer-mode conditions that do not cover this package). -.PARAMETER OpenClawChannel +.PARAMETER ReleaseChannel Which MSIX release channel to build (only meaningful with -PackageMsix): Dev - Local builds. Identity OpenClaw.Companion.Dev, no embedded @@ -45,20 +45,20 @@ AppInstaller pointing at the stable polled feed on main. CI uses this for tags matching vX.Y.Z. - Stable and Alpha require -OpenClawReleaseTag so the embedded AppInstaller's + Stable and Alpha require -ReleaseTag so the embedded AppInstaller's MainPackage Uri can reference the matching GitHub release. -.PARAMETER OpenClawReleaseTag +.PARAMETER ReleaseTag Git tag corresponding to this MSIX, e.g. v0.6.4 or v0.6.4-alpha.1. Required - when -OpenClawChannel is Stable or Alpha. Used by the embedded AppInstaller's + when -ReleaseChannel is Stable or Alpha. Used by the embedded AppInstaller's MainPackage Uri to reference the right GitHub release asset. .EXAMPLE .\build.ps1 .\build.ps1 -Project WinUI -Configuration Release .\build.ps1 -Project WinUI -PackageMsix - .\build.ps1 -Project WinUI -PackageMsix -OpenClawChannel Alpha -OpenClawReleaseTag v0.6.4-alpha.1 - .\build.ps1 -Project WinUI -PackageMsix -OpenClawChannel Stable -OpenClawReleaseTag v0.6.4 + .\build.ps1 -Project WinUI -PackageMsix -ReleaseChannel Alpha -ReleaseTag v0.6.4-alpha.1 + .\build.ps1 -Project WinUI -PackageMsix -ReleaseChannel Stable -ReleaseTag v0.6.4 .\build.ps1 -CheckOnly #> @@ -76,9 +76,9 @@ param( [switch]$PackageMsix, [ValidateSet("Dev", "Alpha", "Stable")] - [string]$OpenClawChannel = "Dev", + [string]$ReleaseChannel = "Dev", - [string]$OpenClawReleaseTag + [string]$ReleaseTag ) $ErrorActionPreference = "Stop" @@ -351,7 +351,7 @@ function Build-Project($name, $path, $useRid = $false, $publishMsix = $false) { # MSIX file production: dotnet publish so the self-contained layout MSIX # tooling packages matches what end-users install. -p:PackageMsix=true # turns on GenerateAppxPackageOnBuild in the csproj. The csproj's - # SyncAppxManifestVersionTarget reads $(OpenClawChannel) to pick the + # SyncAppxManifestVersionTarget reads $(ReleaseChannel) to pick the # Identity Name / DisplayName / embedded-AppInstaller behavior; default # Dev produces an OpenClaw.Companion.Dev MSIX with no auto-update. $dotnetArgs = @( @@ -360,10 +360,10 @@ function Build-Project($name, $path, $useRid = $false, $publishMsix = $false) { "-r", $rid, "--self-contained", "-p:PackageMsix=true", - "-p:OpenClawChannel=$OpenClawChannel" + "-p:ReleaseChannel=$ReleaseChannel" ) - if ($OpenClawReleaseTag) { - $dotnetArgs += "-p:OpenClawReleaseTag=$OpenClawReleaseTag" + if ($ReleaseTag) { + $dotnetArgs += "-p:ReleaseTag=$ReleaseTag" } } elseif ($useRid) { # WinUI requires runtime identifier for self-contained WebView2 support. @@ -442,14 +442,14 @@ if ($PackageMsix) { # Stable and Alpha embed an AppInstaller that references a specific GitHub # release asset — without a tag we can't compute that URI. - if ($OpenClawChannel -in @("Stable", "Alpha") -and -not $OpenClawReleaseTag) { - Write-Error "-OpenClawChannel $OpenClawChannel requires -OpenClawReleaseTag (e.g. v0.6.4-alpha.1 or v0.6.4)" + if ($ReleaseChannel -in @("Stable", "Alpha") -and -not $ReleaseTag) { + Write-Error "-ReleaseChannel $ReleaseChannel requires -ReleaseTag (e.g. v0.6.4-alpha.1 or v0.6.4)" exit 1 } - if ($OpenClawChannel -eq "Dev" -and $OpenClawReleaseTag) { - Write-Warning "-OpenClawReleaseTag '$OpenClawReleaseTag' is ignored for Dev channel (Dev MSIXs do not embed an AppInstaller)" + if ($ReleaseChannel -eq "Dev" -and $ReleaseTag) { + Write-Warning "-ReleaseTag '$ReleaseTag' is ignored for Dev channel (Dev MSIXs do not embed an AppInstaller)" } - Write-Info "MSIX channel: $OpenClawChannel$(if ($OpenClawReleaseTag) { " (release tag: $OpenClawReleaseTag)" })" + Write-Info "MSIX channel: $ReleaseChannel$(if ($ReleaseTag) { " (release tag: $ReleaseTag)" })" $devPfx = Join-Path $env:LOCALAPPDATA "OpenClawTray\dev-msix.pfx" if (Test-Path $devPfx) { @@ -461,8 +461,8 @@ if ($PackageMsix) { Write-Host " .\scripts\setup-dev-msix-cert.ps1" -ForegroundColor White exit 1 } -} elseif ($OpenClawChannel -ne "Dev" -or $OpenClawReleaseTag) { - Write-Warning "-OpenClawChannel/-OpenClawReleaseTag are only meaningful with -PackageMsix; ignoring." +} elseif ($ReleaseChannel -ne "Dev" -or $ReleaseTag) { + Write-Warning "-ReleaseChannel/-ReleaseTag are only meaningful with -PackageMsix; ignoring." } for ($i = 0; $i -lt $toBuild.Count; $i++) { diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 7f78f68cb..5e55697d7 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -44,10 +44,10 @@ - Dev + Dev @@ -110,7 +110,7 @@ file nor include it. The file is gitignored. --> + Condition="'$(ReleaseChannel)' != 'Dev'" /> @@ -132,7 +132,7 @@ real release version is patched in only when actually producing a .msix file (PackageMsix=true), so default `dotnet build` runs leave the file untouched and git stays clean. - This task ALSO patches Identity Name and DisplayName based on $(OpenClawChannel) + This task ALSO patches Identity Name and DisplayName based on $(ReleaseChannel) (Stable / Alpha / Dev) and conditionally removes the element for Dev MSIX builds. See the "Channel" PropertyGroup below for the channel -> identity/display-name mapping. @@ -281,25 +281,25 @@ manifest would ship in the .msix package. --> - - <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Stable'">OpenClaw.Companion - <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Alpha'">OpenClaw.Companion.Alpha - <_OpenClawIdentityName Condition="'$(OpenClawChannel)' == 'Dev'">OpenClaw.Companion.Dev + <_OpenClawIdentityName Condition="'$(ReleaseChannel)' == 'Stable'">OpenClaw.Companion + <_OpenClawIdentityName Condition="'$(ReleaseChannel)' == 'Alpha'">OpenClaw.Companion.Alpha + <_OpenClawIdentityName Condition="'$(ReleaseChannel)' == 'Dev'">OpenClaw.Companion.Dev - <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Stable'">OpenClaw Companion - <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Alpha'">OpenClaw Companion Alpha - <_OpenClawDisplayName Condition="'$(OpenClawChannel)' == 'Dev'">OpenClaw Companion Dev + <_OpenClawDisplayName Condition="'$(ReleaseChannel)' == 'Stable'">OpenClaw Companion + <_OpenClawDisplayName Condition="'$(ReleaseChannel)' == 'Alpha'">OpenClaw Companion Alpha + <_OpenClawDisplayName Condition="'$(ReleaseChannel)' == 'Dev'">OpenClaw Companion Dev - <_OpenClawRemoveAutoUpdate Condition="'$(OpenClawChannel)' == 'Dev'">true + <_OpenClawRemoveAutoUpdate Condition="'$(ReleaseChannel)' == 'Dev'">true <_OpenClawRemoveAutoUpdate Condition="'$(_OpenClawRemoveAutoUpdate)' == ''">false - @@ -336,24 +336,24 @@ + Condition="'$(PackageMsix)' == 'true' And '$(ReleaseChannel)' != 'Dev'"> - + <_OpenClawEmbedArch Condition="'$(RuntimeIdentifier)' == 'win-arm64'">arm64 <_OpenClawEmbedArch Condition="'$(_OpenClawEmbedArch)' == '' And '$(Platform)' == 'ARM64'">arm64 <_OpenClawEmbedArch Condition="'$(_OpenClawEmbedArch)' == ''">x64 - <_OpenClawEmbedSource>$(OpenClawRepoRoot)installer\appinstaller\openclaw-update.$(OpenClawChannel.ToLowerInvariant()).appinstaller + <_OpenClawEmbedSource>$(OpenClawRepoRoot)installer\appinstaller\openclaw-update.$(ReleaseChannel.ToLowerInvariant()).appinstaller <_OpenClawEmbedOutput>$(MSBuildThisFileDirectory)openclaw-update.appinstaller - <_OpenClawFeedFilename Condition="'$(OpenClawChannel)' == 'Stable'">openclaw-$(_OpenClawEmbedArch).appinstaller - <_OpenClawFeedFilename Condition="'$(OpenClawChannel)' == 'Alpha'">openclaw-alpha-$(_OpenClawEmbedArch).appinstaller + <_OpenClawFeedFilename Condition="'$(ReleaseChannel)' == 'Stable'">openclaw-$(_OpenClawEmbedArch).appinstaller + <_OpenClawFeedFilename Condition="'$(ReleaseChannel)' == 'Alpha'">openclaw-alpha-$(_OpenClawEmbedArch).appinstaller <_OpenClawFeedUri>https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/$(_OpenClawFeedFilename) - <_OpenClawMainPackageUri>https://github.com/openclaw/openclaw-windows-node/releases/download/$(OpenClawReleaseTag)/OpenClaw.Tray.WinUI_$(_AppxManifestVersion)_$(_OpenClawEmbedArch).msix + <_OpenClawMainPackageUri>https://github.com/openclaw/openclaw-windows-node/releases/download/$(ReleaseTag)/OpenClaw.Tray.WinUI_$(_AppxManifestVersion)_$(_OpenClawEmbedArch).msix + Text="Rendered embedded AppInstaller: channel=$(ReleaseChannel) arch=$(_OpenClawEmbedArch) version=$(_AppxManifestVersion) release-tag=$(ReleaseTag)" /> + Version="0.6.7.0" /> - OpenClaw Companion + OpenClaw Companion Alpha OpenClaw Foundation Assets\StoreLogo.png + Uri="https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-alpha-arm64.appinstaller"> + + Uri="https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/openclaw-alpha-x64.appinstaller"> openclaw-$(_OpenClawEmbedArch).appinstaller <_OpenClawFeedFilename Condition="'$(ReleaseChannel)' == 'Alpha'">openclaw-alpha-$(_OpenClawEmbedArch).appinstaller - <_OpenClawFeedUri>https://raw.githubusercontent.com/openclaw/openclaw-windows-node/main/installer/appinstaller/$(_OpenClawFeedFilename) + + <_OpenClawFeedUri>https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user%2Fkmahone%2Fmsix/installer/appinstaller/$(_OpenClawFeedFilename) <_OpenClawMainPackageUri>https://github.com/openclaw/openclaw-windows-node/releases/download/$(ReleaseTag)/OpenClaw.Tray.WinUI_$(_AppxManifestVersion)_$(_OpenClawEmbedArch).msix From 18f51d90893ab1147d8abbfb246faf0ad073bfc4 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 11:43:38 -0700 Subject: [PATCH 34/38] msixtest: bump alpha disk feed to 0.6.9.0 / v0.6.9-msixtest.1 for update rehearsal --- installer/appinstaller/openclaw-alpha-x64.appinstaller | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller index 6472d924a..d8e2d7b46 100644 --- a/installer/appinstaller/openclaw-alpha-x64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -5,15 +5,15 @@ before merging this branch to main. --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.9-msixtest.1/OpenClaw.Tray.WinUI_0.6.9.0_x64.msix" /> From b14e0a0b992a527a6e4d89d715b3b0cd36f98215 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 12:13:39 -0700 Subject: [PATCH 35/38] AppInstaller: HoursBetweenUpdateChecks=0, UpdateBlocksActivation=true UpdateBlocksActivation=false never applies on a persistent tray app - Windows downloads then deletes the staged package because the app is still running. Switch to the canonical persistent-app settings and sync OnLaunch across all six .appinstaller files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- installer/appinstaller/openclaw-alpha-arm64.appinstaller | 1 + installer/appinstaller/openclaw-alpha-x64.appinstaller | 1 + installer/appinstaller/openclaw-arm64.appinstaller | 1 + installer/appinstaller/openclaw-update.alpha.appinstaller | 2 +- installer/appinstaller/openclaw-update.stable.appinstaller | 2 +- installer/appinstaller/openclaw-x64.appinstaller | 1 + 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/installer/appinstaller/openclaw-alpha-arm64.appinstaller b/installer/appinstaller/openclaw-alpha-arm64.appinstaller index 35405a7f1..76a1bcbdb 100644 --- a/installer/appinstaller/openclaw-alpha-arm64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-arm64.appinstaller @@ -16,6 +16,7 @@ Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.0.0-alpha.1/OpenClawCompanion-0.0.0-alpha.1-win-arm64.msix" /> + diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller index d8e2d7b46..b9585adf3 100644 --- a/installer/appinstaller/openclaw-alpha-x64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -16,6 +16,7 @@ Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.9-msixtest.1/OpenClaw.Tray.WinUI_0.6.9.0_x64.msix" /> + diff --git a/installer/appinstaller/openclaw-arm64.appinstaller b/installer/appinstaller/openclaw-arm64.appinstaller index d208640c3..8d0b4cee9 100644 --- a/installer/appinstaller/openclaw-arm64.appinstaller +++ b/installer/appinstaller/openclaw-arm64.appinstaller @@ -12,6 +12,7 @@ Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.0.0/OpenClawCompanion-0.0.0-win-arm64.msix" /> + diff --git a/installer/appinstaller/openclaw-update.alpha.appinstaller b/installer/appinstaller/openclaw-update.alpha.appinstaller index 017204e9b..a63f136fc 100644 --- a/installer/appinstaller/openclaw-update.alpha.appinstaller +++ b/installer/appinstaller/openclaw-update.alpha.appinstaller @@ -25,7 +25,7 @@ Uri="{{MAIN_PACKAGE_URI}}" /> - + diff --git a/installer/appinstaller/openclaw-update.stable.appinstaller b/installer/appinstaller/openclaw-update.stable.appinstaller index ba7bd1151..bc73c6d5a 100644 --- a/installer/appinstaller/openclaw-update.stable.appinstaller +++ b/installer/appinstaller/openclaw-update.stable.appinstaller @@ -36,7 +36,7 @@ Uri="{{MAIN_PACKAGE_URI}}" /> - + diff --git a/installer/appinstaller/openclaw-x64.appinstaller b/installer/appinstaller/openclaw-x64.appinstaller index 582d1e992..7db9205fe 100644 --- a/installer/appinstaller/openclaw-x64.appinstaller +++ b/installer/appinstaller/openclaw-x64.appinstaller @@ -12,6 +12,7 @@ Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.0.0/OpenClawCompanion-0.0.0-win-x64.msix" /> + From 7e96ee7d80069d92e0353f9d3e9fe53023e4ae7f Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 12:50:23 -0700 Subject: [PATCH 36/38] alpha-x64 feed: bump to 0.6.11.0 for auto-update test #2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- installer/appinstaller/openclaw-alpha-x64.appinstaller | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller index b9585adf3..0dad39d79 100644 --- a/installer/appinstaller/openclaw-alpha-x64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -5,15 +5,15 @@ before merging this branch to main. --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.11-msixtest.1/OpenClaw.Tray.WinUI_0.6.11.0_x64.msix" /> From a3f2e112d3a122cb949c3834f55c80f18265f051 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 13:06:20 -0700 Subject: [PATCH 37/38] back to 0.6.10 --- installer/appinstaller/openclaw-alpha-x64.appinstaller | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller index 0dad39d79..e3c9df3e2 100644 --- a/installer/appinstaller/openclaw-alpha-x64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -5,15 +5,15 @@ before merging this branch to main. --> + Version="0.6.10.0" + Uri="https://raw.githubusercontent.com/openclaw/openclaw-windows-node/user/kmahone/msix/installer/appinstaller/openclaw-alpha-x64.appinstaller"> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.10-msixtest.1/OpenClaw.Tray.WinUI_0.6.10.0_x64.msix" /> From b05f56190d3caa06d48f600c1cb9773a84924625 Mon Sep 17 00:00:00 2001 From: Keith Mahoney Date: Fri, 12 Jun 2026 13:08:03 -0700 Subject: [PATCH 38/38] 0.6.11 --- installer/appinstaller/openclaw-alpha-x64.appinstaller | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/appinstaller/openclaw-alpha-x64.appinstaller b/installer/appinstaller/openclaw-alpha-x64.appinstaller index e3c9df3e2..c8533c42e 100644 --- a/installer/appinstaller/openclaw-alpha-x64.appinstaller +++ b/installer/appinstaller/openclaw-alpha-x64.appinstaller @@ -5,15 +5,15 @@ before merging this branch to main. --> + Uri="https://github.com/openclaw/openclaw-windows-node/releases/download/v0.6.11-msixtest.1/OpenClaw.Tray.WinUI_0.6.11.0_x64.msix" />