Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an in-app auto-updater for MarkEdit, implemented as a sandboxed app-side updater flow backed by an embedded XPC service (UpdateInstaller) and a shared Swift package (UpdaterCore) that enforces signature/identity trust rules.
Changes:
- Add
UpdateInstallerXPC service + detached installer runner to stage, verify, and atomically swap app bundles. - Add
UpdaterCoreSwift package (verifier + path rules) with unit tests and CI coverage. - Integrate download/staging/install UX into the app’s updater UI/menu, preferences, and localizations.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| UpdateInstaller/UpdateService.swift | XPC service implementation for staging updates and launching a detached installer. |
| UpdateInstaller/UpdateRunner.swift | Detached installer logic that waits for app exit and swaps in the staged app. |
| UpdateInstaller/UpdaterCore/Tests/UpdaterCoreTests.swift | Unit tests for staging directory rules, host bundle resolution, argument parsing, and identity checks. |
| UpdateInstaller/UpdaterCore/Sources/StagingLocation.swift | Defines and validates updater staging directory naming rules. |
| UpdateInstaller/UpdaterCore/Sources/InstallerArguments.swift | Shared command-line argument building/parsing for installer invocation. |
| UpdateInstaller/UpdaterCore/Sources/HostBundle.swift | Resolves the containing host app bundle from an embedded XPC service URL. |
| UpdateInstaller/UpdaterCore/Sources/BundleVerifier.swift | Implements signature/requirement checks and identity/version verification for updates. |
| UpdateInstaller/UpdaterCore/README.md | Documents the purpose and scope of the UpdaterCore package. |
| UpdateInstaller/UpdaterCore/Package.swift | Declares the UpdaterCore Swift package (macOS 15) and its test target/plugins. |
| UpdateInstaller/UpdateInstalling.swift | Defines the NSXPC interface between the app and installer service. |
| UpdateInstaller/main.swift | Service entrypoint; dispatches into detached install mode and restricts XPC clients. |
| UpdateInstaller/Info.plist | XPC service plist configuration. |
| MarkEditMac/Sources/Updater/AppVersion.swift | Adds compatibility and asset-selection helpers for update archives (universal vs arm64). |
| MarkEditMac/Sources/Updater/AppUpdater+Install.swift | App-side staging/install/commit/discard logic and install error presentation. |
| MarkEditMac/Sources/Updater/AppUpdater+Download.swift | Downloads release assets to a fixed temporary archive name. |
| MarkEditMac/Sources/Updater/AppUpdater.swift | Updater UI flow updates: automatic staging, menu updates, progress animation, and release notes. |
| MarkEditMac/Sources/Main/AppRuntimeConfig.swift | Adds .automatic update behavior option. |
| MarkEditMac/Sources/Main/AppResources.swift | New localized strings/icons for updater UI and menu actions. |
| MarkEditMac/Sources/Main/AppPreferences.swift | Stores staged update metadata (path/version/notes/unapplied). |
| MarkEditMac/Sources/Main/Application/AppDelegate+Document.swift | Minor formatting-only change in window keying condition. |
| MarkEditMac/Sources/Main/Application/AppDelegate.swift | Adds restart update menu item outlet and hooks staging/cleanup into lifecycle. |
| MarkEditMac/Sources/Editor/Controllers/EditorViewController+UI.swift | Minor formatting-only change in presented VC filtering. |
| MarkEditMac/Sources/Editor/Controllers/EditorViewController+Menu.swift | Ensures cancelled termination doesn’t trigger relaunch; relaunch-triggered termination path. |
| MarkEditMac/Resources/Localizable.xcstrings | Adds translations for new updater strings and menu text. |
| MarkEditMac/Modules/Sources/ExtensionCore/Internal/ExtensionEnvironment.swift | Uses non-optional app short version string for extension minAppVersion checks. |
| MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/RunLoop+Extension.swift | Adds RunLoop.performOnMain helper to schedule main-actor work on the main run loop. |
| MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/NSWorkspace+Extension.swift | Minor formatting-only change in app URL lookup. |
| MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/Bundle+Extension.swift | Makes shortVersionString non-optional and adds isAppleSiliconOnly. |
| MarkEditMac/Base.lproj/Main.storyboard | Adds/rewires updater menu items (details/restart) and new SF Symbols resources. |
| MarkEdit.xcodeproj/project.pbxproj | Adds UpdateInstaller target, embeds XPC service, and wires UpdaterCore product dependency. |
| .github/workflows/build-and-test.yml | Runs UpdaterCoreTests in CI in addition to existing schemes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
UpdateInstaller/UpdateRunner.swift:86
touch(path:)opens the app bundle usingO_SYMLINK. For a normal (non-symlink).appdirectory, this flag can causeopen()to fail, making the LaunchServices refresh a no-op. Open the path normally (or verifyopen()succeeds and log failures).
static func touch(path: String) {
let descriptor = open(path, O_RDONLY | O_SYMLINK)
guard descriptor != -1 else {
return
UpdateInstaller/UpdaterCore/Sources/InstallerArguments.swift:29
InstallerArguments(parsing:)does not require the--installflag, so any argv containing--staged/--pidwill be accepted even if it wasn’t intended as an install invocation. SinceinstallFlagis part of the contract (and is howmain.swiftdecides to run the installer), parsing should reject argument lists that don’t include it.
public init?(parsing arguments: [String]) {
let options = Self.options(in: arguments)
guard let stagedPath = options["--staged"], !stagedPath.isEmpty,
let processIdentifier = options["--pid"].flatMap({ Int32($0) }) else {
MarkEditMac/Modules/Sources/AppKitExtensions/Foundation/Bundle+Extension.swift:16
shortVersionStringfalls back to "1.0.0" when missing. That value is also a plausible real app version, and the updater code now treats "1.0.0" as an invalid sentinel. Use an unambiguous sentinel (e.g. "0.0.0") to avoid misclassifying a real 1.0.0 release as invalid.
var shortVersionString: String {
(infoDictionary?["CFBundleShortVersionString"] as? String) ?? "1.0.0"
}
MarkEditMac/Sources/Updater/AppUpdater.swift:59
- The assertion treats version "1.0.0" as invalid, which could trip on a legitimate 1.0.0 release. If the intent is to catch a missing Info.plist value, assert against an unambiguous sentinel (e.g. "0.0.0") that matches
Bundle.shortVersionString’s fallback.
// A removed bad release may require returning to an older version
let currentVersion = Bundle.main.shortVersionString
Logger.assert(currentVersion != "1.0.0", "Invalid current version string")
No description provided.