How vorssaint-utils Handles Update Checks: GitHub Releases API Integration Explained

vorssaint-utils queries the GitHub Releases API every 60 minutes through UpdateService.swift, comparing semantic versions locally and surfacing available DMG updates to users via a state-driven notification system.

The vorssaint-utils repository implements a comprehensive self-update mechanism that automatically monitors GitHub Releases for newer versions while respecting user preferences for stability and frequency. At the heart of this system lies a Swift-based service architecture that balances timely notifications with API efficiency.

Core Update Service Architecture

The update-check logic is centralized in Sources/Vorssaint/Services/Update/UpdateService.swift, which orchestrates network requests, state management, and user notifications. Supporting this core are three additional files:

This modular design separates concerns between checking, selecting, and installing updates.

Initialization and Automatic Scheduling

When the application launches, the entry point calls UpdateService.shared.startAutomaticChecks(). This method, implemented around lines 62-98 of UpdateService.swift, performs three critical setup tasks:

  1. Loads persisted preferences – Retrieves previous install results and default settings for beta updates
  2. Schedules periodic checks – Invokes configureAutomaticChecks() to create an hourly Timer (60-minute interval) that repeatedly calls check(manual: false)
  3. Queues immediate validation – Schedules a one-off delayed check 6 seconds after launch (lines 80-84) to ensure users see availability promptly without blocking startup

The service persists user preferences through DefaultsKey.autoCheckUpdates and DefaultsKey.includeBetaUpdates, defined in Sources/Vorssaint/Core/Defaults.swift.

User-Configurable Update Preferences

The system exposes two boolean flags that control check behavior:

autoCheckEnabled (default: true, lines 40-46)
Toggles the hourly background timer. When disabled, the app only checks when manually requested.

includeBetaUpdates (default: matches build type, lines 48-55)
Determines whether pre-release versions appear in update notifications. Beta builds default this to true, while stable builds default to false.

These settings integrate with the UI through standard UserDefaults bindings, allowing real-time preference changes without restarts.

The Update Check Execution Flow

The check(manual:) method (starting line 16) implements the actual network logic with several safeguards:

API Endpoint Selection

The method aborts immediately if running in a developer build or if a check is already in progress (lines 16-21). Based on includeBetaUpdates, it selects between:

  • Latest release endpoint – Used when beta updates are disabled
  • List-of-last-10 endpoint – Used when beta updates are enabled to scan recent pre-releases

Request Configuration

All requests include proper Accept and User-Agent headers and explicitly disable local caching using .reloadIgnoringLocalCacheData (lines 25-28) to ensure fresh data.

Version Parsing and Selection

Upon receiving JSON, the decoder maps responses into GitHubRelease objects. The system scans each release for assets ending with .dmg, creating ReleaseCandidate objects containing:

  • Semantic version tag
  • Pre-release flag
  • Direct DMG download URL
  • File size and release notes

The selection logic resides in UpdateServiceSupport.selectUpdate (lines 99-140 of UpdateServiceSupport.swift), which applies semantic-version comparison rules against the current app version while respecting the beta-inclusion preference.

State Management and User Notifications

UpdateService maintains a reactive state property that drives UI updates through Combine publishers. The possible states include:

  • .available(version:) – Triggered when selectUpdate identifies a newer version; populates availableNotes with formatted release notes and posts a macOS system notification
  • .upToDate – Set when no newer version exists
  • .failed(String) – Captures network failures, malformed JSON, or GitHub API errors (lines 33-38)
  • .checking – Indicates an active request

Notifications are suppressed during manual checks or when the version was previously advertised to prevent alert fatigue.

Staleness-Aware Rechecking

To ensure freshness without API abuse, UI components call checkIfStale(maxAge:) when becoming active (e.g., when the menu bar panel opens). If the last successful check exceeds the supplied maxAge (default 15 minutes), the method triggers a fresh check(manual: false) (lines 92-104). This pattern ensures users see critical updates immediately upon interaction while maintaining the 60-minute background interval.

Complete Implementation Examples

Starting Automatic Checks

Typically invoked from AppDelegate or the SwiftUI app initializer:

// In application didFinishLaunching or @main struct init
UpdateService.shared.startAutomaticChecks()

Triggering Manual Checks

Connect to a "Check for Updates" button:

@IBAction func checkForUpdates(_ sender: Any) {
    UpdateService.shared.check(manual: true)
}

Reacting to State Changes

Bind to the service's state publisher to update UI components:

private var cancellable: AnyCancellable?

func bindUpdateState() {
    cancellable = UpdateService.shared.$state.sink { state in
        switch state {
        case .idle, .checking:   // show spinner
            self.updateStatusView.showProgress()
        case .upToDate:
            self.updateStatusView.showMessage("You’re up‑to‑date")
        case .available(let version):
            self.updateStatusView.showUpdateAvailable(version: version)
        case .downloading(let progress):
            self.updateStatusView.showDownloadProgress(progress ?? 0)
        case .installing:
            self.updateStatusView.showMessage("Installing…")
        case .failed(let error):
            self.updateStatusView.showError(error)
        }
    }
}

Configuring Beta Update Preferences

Link a settings toggle to the service:

// Toggle in Settings UI
@IBAction func toggleBetaUpdates(_ sender: NSButton) {
    UpdateService.shared.includeBetaUpdates = (sender.state == .on)
}

Summary

  • vorssaint-utils implements update checks through UpdateService.swift, which schedules hourly automatic polls via Timer and immediate staleness checks every 15 minutes during UI interaction
  • The system queries the GitHub Releases API with cache-busting headers, selecting between latest-release and recent-releases endpoints based on beta preferences
  • Semantic version comparison occurs in UpdateServiceSupport.swift, filtering for .dmg assets and respecting pre-release flags
  • A state machine drives the user experience through Combine publishers, handling .available, .upToDate, and .failed states with appropriate macOS notifications
  • User defaults in Defaults.swift persist preferences for automatic checking and beta inclusion across app launches

Frequently Asked Questions

How often does vorssaint-utils check for updates?

By default, vorssaint-utils performs a background check every 60 minutes through a repeating Timer created in configureAutomaticChecks(). Additionally, whenever the user opens specific UI panels or the app becomes active, checkIfStale(maxAge:) triggers a fresh check if the last successful query was more than 15 minutes ago.

Can I disable automatic update checks in vorssaint-utils?

Yes. The autoCheckEnabled property (backed by DefaultsKey.autoCheckUpdates) controls the hourly timer. Setting this to false prevents automatic background checks, though users can still manually trigger checks via check(manual: true).

How does vorssaint-utils handle beta versions?

The includeBetaUpdates flag determines whether pre-release versions appear in update notifications. When enabled, the service queries the list-of-last-10 releases endpoint to scan recent betas; when disabled, it queries only the latest stable release. Beta builds of vorssaint-utils default this setting to true, while stable builds default to false.

What happens when vorssaint-utils finds an available update?

When UpdateServiceSupport.selectUpdate identifies a newer version, the service transitions to the .available(version:) state, populates availableNotes with release notes, and posts a macOS system notification. The actual download and installation occur only when the user explicitly initiates the process through downloadAndInstall(), which validates the DMG size against downloadExpectedBytes before handing off to the installation script.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →