Conversation
The installer existed with nothing to drive it. This adds the client the release plan's updater boundary described, meeting the conditions it set: signature, publisher and embedded version all verified before elevation, and the /D= target computed from the running executable rather than read from the uninstall registry key, which anything running as the user can write. GitHub Releases is the feed, so there is no manifest server to run and no second place a version is recorded. A tag that is not a version this project publishes is refused rather than string-compared, and the asset has to be named for the version the tag claims, so a file attached beside the real installer cannot be served in its place. An updater executes code that arrived over the network, with elevation, so what it refuses matters more than how it installs. The host allowlist is applied after redirects, because that is where the host can change. The size ceiling holds against Content-Length and again while the bytes arrive, since a hostile server owes us no honest header. The digest is taken while streaming and compared again before launch, closing the gap in which anything able to write to the download directory could swap the file. Authenticode must report Valid, with this project's thumbprint and publisher name, and the installer's own embedded FileVersion must match the release: without that last check, re-uploading an older but still validly signed installer under a newer asset name would roll every install back onto a build whose faults are already fixed. Two things the reference implementation does are deliberately not carried over. It terminates the running application before installing, which would contradict the promise that maintenance never force-kills a copy mid-transfer; this verifies and hands over instead, and says so before the elevation prompt so the installer's refusal does not read as a broken update. And it rejects any tag that is not X.Y.Z, which would make the updater blind to Offloader's own first packaged release. The grammar here is the one versioning.py already enforces, with a test asserting both orderings agree, because a disagreement would let an update install a build Windows then considers older. The in-app check, notification and progress surface stay deferred.
The assertion hardcoded a Windows path, and a backslash is not a separator off Windows: install_target resolves the running executable, so on Linux and macOS the literal resolved against the working directory and the test failed everywhere except where it was written. The property it checks is platform-independent, so it now uses a real directory and the fallback is asserted separately.
owenpkent
left a comment
There was a problem hiding this comment.
Reviewed feed discovery, version/asset selection, download handling, installer verification, installed-location handling, and the CLI handoff. The actual feed cannot deliver the planned beta-to-beta updates; details inline.
Additional updater-security findings are supplied privately under SECURITY.md, including their affected call paths and suggested regression coverage. This public comment omits those reproductions. Validation for this review was static code inspection and verification of GitHub's documented release-feed behavior; no downloaded installer was executed.
| #: Where releases are published. Pinned deliberately: this URL is compiled into | ||
| #: every shipped build, so moving it orphans every install that already exists. | ||
| #: Treat a rename or transfer of the repository as a breaking change. | ||
| FEED_URL = "https://api.github.com/repos/owenpkent/offloader/releases/latest" |
There was a problem hiding this comment.
[P2] Use a feed that can discover the beta releases being shipped.
GitHub's /releases/latest endpoint returns a published full release and excludes releases marked as prereleases. Consequently an installed 0.1.0b1 cannot discover 0.1.0b2 or 0.1.0rc1 through this URL, despite the prerelease ordering supported below. If the repository has only beta releases, the endpoint does not return any eligible candidate. This also conflicts with the candidate workflow's use of --prerelease.
Select the greatest eligible version from the releases collection, or define explicit stable/beta channels and their selection rules. Add a feed-level test containing multiple beta/RC releases, a stable release, and drafts, rather than testing only version comparison against a hand-picked single payload.
Reference: GitHub's latest-release API contract.
/releases/latest is documented as returning the newest published full release and excluding prereleases. Every release this project has planned so far is a prerelease: the candidate workflow publishes with --prerelease, and the first packaged build is 0.1.0b1. So an installed 0.1.0b1 could not discover 0.1.0b2 or 0.1.0rc1 through that endpoint, and a repository holding only betas answered with nothing at all. The module already went out of its way not to reject prereleases the way Alpha-OSK's updater does, and then asked an endpoint that had already dropped them. The feed is the collection now. It is ordered by creation date rather than by version, so every entry is read and the greatest eligible one wins: a patched 0.1.0b2 published after 0.1.0rc1 is listed above it without being above it. Drafts are skipped, because a draft is visible to anyone who can write to the repository and its assets are not published, so offering one would hand an installer to this machine before the release exists for anybody else. Which releases an install is offered is now stated rather than implied. Newer, and on the channel it is already on: a build that is itself a prerelease is testing the prereleases and takes the next one, and a stable install is offered only stable releases. 0.1.0b1 finding 0.1.0b2 must not also mean 1.0.0 finding 1.0.1b1. A prerelease install still takes the stable release when it arrives, because 0.1.0b2 < 0.1.0 in the existing ordering, which is how a beta tester ends up on the shipping build without doing anything. Changing FEED_URL is free today and would not be later: it is compiled into every shipped build, and nothing has shipped. Eight feed-level tests against a collection holding betas, a release candidate, a stable release and a draft, rather than version comparison against one hand-picked payload. One of them covers an unusable entry not hiding the rest of the feed. docs/updates.md updated to match.
…ring This branch exists to make the docs describe the checkout, and one of the claims it added does not. README, the release plan and updates.md all present "offloader update finds a release" as working, and the version ordering section explains at length that prereleases are ordered rather than rejected. The endpoint underneath is /releases/latest, which GitHub documents as returning the newest published full release and excluding prereleases. The first packaged release is planned as 0.1.0b1, so an installed beta cannot discover its successor, and a repository holding only the betas the candidate workflow publishes with --prerelease answers with nothing at all. The ordering is right; the endpoint is wrong. All three places now say so and point at the correction pending on #13, and the release plan no longer counts discovery as a gate that can be signed off. Nothing here claims the fix: it states the limit as the checkout currently has it, which is what this branch is for.
Summary
The installer landed in #7 with nothing to drive it. This is the update client the release plan's Updater boundary section described, and it meets the conditions that section set: signature, publisher and embedded version verified before elevation, and the
/D=target computed from the running executable rather than read from the uninstall registry key.Exit status distinguishes "nothing to do" (0) from "an update exists but was not applied" (1) from a verification failure (2), so it can be scripted.
Two deliberate divergences from alpha-osk
Worth reviewing first, because they are where I did not copy the reference:
taskkills the app and a helper process restarts it. Offloader's installer refuses maintenance while a transfer is in flight and exits non-zero, anddocs/data-safety.mdmakes that a promise. So this verifies and hands over. The command prints the consequence before the UAC prompt, because an operator who does not know it reads the installer's refusal as a broken update.X.Y.Ztags. Offloader's first packaged release is planned as0.1.0b1, so that rule would make the updater blind to the very build it ships in. The grammar here is the onebuild/windows/versioning.pyalready enforces, andtest_the_ordering_agrees_with_the_windows_version_fieldsasserts both orderings agree across a list of versions — a disagreement would let an update install a build Windows then considers older than the one it replaced.What it refuses
An updater runs network-delivered code with elevation, so the refusals are the design:
Content-Lengthand mid-streamValid+ our thumbprint + our publisher nameFileVersionmatches the releaselatestor1.0.3-evilbeing string-compared into an upgradeOffloader-Setup-{version}.exe/D=is emitted last and unquoted, the two rules NSIS enforces silently. Certificate constants are duplicated frombuild/windows/sign.pyrather than imported, since the build scripts are not shipped — flagging that as the one bit of duplication, in case you would rather move them into the package.Not included
download()takes aprogresscallback so the GUI can wire straight into it..github/workflows/release.ymlstill does not exist. Worth knowing: alpha-osk has no release automation either (releases are built andgh release created by hand), so this is net-new work rather than parity. Happy to do it next.Test plan
tests/test_update.py— 62 tests, mostly refusals: unparseable versions fail closed, prerelease ordering, the cross-check againstversioning.py, malformed feeds, wrong/renamed assets, non-HTTPS and off-allowlist hosts, a redirect to another host, the ceiling both ways, digest-changed-after-download, each signature rule failing on its own, thumbprint compared without spacing or case,/D=shape, and a declined UAC prompt reported as such rather than as a failed downloadruff check src tests scripts build/windowscleanoffloader updateagainst the real feed with no releases published returns "0.1.0 is the newest release available", exit 0 (fails closed on the 404 rather than raising)