WinAppMigrator

CI Jobs

For developers. What .github/workflows/ci.yml runs, where, and what each job proves. The release workflow is on Releases and signing, and the website's is at the end of this page.

When It Runs

On every push to main, on every pull request to main, and when started by hand. The workflow only has read access to the repository.

The Jobs at a Glance

ci.yml defines 12 jobs. Three of them run once for each of three Windows images, so each run of the workflow is 18 jobs in all.

Job Name in GitHub Runs on Waits for
windows-quality Windows quality gates windows-latest
elevated-round-trip Elevated round trip windows-latest
elevated-offline-and-profiles Elevated offline and profiles windows-latest
elevated-packaging Elevated packaging windows-latest
elevated-footprint Elevated footprint windows-latest
elevated-big-package Elevated big package windows-latest
move-build Move between PCs, build windows-latest
move-install Move between PCs, install on ... windows-latest, windows-2022, windows-11-arm move-build
real-apps-build Real apps, build windows-latest
real-apps-install Real apps, install on ... windows-latest, windows-2022, windows-11-arm real-apps-build
real-apps-runtime Real apps, a PC with no runtime windows-latest
real-windows Real windows on ... windows-latest, windows-2022, windows-11-arm windows-quality, move-build

The README describes these images as Windows Server 2025, Windows Server 2022, and Windows 11 on Arm. GitHub's runners are elevated and thrown away after each job, which is why the jobs that change a machine run only there. Every job that builds Rust installs the toolchain at the version pinned in rust-toolchain.toml, 1.99.1.

Every job except Windows quality gates and Move between PCs, install sets WAM_ELEVATED_E2E=1. With GitHub's own GITHUB_ACTIONS and RUNNER_ENVIRONMENT, it is what lets the elevated_* tests and the real-app and real-window scripts run. The end-to-end test needs only GitHub's two, and check-moved-app.ps1 has no such check of its own, so neither should ever be run on a real PC. See Building and testing.

What Each Job Proves

Windows Quality Gates

Runs every check a change has to pass: cargo fmt --check, cargo check and cargo clippy with --all-targets, cargo test --workspace --all-features, cargo audit, cargo deny check, then a release build of the stub, a release build of the client for Windows 7 with scripts/build-client.ps1, and a release build of everything. It then checks the app holds the stub and the client it just built, byte for byte, with check-stub-embedded.ps1, which takes -Name client for the client. Its toolchain has rust-src as well, which the client's build needs.

Its tests include Network PC…: the app's real link code pairs with the client's own serving code over the runner's loopback, and reads through it. Nothing there runs the client program itself, takes a snapshot, or opens the firewall.

Here the end-to-end test installs for real, and fails rather than skipping if it cannot.

It also builds a harmless stand-in for planted code, a D3D12Core.dll that only says it ran, and hands it and the app on, for a day, to the Real windows jobs.

Proves: the code is formatted, lint-free, tested, free of known advisories, within dependency policy, and that the release build carries its stub and its client.

Elevated Round Trip

Runs elevated_round_trip.rs. It sets up a harmless fake app with files, services, a driver entry, scheduled tasks, registry values, environment variables, Path entries, and shortcuts. Another app is tied into Windows in every way there is, beside another app's entries. It builds an installer with the real code, removes the app, installs, and checks the machine with Windows' own tools, not the app's verifier. Hand-written packages ask for what the rules refuse, and for what is already there. Each runtime's download is really fetched and checked as Microsoft's.

It then takes the install back out every way that can happen, comparing the machine before and after: an uninstall run from the command Windows has for it, with and without changes by another program in between, a failure late in an install, and an installer killed part-way and while undoing that. A real account that is not an administrator tries to change what an uninstaller trusts.

Proves: installing, the rules, rollback, and uninstalling work on a real machine, and leave it as it was.

Elevated Offline and Profiles

Runs elevated_offline_and_profiles.rs, on a clean runner of its own. It makes a real NTFS VHD laid out as a Windows install, with registry hives made to be another PC's. Its values point back at a marker on the runner in every way they can be written, and links on it lead back there. The app is removed from the runner, the drive is scanned at another letter, and the drive's contents are hashed before and after. A scan is killed with hives loaded, to see the next start clean them up. It also makes a real second account whose registry is not loaded, and compares what the task reader says of every real task with schtasks.

Proves: building from a connected drive works, follows nothing off the drive, writes nothing to it, and keeps accounts apart.

Elevated Packaging

Runs elevated_packaging.rs. A file is written to the whole time it is being built from. The package is compared with its manifest by .NET's own zip reader. A two-file installer's loader is given a valid signature from a throwaway certificate, and its data file is then changed by one byte, swapped for another package, cut short, removed, and replaced by a link, with Get-AuthenticodeSignature saying what it sees of each. A small VHD with too little room is what an install and a build are refused on, before anything changes, and packages say they need what the runner is not. It measures how much a build and an install really read. It also runs the code with TEMP, ProgramData, ProgramFiles, and SystemRoot all set wrong, to show nothing here goes by them.

Proves: builds and installs read and leave what they should, two-file installers hold together under a signature, and refusals happen before any change.

Elevated Footprint

Runs elevated_footprint.rs. A fake app has things to leave out, a hidden part to add, and things that must never be offered: a shared folder, another app's, a link, what is named like Windows', and hidden entries that name each of those. What is in the package is read with .NET's zip reader, and what is on the machine with sc.exe, schtasks.exe, and Get-FileHash. Then it is uninstalled, and made to fail part-way.

Proves: what a person chooses on the screen is what is built and installed, and what must never be offered is not.

Elevated Big Package

Runs elevated_big_package.rs, with up to two hours and a runner to itself. It builds, installs, and checks a real package past 4 GiB, with a real file of over 4 GiB of data that does not compress. Get-FileHash checks what came out, .NET reads the data file as a zip, and Windows itself starts the loader.

Proves: the two-file installer works at the size it exists for.

Move Between PCs, Build and Install

move-build runs elevated_move.rs: it sets up a fake app, builds its installers with the real code, and uploads them, unsigned, for a day. Then move-install runs on three fresh runners, one per Windows image. Those check out only .github/scripts, build nothing, and run nothing of the project's but those installers, check-moved-app.ps1, which installs them and looks with Windows' own tools, and run-program.ps1, which it loads to run a program and keep what it prints.

Proves: an installer built on one machine installs on another that has never seen the app, on each kind of Windows.

Real Apps, Build and Install

real-apps-build installs five real open-source apps from their publishers, at the exact versions and SHA-256 hashes in real-apps.json: 7-Zip, PuTTY, Eclipse Mosquitto, windows_exporter, and Git LFS. It checks each works, then builds their installers the way the app does, with both options off and no runtime bundled, and looks through each package for the runner's own account, folders, and tokens before uploading anything. A second step builds again with User Data on, uploads nothing, and shows each account value arrive and go.

real-apps-install then runs on three fresh runners. On each, check-moved-real-apps.ps1 removes any copy of the apps the runner came with, shows every check fails, installs, shows every check passes, removes each app by the command its entry recorded, shows every check fails again, and compares the machine with how it was. It does this once where each app was, and once in a folder of another name. Each check asks Windows to use the thing: pack and unpack an archive, start a COM class, open a file type, start a shortcut, and have a service answer a real client.

These jobs get no secrets and keep no credentials from the checkout. What they upload is a redistribution of those apps, for a day, so every app on the list has a license that allows it.

Proves: real apps from real setups, custom, MSI, NSIS, and Inno Setup, move and work, and come back out cleanly.

Real Apps, a PC with No Runtime

Installs Mosquitto from its publisher, builds its installer with the Visual C++ runtime inside, then takes that runtime off the runner and shows the broker cannot start. It installs with TEMP set to a folder nothing may use, and has the broker serve a client. Nothing is uploaded, since Microsoft's license does not cover passing the runtime on.

Proves: a runtime inside an installer really arrives on a PC without it. This is the only runtime installed for real by CI.

Real Windows

Downloads the app and a fake app's installers, and opens the app, an installer, and an uninstaller on each of the three images, whose logs show a software renderer and a 1024x768 screen. It clicks nothing: it takes a picture, checks each window fits what Windows says a window can use and is in the right light or dark mode, and closes it. It does the same with graphics forced not to start, to see what a person is told. It plants the stand-in D3D12Core.dll where the environment points Direct3D, and checks it never runs. The pictures are kept for a day.

Proves: the real windows open, fit the screen, follow the chosen mode, and cannot be made to load planted code.

The Website

.github/workflows/pages.yml is a workflow of its own. It builds the website from the pages of this wiki and the files in site/, and publishes it with GitHub Pages. GitHub keeps a wiki out of search engines until its repository has 500 stars, so the website is where a search finds these pages.

It runs when site/, docs/images/, the build script, or the workflow changes on main, when this wiki is edited, after a release is published, once a day, and when started by hand. So a change to a page here reaches the website by itself, within a day and usually in a minute or two. .github/scripts/build-site.py sends each page to GitHub's own Markdown renderer, the one this wiki uses, so the website says what the wiki says. After publishing, it tells Bing, and the search engines that share its index, which pages are new or changed, through IndexNow. If that service is down, that is a warning, and the website is still published.

Proves: every link and #anchor between the pages, and every picture the website holds itself, leads somewhere. The build fails on one that does not.

What CI Does Not Do

See Requirements and testing.

Last changed 2026-11-11. Describes WinAppMigrator as of its latest release, 0.6.0.