Add update checks and in-place auto-update from GitHub Releases - #40
Merged
Merged
Conversation
No Sparkle — zero third-party dependencies — so the mechanism is written out in full and kept small. - `AppVersion`: the `major.minor.patch` in Info.plist and in a `vX.Y.Z` tag. Nil for a `swift run` build, which has no Info.plist. - `ReleaseFeed`: `releases/latest` from the GitHub API, decoded to a `Release` with the first `.dmg` asset. A release without one is still reported and offered as a page. - `UpdateInstaller`: download, `hdiutil attach` read-only, verify the new bundle with a `SecRequirement` built from the running app (Developer ID chain, same Team ID, same bundle identifier, strict + nested), copy next to the current app, two renames, delete the old bundle, detach, then hand `open` to a shell that waits for this PID and terminate. An unsigned or unbundled running copy cannot install and is sent to the release page. - `UpdateCoordinator`: a quiet daily check at launch (skipping a version the user skipped, swallowing network errors) and a talkative manual one from LUTzy ▸ Check for Updates…. Feed and installer are injected for tests. - `UpdateSheet`: one sheet for checking / up to date / available with notes / downloading / installing / failed. Settings gains an Updates section. - `UpdateTests`: 15 tests over versions, feed decoding, both check manners, skip, the interval, install failure, and the on-disk swap. The requirement string was checked by hand against the published v0.1.0 DMG: it passes the real app and fails a wrong team. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViSYWttguWwFQm1PEksQ7K
…update The first end-to-end run swapped the bundle and started the relaunch helper, but the old process stayed up behind its "Installing…" sheet: `NSApp.terminate` sent while a SwiftUI sheet was presented did not end the app. The coordinator now brings the sheet down first, and `relaunch` follows `terminate` with a hard `exit(0)` a second later. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViSYWttguWwFQm1PEksQ7K
…for local tests `log stream --level info --predicate 'subsystem == "com.timvbs.LUTzy"'` now shows a check's outcome and each install stage — download, mount, verify, swap, relaunch — which is the only way to see what the updater did on a machine you are not sitting at. It is also how the end-to-end run was read: accessibility could not see the SwiftUI sheet. `LUTZY_SKIP_NOTARIZE=1 scripts/release-dmg.sh <version>` builds and signs without the two notarization rounds, for a local update test; never for something shipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViSYWttguWwFQm1PEksQ7K
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.
Summary
Built-in update checks and auto-update with no third-party code: the feed is this repo's GitHub Releases, the install is a signature-verified swap of the bundle.
SecRequirementderived from the running copy: Apple-anchored Developer ID chain, same Team ID, same bundle identifier, strict and nested. Then copies it next to the current app, swaps with two renames, removes the old bundle, detaches, and handsopento a shell that waits for this process to exit before relaunching.swift runhas no Info.plist, so no version: the automatic check never runs, and a manual check offers the release page instead of an install. An unsigned bundle is refused the same way.vX.Y.Zwith the DMG attached, which is what v0.1.0 already is.Verification
swift test: 356 tests, 0 failures.UpdateTestscovers version parsing and ordering, feed decoding (with and without a DMG, bad tag), both check manners, skipped versions, the 24h interval, the preference, install failure surfacing, and the on-disk swap including a read-only destination.codesign --verify -R=against the app inside the publishedLUTzy-0.1.0.dmg: it passes, and a wrong Team ID fails.scripts/release-dmg.shfrom this branch, notarized, and published. A copy of the same build stamped0.1.0and re-signed was installed in/Applications; on launch the daily check found 0.1.1 and put up the sheet; Install and Relaunch downloaded, verified, and swapped the bundle (/Applications/LUTzy.appat 0.1.1, signature valid, nothing mounted, no staged/retired bundle or temp directory left). First run: the old process did not quit —NSApp.terminateunder the presented sheet was ignored — and the relaunch helper sat waiting; ending the process by hand made it open the new build. Fixed in the second commit (sheet down first, hard exit if terminate is ignored). Second and third runs, from a 0.1.0 stand-in built from the fixed source (LUTZY_SKIP_NOTARIZE=1), read through the newos.Loggerstream: check → available → Install and Relaunch → downloaded, mounted, signature verified, swapped, helper started, process gone within a second, new process up as 0.1.1. No mount, no staged or retired bundle, no temp directory left.Test plan
scripts/release-dmg.sh 0.1.1and publish it asv0.1.1with the DMG.🤖 Generated with Claude Code
https://claude.ai/code/session_01ViSYWttguWwFQm1PEksQ7K