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

> Discover how vorssaint-utils checks for updates by integrating with the GitHub Releases API. Learn about its state-driven notification system for DMG updates.

- Repository: [vorssaint/vorssaint-utils](https://github.com/vorssaint/vorssaint-utils)
- Tags: internals
- Published: 2026-09-09

---

**vorssaint-utils queries the GitHub Releases API every 60 minutes through [`UpdateService.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/Services/Update/UpdateService.swift)**, which orchestrates network requests, state management, and user notifications. Supporting this core are three additional files:

- **[`UpdateServiceSupport.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/UpdateServiceSupport.swift)** – Provides **semantic version** comparison logic and release candidate selection
- **[`UpdateInstallerSupport.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/UpdateInstallerSupport.swift)** – Handles download validation and installation orchestration
- **[`UpdateHighlightsView.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/UpdateHighlightsView.swift)** – Renders the update-available UI and release notes display

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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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:

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

```

### Triggering Manual Checks

Connect to a "Check for Updates" button:

```swift
@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:

```swift
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:

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

```

## Summary

- **vorssaint-utils** implements update checks through [`UpdateService.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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.