Skip to content

In-app auto updater - #1663

Merged
cyanzhong merged 1 commit into
mainfrom
develop
Aug 5, 2026
Merged

In-app auto updater#1663
cyanzhong merged 1 commit into
mainfrom
develop

Conversation

@cyanzhong

Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 UpdateInstaller XPC service + detached installer runner to stage, verify, and atomically swap app bundles.
  • Add UpdaterCore Swift 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.

Comment thread UpdateInstaller/UpdaterCore/Sources/BundleVerifier.swift
Comment thread UpdateInstaller/main.swift
Comment thread UpdateInstaller/UpdateService.swift Outdated
Comment thread UpdateInstaller/UpdateService.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 using O_SYMLINK. For a normal (non-symlink) .app directory, this flag can cause open() to fail, making the LaunchServices refresh a no-op. Open the path normally (or verify open() 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 --install flag, so any argv containing --staged/--pid will be accepted even if it wasn’t intended as an install invocation. Since installFlag is part of the contract (and is how main.swift decides 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

  • shortVersionString falls 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")

Comment thread UpdateInstaller/UpdateService.swift
@cyanzhong
cyanzhong merged commit 008e6cf into main Aug 5, 2026
2 checks passed
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.

2 participants