Use this plan to validate the Surge-managed Horizon installer and update flow on macOS and Windows before relying on GitHub Releases, Homebrew, or WinGet publication.
Prove four things on each target OS:
- Horizon release artifacts can be packed into a Surge installer on that OS.
- The generated installer can install Horizon into the normal per-user location.
- The installed runtime manifest points at a working Surge backend.
- A newer release can be promoted from
betatostable, then applied in place withUpdateManager::download_and_apply.
Use a local filesystem backend before any hosted smoke:
- it removes GitHub Release timing and auth from the first validation pass
- it lets you test
betaandstablepromotion on one machine - it exercises the same package/index/update machinery that the hosted flow depends on
Horizon only shows its in-app update prompt for github_releases + stable managed installs. The local smoke below validates the installer and package-update plumbing directly, using the helper example at crates/horizon-ui/examples/surge-update-smoke.rs. After this passes, do one hosted GitHub Releases smoke for the UI prompt path.
The fastest supported path on macOS or Windows is the wrapper at run-surge-filesystem-smoke.sh. It auto-detects the current target, builds Horizon in debug mode by default, stages the icon asset that surge pack requires, installs 0.2.0-smoke.1, then packs/promotes/applies 0.2.0-smoke.2.
For Windows smoke from a Linux or macOS host, use run-surge-azure-smoke.sh. It provisions a disposable Azure Windows 11 VM, installs Build Tools if they are missing, forces one autologon to get a real desktop session, then launches the smoke through an interactive scheduled task.
Run these steps on the target OS you are validating.
- Build Horizon once. Use the default debug build unless the payload itself must be release.
- Build or reuse the Surge toolchain from the source you are validating.
- Create a temporary one-app Surge manifest that uses
provider: filesystem. - Stage and pack
0.2.0-smoke.1. - Push
0.2.0-smoke.1tobeta, then promote it tostable. - Install
0.2.0-smoke.1with the generated installer. - Verify the installed tree and runtime manifest.
- Stage and pack
0.2.0-smoke.2. - Push
0.2.0-smoke.2tobetaonly and confirm the installedstableapp still reports no update. - Promote
0.2.0-smoke.2tostable, then apply it with the helper example.
Using the same Horizon binary for both smoke versions is acceptable here. The goal is to validate packaging, channel promotion, install layout, and update application, not binary differences.
Important implementation notes from the local smoke:
- keep
channels: [stable, beta]in that order, because Surge binds published installers to the app's default channel - do not pack
0.2.0-smoke.2until after0.2.0-smoke.1is installed, because the stable installer filename is reused and later packs replace the earlier installer - if the temporary manifest lives outside the repo root, pass explicit
--artifacts-dirtosurge packand--packages-dirtosurge push
Important implementation notes from the Azure Windows smoke:
- use
git lfs pullafter cloning on the guest, because Horizon's icons and fonts are stored in Git LFS andsurge packdepends on them - on the tested Azure image, Git and Git LFS are already present, but
C:\BuildTools\Common7\Tools\VsDevCmd.batis not; install Visual Studio Build Tools before compiling - do not rely on the user's Startup folder alone to kick off the smoke, even when autologon and
explorer.exeare both present; start the guest runner with a scheduled task that usesLogonType Interactive - the current best-known disposable VM baseline is
MicrosoftVisualStudio:windowsplustools:base-win11-gen2:latestwithStandard_D4s_v3 - if you are iterating on the Windows smoke, keep the VM warm and reuse it; that avoids the slowest steps: Azure provisioning, first boot, and Build Tools installation
- before cleaning
%LOCALAPPDATA%\\horizon, the smoke helper now force-stops any running processes whose executable lives under that install root; otherwise repeated Windows runs can fail withDevice or resource busy - after the headless installer returns, stop the installer-launched
--surge-first-runHorizon process before moving on to the scripted launch/update checks; otherwise the smoke can inherit a noisy managed-install session from the installer itself - when validating an unmerged Surge fix, point the smoke helpers at the exact Surge source you want:
--surge-path <checkout>for local smoke, or--surge-repo-url <repo> --surge-commit-sha <sha>for Azure smoke - when a Surge override is active, the smoke helper patches
surge-corethrough a localfile://Git source at the exact checkout/commit instead of a raw crate path; that keeps Cargo workspace inheritance working on Windows - the toolchain helper caches by Surge source ref + commit under
.surge/toolchain-bin, so reruns against the same Surge commit skip the expensive rebuild - the Azure helper now streams the guest-side Bash smoke output live into
smoke.stream.log; do not wait for a single end-of-run dump before deciding where the Windows pass is stuck
Create a one-app manifest per target RID:
schema: 1
storage:
provider: filesystem
bucket: <ABSOLUTE_STORE_PATH>
apps:
- id: <APP_ID>
name: Horizon
main: <MAIN_EXE>
installDirectory: horizon
icon: assets/icons/icon-512.png
channels: [stable, beta]
shortcuts: [desktop, start_menu]
installers: [offline-gui]
target:
rid: <RID>The first channel is the installer's default channel. For this smoke, stable must come first so the generated installer exercises the same stable install path that the update flow later checks.
Values:
- Windows x64:
APP_ID=horizon-win-x64,MAIN_EXE=horizon.exe,RID=win-x64 - macOS arm64:
APP_ID=horizon-osx-arm64,MAIN_EXE=horizon,RID=osx-arm64 - macOS x64:
APP_ID=horizon-osx-x64,MAIN_EXE=horizon,RID=osx-x64
- Windows 11 x64
- Rust stable
>= 1.88 - Git Bash or another Bash environment for the repo scripts
- PowerShell 7 or Windows PowerShell for inspection steps
Run this from Git Bash in the repo root:
./scripts/run-surge-filesystem-smoke.sh --rid win-x64Run this from a Linux or macOS host in the repo root after pushing the branch/commit you want the guest to build:
./scripts/run-surge-azure-smoke.shUseful overrides:
--keep-resourceskeeps the VM and resource group for manual inspection--branch <name>and--commit-sha <sha>pin the exact guest checkout--repo-url <https-url>points the guest at a staging fork instead oforigin--surge-repo-url <https-url>and--surge-commit-sha <sha>pin the exact unmerged Surge source the guest should build before it is released--location <region>and--size <vm-size>let you work around regional quota shortages
Warm-VM workflow:
- first pass: run
./scripts/run-surge-azure-smoke.sh --keep-resources - rerun: pass the same
--resource-group,--vm-name, and--admin-password - when those names point at an existing VM, the helper now starts and reuses it instead of provisioning a fresh machine
- reused VMs are kept automatically, because destroying them defeats the purpose of the warm cache
The Azure helper performs the same local-filesystem install/update smoke as the Git Bash one-liner above. It is the fastest repeatable path when you do not already have a Windows machine with Rust, MSVC, and Git Bash configured.
- Build Horizon:
cargo build- Build Surge:
./scripts/build-surge-toolchain.sh --source-path ../surge --output-dir "$PWD/.surge/toolchain-bin"
export PATH="$PWD/.surge/toolchain-bin:$PATH"-
Create a filesystem store, for example
C:/tmp/horizon-surge-store, and write a temporary manifest forwin-x64. -
Stage
0.2.0-smoke.1with the same built binary:
./scripts/stage-surge-artifacts.sh --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.1 --binary target/debug/horizon.exe --main-exe horizon.exe- Pack
0.2.0-smoke.1:
surge --manifest-path <SMOKE_MANIFEST> pack \
--app-id horizon-win-x64 \
--rid win-x64 \
--version 0.2.0-smoke.1 \
--artifacts-dir "$PWD/.surge/artifacts/horizon-win-x64/win-x64/0.2.0-smoke.1" \
--output-dir "$PWD/.surge/packages"- Publish version 1 to
beta, then promote it tostable:
surge --manifest-path <SMOKE_MANIFEST> push --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.1 --channel beta --packages-dir "$PWD/.surge/packages"
surge --manifest-path <SMOKE_MANIFEST> promote --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.1 --channel stable- Run the generated installer headless:
.surge\installers\horizon-win-x64\win-x64\Setup-win-x64-horizon-win-x64-stable-offline-gui.exe --headless- Stop the installer-launched first-run app instance before continuing:
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -like '*--surge-first-run*' -and $_.ExecutablePath -like "$env:LOCALAPPDATA\\horizon\\*" } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }- Verify the install tree:
- install root:
%LOCALAPPDATA%\horizon - active app dir:
%LOCALAPPDATA%\horizon\app - runtime manifest:
%LOCALAPPDATA%\horizon\app\.surge\runtime.yml - desktop shortcut:
%USERPROFILE%\Desktop\Horizon.lnk - start-menu shortcut:
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Horizon.lnk
- Launch the installed
horizon.exeonce and confirm it stays running long enough to create a window.
- Stage and pack version 2:
./scripts/stage-surge-artifacts.sh --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.2 --binary target/debug/horizon.exe --main-exe horizon.exe
surge --manifest-path <SMOKE_MANIFEST> pack \
--app-id horizon-win-x64 \
--rid win-x64 \
--version 0.2.0-smoke.2 \
--artifacts-dir "$PWD/.surge/artifacts/horizon-win-x64/win-x64/0.2.0-smoke.2" \
--output-dir "$PWD/.surge/packages"- Push version 2 to
betaonly:
surge --manifest-path <SMOKE_MANIFEST> push --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.2 --channel beta --packages-dir "$PWD/.surge/packages"- Confirm no
stableupdate is visible yet:
cargo run --config .surge/smoke/win-x64/cargo-config.toml -p horizon-ui --example surge-update-smoke -- --app-exe "$LOCALAPPDATA/horizon/app/horizon.exe"Expected: no update available.
- Promote version 2 to
stable:
surge --manifest-path <SMOKE_MANIFEST> promote --app-id horizon-win-x64 --rid win-x64 --version 0.2.0-smoke.2 --channel stable- Apply the update:
cargo run -p horizon-ui --example surge-update-smoke -- --apply --app-exe "$LOCALAPPDATA/horizon/app/horizon.exe"- Re-check the install tree:
- runtime manifest version becomes
0.2.0-smoke.2 - previous snapshot exists as
%LOCALAPPDATA%\horizon\app-0.2.0-smoke.1 %LOCALAPPDATA%\horizon\app\.surge-cacheexists- relaunch from the installed binary still works
After the local filesystem smoke is green, run the WinGet publication smoke with scripts/run-winget-azure-smoke.sh. That validates manifest rendering, WinGet install/upgrade/uninstall, and launch in a disposable Windows 11 VM. It does not replace the local Surge update smoke above.
Use a separate public staging repo for this step, not peters/horizon.
- Mirror the Horizon release workflow in the staging repo. The workflow now defaults Surge storage to the current repo for non-
peters/horizonreleases, so no.surge/surge.ymledit is required. - Cut a stable release in the staging repo, install it on Windows from the generated Surge installer, and confirm
%LOCALAPPDATA%\\horizon\\app\\.surge\\runtime.ymlrecords the staging repo inbucket:. - Cut a second stable release in the same staging repo.
- Launch the already-installed app and wait for the Horizon update prompt.
- Click
Download Installerand confirm the browser opens the staging repo URL, not production.
Success criteria:
- prompt appears for the second staged stable release
- opened URL matches
https://github.com/<staging-owner>/<staging-repo>/releases/download/v<version>/... - downloaded installer launches and upgrades the existing install
Run this once per architecture you ship, on native hardware or a matching VM:
- Apple Silicon for
osx-arm64 - Intel for
osx-x64
- macOS 14+ on the target architecture
- Xcode Command Line Tools
- Rust stable
>= 1.88
Run this from the repo root:
./scripts/run-surge-filesystem-smoke.sh- Build Horizon:
cargo build- Build Surge:
./scripts/build-surge-toolchain.sh --source-path ../surge --output-dir "$PWD/.surge/toolchain-bin"
export PATH="$PWD/.surge/toolchain-bin:$PATH"-
Create a filesystem store, for example
/tmp/horizon-surge-store, and write a temporary manifest for the target RID. -
Stage
0.2.0-smoke.1with the same binary:
./scripts/stage-surge-artifacts.sh --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.1 --binary target/debug/horizon --main-exe horizon- Pack, push, and promote version 1:
surge --manifest-path <SMOKE_MANIFEST> pack \
--app-id <APP_ID> \
--rid <RID> \
--version 0.2.0-smoke.1 \
--artifacts-dir "$PWD/.surge/artifacts/<APP_ID>/<RID>/0.2.0-smoke.1" \
--output-dir "$PWD/.surge/packages"
surge --manifest-path <SMOKE_MANIFEST> push --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.1 --channel beta --packages-dir "$PWD/.surge/packages"
surge --manifest-path <SMOKE_MANIFEST> promote --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.1 --channel stable- Run the generated installer headless:
chmod +x .surge/installers/<APP_ID>/<RID>/Setup-<RID>-<APP_ID>-stable-offline-gui.bin
.surge/installers/<APP_ID>/<RID>/Setup-<RID>-<APP_ID>-stable-offline-gui.bin --headless- Verify the install tree:
- install root:
~/Library/Application Support/horizon - active app dir:
~/Library/Application Support/horizon/app - runtime manifest:
~/Library/Application Support/horizon/app/.surge/runtime.yml - applications shortcut:
~/Applications/Horizon.app - desktop shortcut:
~/Desktop/Horizon.app
-
Launch the installed Horizon binary once from
~/Library/Application Support/horizon/app/horizon. -
Capture:
mkdir -p /tmp/horizon-surge-smoke
screencapture -x /tmp/horizon-surge-smoke/install-launch.png- Stage and pack version 2:
./scripts/stage-surge-artifacts.sh --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.2 --binary target/debug/horizon --main-exe horizon
surge --manifest-path <SMOKE_MANIFEST> pack \
--app-id <APP_ID> \
--rid <RID> \
--version 0.2.0-smoke.2 \
--artifacts-dir "$PWD/.surge/artifacts/<APP_ID>/<RID>/0.2.0-smoke.2" \
--output-dir "$PWD/.surge/packages"- Push version 2 to
betaonly:
surge --manifest-path <SMOKE_MANIFEST> push --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.2 --channel beta --packages-dir "$PWD/.surge/packages"- Confirm no
stableupdate is visible yet:
cargo run --config .surge/smoke/"$RID"/cargo-config.toml -p horizon-ui --example surge-update-smoke -- --app-exe "$HOME/Library/Application Support/horizon/app/horizon"Expected: no update available.
- Promote version 2 to
stable:
surge --manifest-path <SMOKE_MANIFEST> promote --app-id <APP_ID> --rid <RID> --version 0.2.0-smoke.2 --channel stable- Apply the update:
cargo run -p horizon-ui --example surge-update-smoke -- --apply --app-exe "$HOME/Library/Application Support/horizon/app/horizon"- Re-check the install tree:
- runtime manifest version becomes
0.2.0-smoke.2 - previous snapshot exists as
~/Library/Application Support/horizon/app-0.2.0-smoke.1 - relaunch from the installed binary still works
- Capture:
screencapture -x /tmp/horizon-surge-smoke/post-update-launch.pngUse a separate public staging repo for this step, not peters/horizon.
- Mirror the Horizon release workflow in the staging repo. The workflow now defaults Surge storage to the current repo for non-
peters/horizonreleases, so no.surge/surge.ymledit is required. - Cut a stable release in the staging repo, install it on macOS from the generated Surge installer, and confirm
~/Library/Application Support/horizon/app/.surge/runtime.ymlrecords the staging repo inbucket:. - Cut a second stable release in the same staging repo.
- Launch the already-installed app and wait for the Horizon update prompt.
- Click
Download Installerand confirm the browser opens the staging repo URL, not production.
Success criteria:
- prompt appears for the second staged stable release
- opened URL matches
https://github.com/<staging-owner>/<staging-repo>/releases/download/v<version>/... - downloaded installer launches and upgrades the existing install
The smoke is green only if all of these hold on the tested OS:
- both smoke versions pack successfully
- the generated installer runs successfully
- the installed tree contains
.surge/runtime.yml - the installed runtime manifest names the expected
filesystembackend and channel - the
stableinstall ignores a newerbeta-only release - the helper example detects the newer release after promotion to
stable download_and_applycompletes without error- the installed app still launches after update
Check these in order:
- Installer creation failed:
- verify the Surge toolchain directory contains
surge,surge-supervisor,surge-installer,surge-installer-ui, and the native runtime library
- verify the Surge toolchain directory contains
- Installer runs but no app appears under the install root:
- inspect the installer's stderr output and the generated
installer.yml
- inspect the installer's stderr output and the generated
- Install succeeds but update helper cannot find updates:
- inspect
.surge/runtime.ymlfor wrong provider/channel/bucket fields - inspect the filesystem store for
releases.zstand package artifacts under the expected app scope
- inspect
- Update helper sees the update but apply fails:
- inspect
.surge-staging,.surge-cache, and the previousapp-<version>snapshot
- inspect