fix: stop the updater installing a disk image over the app - #142
Merged
Conversation
The GitHub provider's default matcher takes the first asset whose name contains both the platform and the architecture, and the API returns assets alphabetically -- so OneAgent-darwin-arm64.dmg won over the sibling .zip. detectArchive only recognises .zip and .tar.gz, so the .dmg was treated as "not an archive, pass it through" and the helper moved the disk image onto /Applications/OneAgent.app. The app could no longer launch, and the helper reported success: it swapped a file, and `open` on a .dmg exits 0. ExtractableAssetMatcher filters to formats the updater unpacks before deferring to the upstream matcher, so its sidecar and architecture handling is unchanged. DownloadAndInstall now also rejects a staged artifact that still carries a container suffix. That check has to happen before the user is offered a restart: the swap runs after this process exits, so there is no interface left to report it. UPDATE_NOT_INSTALLABLE is a new code rather than InternalError because the only way forward is a manual download and retrying re-downloads the same asset. The task centre renders a single line and was dropping the hint, which for this failure is the half that names a next step, so failureLine joins the two through the translation table. Refs #139 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paulkm2006
approved these changes
Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #139.
The failure
A v0.4.0 install that accepted the update to v0.5.0 could no longer launch.
/Applications/OneAgent.apphad become a 5.2MB regular file:That size matches
OneAgent-darwin-arm64.dmgexactly.Why
Two upstream behaviours combine, and our asset naming decides whether they fire.
DefaultAssetMatchertakes the first asset whose name contains both the platform and the architecture. The GitHub API returns assets alphabetically, soOneAgent-darwin-arm64.dmgprecedesOneAgent-darwin-arm64.zip. It skips.sig, checksums and-installer., but nothing restricts it to formats the updater can unpack.detectArchivethen recognises only.zip,.tar.gzand.tgz. A.dmgfalls through toarchiveNone, whose contract is "not an archive, hand it to the helper unchanged" — so the helper moved the disk image onto the bundle path.The helper logged success, and it was not wrong on its own terms:
replaceTargetmoved a file, and macOSlaunchshells out toopen, which exits 0 for a.dmgbecause mounting a disk image is a valid open. Both rollback points are conditioned on those two steps failing, so neither fired.The upstream comment at
updater.go:315describes this exact hazard for.zip— "without this step the helper would replace /Applications/MyApp.app (a directory) with the downloaded .zip (a file)" — and.dmgwas simply not on the list.Only reachable once the app is installed somewhere writable. Four earlier attempts ran from the mounted dmg, where App Translocation put the bundle on a read-only volume and the backup step failed, aborting the update.
The change
ExtractableAssetMatcher(internal/binding/update_asset.go) filters to extractable suffixes, then delegates toDefaultAssetMatcher, mapping the index back. Filtering first rather than reimplementing keeps the upstream sidecar, installer and architecture-alias handling (x86_64,aarch64) intact.DownloadAndInstallrejects an uninstallable staged artifact.DownloadedPathis set after extraction, so a container suffix surviving there means extraction did not happen. This check must precede the restart offer — the swap runs after the process exits, and there is no interface left to report a failure then. Defensive on purpose: it holds even if upstream matching changes again.UPDATE_NOT_INSTALLABLEis a new error code.INTERNAL_ERRORmaps to no copy by design (failureCopy.test.ts:48pins that), and this failure needs to say something specific: retrying re-downloads the same asset, so the only way forward is a manual download. It shares exit code 10 since existing numeric codes are pinned by tests.failureLinejoins message and hint for single-line surfaces. The task centre was dropping.hint, which here is the actionable half — "The downloaded update cannot be installed" alone names no next step.Tests
.zipfor all four published platform/arch pairs, driven by v0.5.0's real 9-asset list in API orderDefaultAssetMatcherstill picks the.dmg, so the regression case stays honest; it skips rather than fails if upstream changes, pointing at the filter possibly being redundantDownloadAndInstallrejects.dmg/.zip/.msi/.AppImageand accepts.app, a bare binary, and an unreported path; the error is non-retryable and the artifact name stays in the private cause, not the user-facing messageAppUpdatershows both the message and the manual-download hint in the task centreEach layer was verified load-bearing by neutering it and confirming the tests fail.
Verification
go test ./...,go test -race ./internal/binding ./internal/errors,go vet ./...,go build -tags wails ./cmd/oneagent-desktopall pass; frontend 334 tests pass andpnpm run buildsucceeds;check-docs.pyclean.internal/process/process.goshows up undergofmt -lbut is untouched by this branch and unmodified ingit status— pre-existing.Not covered here
openon a dmg reports success, so there was no rollback. Judging "launched" by an exit code is the underlying weakness.Windows was reasoned about but not verified on hardware: the default matcher already skips
-installer.exe, and the test covers both Windows pairs selecting the.zip.🤖 Generated with Claude Code