From cc0029ee975fb262026559197c7a00e5f81da73e Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 16:20:11 +0200 Subject: [PATCH 1/4] feat(dependencies): add Sparkle package as a dependency and update Package.resolved --- InputMetrics.xcodeproj/project.pbxproj | 17 +++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/InputMetrics.xcodeproj/project.pbxproj b/InputMetrics.xcodeproj/project.pbxproj index dc47689..75fa1b3 100644 --- a/InputMetrics.xcodeproj/project.pbxproj +++ b/InputMetrics.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + DFEF2B492F7ABE0D00B1FB6B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = DFEF2B482F7ABE0D00B1FB6B /* Sparkle */; }; GRDB_PKG /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = GRDB_REF /* GRDB */; }; /* End PBXBuildFile section */ @@ -57,6 +58,7 @@ buildActionMask = 2147483647; files = ( GRDB_PKG /* GRDB in Frameworks */, + DFEF2B492F7ABE0D00B1FB6B /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -109,6 +111,7 @@ name = InputMetrics; packageProductDependencies = ( GRDB_REF /* GRDB */, + DFEF2B482F7ABE0D00B1FB6B /* Sparkle */, ); productName = InputMetrics; productReference = APP_PRODUCT /* InputMetrics.app */; @@ -156,6 +159,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( GRDB_PACKAGE /* XCRemoteSwiftPackageReference "GRDB.swift" */, + DFEF2B472F7ABE0D00B1FB6B /* XCRemoteSwiftPackageReference "Sparkle" */, ); preferredProjectObjectVersion = 77; productRefGroup = PRODUCTS_GROUP /* Products */; @@ -460,6 +464,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + DFEF2B472F7ABE0D00B1FB6B /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.9.1; + }; + }; GRDB_PACKAGE /* XCRemoteSwiftPackageReference "GRDB.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/groue/GRDB.swift.git"; @@ -471,6 +483,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + DFEF2B482F7ABE0D00B1FB6B /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = DFEF2B472F7ABE0D00B1FB6B /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; GRDB_REF /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = GRDB_PACKAGE /* XCRemoteSwiftPackageReference "GRDB.swift" */; diff --git a/InputMetrics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/InputMetrics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f3a5ce0..48192e3 100644 --- a/InputMetrics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/InputMetrics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9ae88ccc993f2bc32f27592fda1ef3ef4b36a656f8afceaeca43ca99c7d6dbae", + "originHash" : "91a095bb8faecc98f5715f1db6700ad01222bfd82cc6aa394830b048bf30c6de", "pins" : [ { "identity" : "grdb.swift", @@ -9,6 +9,15 @@ "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", "version" : "6.29.3" } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "066e75a8b3e99962685d6a90cdd5293ebffd9261", + "version" : "2.9.1" + } } ], "version" : 3 From 54f984555c54b58d6dc1a436472b8b403e868d1e Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 16:22:50 +0200 Subject: [PATCH 2/4] feat(sparkle): add Sparkle auto-update support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Initialize SPUStandardUpdaterController in AppDelegate - Add "Check for Updates…" to right-click context menu - Set SUFeedURL to GitHub Pages appcast in Info.plist - Add appcast generation + EdDSA signing step to release workflow - Add scripts/generate-appcast.sh for local testing Requires SPARKLE_PRIVATE_KEY secret in GitHub repo settings. Generate key pair with: generate_keys (bundled with Sparkle). Closes #246 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/release.yml | 54 +++++++++++++++++++++ InputMetrics/InputMetrics/AppDelegate.swift | 14 ++++++ InputMetrics/InputMetrics/Info.plist | 4 ++ scripts/generate-appcast.sh | 46 ++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100755 scripts/generate-appcast.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f49a14a..45e52fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,6 +156,60 @@ jobs: TAG=${{ github.event.release.tag_name }} gh release upload "$TAG" "InputMetrics-${TAG}.dmg" --clobber + - name: Generate and sign appcast + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=${{ github.event.release.tag_name }} + VERSION=${TAG#v} + DMG="InputMetrics-${TAG}.dmg" + + # Locate sign_update bundled with the resolved Sparkle package + SPARKLE_DIR=$(find ~/Library/Developer/Xcode/DerivedData -name "sign_update" 2>/dev/null | head -1 | xargs -I{} dirname {} 2>/dev/null || true) + if [ -z "$SPARKLE_DIR" ]; then + SPARKLE_DIR=$(find .build -name "sign_update" 2>/dev/null | head -1 | xargs -I{} dirname {} 2>/dev/null || true) + fi + + # Sign the DMG and capture EdDSA signature + SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | "$SPARKLE_DIR/sign_update" "$DMG" --ed-key-file -) + + DMG_SIZE=$(stat -f%z "$DMG") + RELEASE_DATE=$(date -u "+%a, %d %b %Y %H:%M:%S +0000") + DOWNLOAD_URL="https://github.com/owieth/InputMetrics/releases/download/${TAG}/${DMG}" + + cat > docs/appcast.xml << EOF + + + + InputMetrics + https://owieth.github.io/InputMetrics/appcast.xml + InputMetrics update feed + en + + Version ${VERSION} + ${RELEASE_DATE} + ${VERSION} + ${VERSION} + 15.0 + https://github.com/owieth/InputMetrics/releases/tag/${TAG} + + + + + EOF + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/appcast.xml + git commit -m "chore(release): update appcast for ${TAG}" + git push origin HEAD:main + - name: Cleanup keychain and provisioning profile if: always() run: | diff --git a/InputMetrics/InputMetrics/AppDelegate.swift b/InputMetrics/InputMetrics/AppDelegate.swift index 7d07c46..8e368ac 100644 --- a/InputMetrics/InputMetrics/AppDelegate.swift +++ b/InputMetrics/InputMetrics/AppDelegate.swift @@ -1,6 +1,7 @@ import SwiftUI import AppKit import os +import Sparkle @MainActor class AppDelegate: NSObject, NSApplicationDelegate { @@ -11,6 +12,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var backgroundActivity: NSObjectProtocol? private var keyboardPermissionTimer: Timer? private var isQuittingFromMenu = false + private let updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) func applicationDidFinishLaunching(_ notification: Notification) { // Prevent App Nap from suspending background event monitoring @@ -111,6 +117,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Settings", action: #selector(openSettings), keyEquivalent: ",")) menu.addItem(NSMenuItem.separator()) + let updateItem = NSMenuItem(title: "Check for Updates…", action: #selector(checkForUpdates), keyEquivalent: "") + updateItem.target = self + menu.addItem(updateItem) + menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit InputMetrics", action: #selector(quitApp), keyEquivalent: "q")) statusItem?.menu = menu @@ -140,6 +150,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { WindowManager.shared.openSettingsWindow() } + @objc private func checkForUpdates() { + updaterController.checkForUpdates(nil) + } + @objc private func quitApp() { isQuittingFromMenu = true NSApp.terminate(nil) diff --git a/InputMetrics/InputMetrics/Info.plist b/InputMetrics/InputMetrics/Info.plist index 1fad40e..ee21343 100644 --- a/InputMetrics/InputMetrics/Info.plist +++ b/InputMetrics/InputMetrics/Info.plist @@ -43,6 +43,10 @@ + SUFeedURL + https://owieth.github.io/InputMetrics/appcast.xml + SUEnableAutomaticChecks + LSApplicationCategoryType public.app-category.utilities NSPrincipalClass diff --git a/scripts/generate-appcast.sh b/scripts/generate-appcast.sh new file mode 100755 index 0000000..43efe97 --- /dev/null +++ b/scripts/generate-appcast.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Generates appcast.xml for Sparkle auto-updates. +# Usage: ./scripts/generate-appcast.sh +# Requires: Sparkle's generate_appcast tool (bundled with Sparkle SPM package) +# The output appcast.xml is written to docs/appcast.xml for GitHub Pages hosting. + +set -euo pipefail + +VERSION="${1:?Usage: $0 }" +DMG_PATH="${2:?Usage: $0 }" +REPO="owieth/InputMetrics" +DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/InputMetrics-v${VERSION}.dmg" +APPCAST_PATH="docs/appcast.xml" + +DMG_SIZE=$(stat -f%z "$DMG_PATH") +RELEASE_DATE=$(date -u "+%a, %d %b %Y %H:%M:%S +0000") + +cat > "$APPCAST_PATH" << EOF + + + + InputMetrics + https://owieth.github.io/InputMetrics/appcast.xml + InputMetrics update feed + en + + Version ${VERSION} + ${RELEASE_DATE} + ${VERSION} + ${VERSION} + 15.0 + https://github.com/${REPO}/releases/tag/v${VERSION} + + + + +EOF + +echo "Generated $APPCAST_PATH for v${VERSION}" +echo "NOTE: Replace PLACEHOLDER_SIGNATURE with the actual EdDSA signature." +echo " Run: sign_update \"$DMG_PATH\" with your Sparkle private key." From 0934bc611d56a0ea27d999a9dd084953e760ddad Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 16:26:08 +0200 Subject: [PATCH 3/4] feat(sparkle): add SUPublicEDKey to Info.plist --- InputMetrics/InputMetrics/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InputMetrics/InputMetrics/Info.plist b/InputMetrics/InputMetrics/Info.plist index ee21343..833607c 100644 --- a/InputMetrics/InputMetrics/Info.plist +++ b/InputMetrics/InputMetrics/Info.plist @@ -43,6 +43,8 @@ + SUPublicEDKey + 9LrnDKtkBq1IcxjOFHBNmH8xdEH+MVhyZOI5nfVpR1o= SUFeedURL https://owieth.github.io/InputMetrics/appcast.xml SUEnableAutomaticChecks From 6eb56b11e45b6197711db777321db674dd35dff4 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 16:26:28 +0200 Subject: [PATCH 4/4] fix(sparkle): use correct sign_update path from SPM artifacts in CI --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45e52fb..8882eb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,14 +165,14 @@ jobs: VERSION=${TAG#v} DMG="InputMetrics-${TAG}.dmg" - # Locate sign_update bundled with the resolved Sparkle package - SPARKLE_DIR=$(find ~/Library/Developer/Xcode/DerivedData -name "sign_update" 2>/dev/null | head -1 | xargs -I{} dirname {} 2>/dev/null || true) - if [ -z "$SPARKLE_DIR" ]; then - SPARKLE_DIR=$(find .build -name "sign_update" 2>/dev/null | head -1 | xargs -I{} dirname {} 2>/dev/null || true) + # Locate sign_update from resolved Sparkle SPM artifacts + SIGN_UPDATE=$(find ~/Library/Developer/Xcode/DerivedData -path "*/artifacts/sparkle/Sparkle/bin/sign_update" 2>/dev/null | head -1) + if [ -z "$SIGN_UPDATE" ]; then + echo "ERROR: sign_update not found. Ensure package dependencies are resolved." && exit 1 fi # Sign the DMG and capture EdDSA signature - SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | "$SPARKLE_DIR/sign_update" "$DMG" --ed-key-file -) + SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | "$SIGN_UPDATE" "$DMG" --ed-key-file -) DMG_SIZE=$(stat -f%z "$DMG") RELEASE_DATE=$(date -u "+%a, %d %b %Y %H:%M:%S +0000")