Skip to content

Update Boost from inside the app - #4

Merged
Kernel-Hunter merged 4 commits into
mainfrom
auto-update
Oct 31, 2026
Merged

Kernel-Hunter merged 4 commits into
mainfrom
auto-update

Conversation

@Kernel-Hunter

Copy link
Copy Markdown
Owner

Boost can now update itself. When a new version is out, the app shows it with a link to the release notes and an Install Update button. One more click relaunches into the new version. Nobody has to watch GitHub, delete the old app and reinstall.

It uses Sparkle 2.10.0, pinned exactly in Package.swift.

What the user sees

  • Check for Updates… in the Boost menu, under About.
  • An Updates section in Settings with "Check for updates automatically" and a Check Now button.
  • Sparkle's own prompt on the second launch, asking whether to check automatically. Nothing goes over the network before that answer. The first launch sends nothing.
  • Automatic installs are off (SUAllowsAutomaticUpdates is false), so the prompt has no "install automatically" checkbox and nothing is installed without a click. No system profile is sent.

Build

  • build.sh copies Sparkle.framework into Contents/Frameworks. The binary gets an @executable_path/../Frameworks rpath through Package.swift.
  • Signing is now inside out (Sparkle's XPC services, Autoupdate, Updater.app, the framework, then the app) instead of --deep. The same steps run with the local identity and with the ad-hoc fallback CI uses. The universal arm64 + x86_64 check still runs, and the framework is universal too.
  • Info.plist gets SUFeedURL, SUPublicEDKey and SUAllowsAutomaticUpdates.
  • VERSION is now 1.5.0.

Feed

The feed is docs/appcast.xml, served by the existing Pages site at https://kernel-hunter.github.io/boost/appcast.xml. I picked Pages over raw.githubusercontent.com because Pages already serves docs/ from main over HTTPS, and the URL is the project's own site. That URL stays the same if the repo's layout or default branch changes later, and the file is served as XML rather than text/plain. It is empty until the first release is added.

Releasing

Scripts/appcast.sh <version> runs after the release workflow has published the tag. It downloads Boost.zip and checks it against the SHA-256 in the release notes. Then it checks that the app inside has the right version and our public key, signs the zip with sign_update using the Keychain key, updates the item in the feed and commits. Signing stays on the maintainer's Mac, so no key goes to GitHub. Steps are in docs/releasing.md.

Docs

The README, SECURITY.md and the website all said Boost makes no network requests. They now say what the check sends (one HTTPS request for the feed, no identifiers, your IP and a User-Agent with the version), when it happens and how to turn it off. The README has a short Updating note. SECURITY.md also said release builds are notarized, which was wrong, so that is fixed too.

Testing

  • swift build, ./Scripts/test.sh (324 tests) and ./Scripts/build.sh --no-install pass. codesign --verify --deep --strict passes on the bundle and Sparkle.framework is embedded.
  • Real update between two ad-hoc builds, both with a separate bundle id (boost.local.app.updatetest) and pointed at a local python3 -m http.server. The feed was generated by appcast.sh itself against a fake release. 1.4.99 run from a temp folder made no request on first launch. Check for Updates fetched the feed, Install Update downloaded the zip, and Install and Relaunch replaced the bundle in place. On disk it then read 1.5.0, with the same CDHash as the 1.5.0 build, and the relaunched app was running.
  • A 1.5.1 zip with a signature that does not match was refused ("The update is improperly signed and could not be validated"). The app stayed on 1.5.0.
  • The Settings toggle writes Sparkle's SUEnableAutomaticChecks both ways.

Ad-hoc to ad-hoc works because of how SUUpdateValidator.m decides. An app update passes if the EdDSA signature checks out against the old app's key, or if the new app's code signature matches the old one. Ad-hoc builds never match each other, since their designated requirement is their own cdhash, but EdDSA passes. Sparkle then only requires the new app's own signature to be valid. The inside-out signing in build.sh is there to keep that true.

Before the first release

Merge, then tag v1.5.0. After CI publishes it, run ./Scripts/appcast.sh 1.5.0 and push. People on 1.4.1 or older need to update once the old way. The Homebrew tap change (auto_updates true) is a separate PR on homebrew-boost and should only merge after the cask is on 1.5.0.

Updating meant noticing a release on GitHub, deleting the old app and
installing the new one by hand, so most people stayed on whatever they first
installed. Boost now uses Sparkle 2.10.0 (pinned exactly) to find a new
version, show it, and install it when the user clicks Install.

Nothing is checked until the user agrees: Sparkle asks on the second launch,
and Check for Updates in the Boost menu and the toggle in Settings work at
any time. Automatic installs are off and no system profile is sent.

build.sh embeds Sparkle.framework in Contents/Frameworks and signs inside
out (Sparkle's helpers, the framework, then the app) instead of --deep, for
both the local identity and the ad-hoc fallback CI uses. Sparkle accepts an
update between two ad-hoc builds because it trusts the EdDSA signature on
the zip and only requires the new app's own code signature to be valid.
That was tested end to end with two ad-hoc builds.
The app reads docs/appcast.xml from the project's GitHub Pages site.
Scripts/appcast.sh adds a published release to it: it downloads the zip CI
built, checks it against the SHA-256 in the release notes, checks the app
inside has the right version and public key, signs it with the EdDSA key
from the maintainer's Keychain, and commits the feed.

Signing stays on the maintainer's Mac. As a GitHub secret the key would let
anyone who got into the repo push an update to every install.
docs/releasing.md has the steps.
README, SECURITY.md and the website all said Boost makes no network requests
and has no update check. With in-app updates that is no longer true, so say
exactly what happens: one HTTPS request for the feed, no identifiers, only
after the user agrees or clicks Check for Updates, and how to turn it off.

Also corrects SECURITY.md, which said release builds are notarized. They are
ad-hoc signed by CI and not notarized.
@Kernel-Hunter
Kernel-Hunter merged commit 613b54a into main Oct 31, 2026
1 check passed
@Kernel-Hunter
Kernel-Hunter deleted the auto-update branch October 31, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant