Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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" | "$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
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>InputMetrics</title>
<link>https://owieth.github.io/InputMetrics/appcast.xml</link>
<description>InputMetrics update feed</description>
<language>en</language>
<item>
<title>Version ${VERSION}</title>
<pubDate>${RELEASE_DATE}</pubDate>
<sparkle:version>${VERSION}</sparkle:version>
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://github.com/owieth/InputMetrics/releases/tag/${TAG}</sparkle:releaseNotesLink>
<enclosure
url="${DOWNLOAD_URL}"
length="${DMG_SIZE}"
type="application/octet-stream"
sparkle:edSignature="${SIGNATURE}"
/>
</item>
</channel>
</rss>
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: |
Expand Down
17 changes: 17 additions & 0 deletions InputMetrics.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -57,6 +58,7 @@
buildActionMask = 2147483647;
files = (
GRDB_PKG /* GRDB in Frameworks */,
DFEF2B492F7ABE0D00B1FB6B /* Sparkle in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -109,6 +111,7 @@
name = InputMetrics;
packageProductDependencies = (
GRDB_REF /* GRDB */,
DFEF2B482F7ABE0D00B1FB6B /* Sparkle */,
);
productName = InputMetrics;
productReference = APP_PRODUCT /* InputMetrics.app */;
Expand Down Expand Up @@ -156,6 +159,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
GRDB_PACKAGE /* XCRemoteSwiftPackageReference "GRDB.swift" */,
DFEF2B472F7ABE0D00B1FB6B /* XCRemoteSwiftPackageReference "Sparkle" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = PRODUCTS_GROUP /* Products */;
Expand Down Expand Up @@ -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";
Expand All @@ -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" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions InputMetrics/InputMetrics/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import AppKit
import os
import Sparkle

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions InputMetrics/InputMetrics/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
</array>
</dict>
</array>
<key>SUPublicEDKey</key>
<string>9LrnDKtkBq1IcxjOFHBNmH8xdEH+MVhyZOI5nfVpR1o=</string>
<key>SUFeedURL</key>
<string>https://owieth.github.io/InputMetrics/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSPrincipalClass</key>
Expand Down
46 changes: 46 additions & 0 deletions scripts/generate-appcast.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
# Generates appcast.xml for Sparkle auto-updates.
# Usage: ./scripts/generate-appcast.sh <version> <dmg_path>
# 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 <version> <dmg_path>}"
DMG_PATH="${2:?Usage: $0 <version> <dmg_path>}"
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
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>InputMetrics</title>
<link>https://owieth.github.io/InputMetrics/appcast.xml</link>
<description>InputMetrics update feed</description>
<language>en</language>
<item>
<title>Version ${VERSION}</title>
<pubDate>${RELEASE_DATE}</pubDate>
<sparkle:version>${VERSION}</sparkle:version>
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>https://github.com/${REPO}/releases/tag/v${VERSION}</sparkle:releaseNotesLink>
<enclosure
url="${DOWNLOAD_URL}"
length="${DMG_SIZE}"
type="application/octet-stream"
sparkle:edSignature="PLACEHOLDER_SIGNATURE"
/>
</item>
</channel>
</rss>
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."