Skip to content

Commit 843d462

Browse files
bones7456claude
andcommitted
Add Sparkle auto-update; bump to 1.2.3
Replace manual "Check for Updates" dialog (which only opened GitHub) with Sparkle's in-app downloader + relaunch flow. Public EdDSA key is embedded in Info.plist; the release pipeline signs the zip with the private key from SPARKLE_PRIVATE_KEY and publishes appcast.xml as a release asset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5c62e9e commit 843d462

9 files changed

Lines changed: 186 additions & 141 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
APPLE_ID: ${{ secrets.APPLE_ID }}
2222
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
2323
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
24+
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
2425

2526
steps:
2627
- name: Checkout
@@ -77,7 +78,11 @@ jobs:
7778
if ! gh release view "$TAG" >/dev/null 2>&1; then
7879
gh release create "$TAG" --title "Notchy $TAG" --generate-notes
7980
fi
80-
gh release upload "$TAG" dist/Notchy-*.zip dist/Notchy-*.dmg --clobber
81+
ASSETS=(dist/Notchy-*.zip dist/Notchy-*.dmg)
82+
if [ -f dist/appcast.xml ]; then
83+
ASSETS+=(dist/appcast.xml)
84+
fi
85+
gh release upload "$TAG" "${ASSETS[@]}" --clobber
8186
8287
- name: Clean up keychain
8388
if: always()

Notchy.xcodeproj/project.pbxproj

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
BD0000030000000000000003 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = BD0000020000000000000002 /* SwiftTerm */; };
11+
BD0000060000000000000006 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = BD0000050000000000000005 /* Sparkle */; };
1112
/* End PBXBuildFile section */
1213

1314
/* Begin PBXFileReference section */
@@ -28,6 +29,7 @@
2829
buildActionMask = 2147483647;
2930
files = (
3031
BD0000030000000000000003 /* SwiftTerm in Frameworks */,
32+
BD0000060000000000000006 /* Sparkle in Frameworks */,
3133
);
3234
runOnlyForDeploymentPostprocessing = 0;
3335
};
@@ -71,6 +73,7 @@
7173
name = Notchy;
7274
packageProductDependencies = (
7375
BD0000020000000000000002 /* SwiftTerm */,
76+
BD0000050000000000000005 /* Sparkle */,
7477
);
7578
productName = Notchy;
7679
productReference = 31844E332F7210B20074F7B7 /* Notchy.app */;
@@ -102,6 +105,7 @@
102105
minimizedProjectReferenceProxies = 1;
103106
packageReferences = (
104107
BD0000010000000000000001 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
108+
BD0000040000000000000004 /* XCRemoteSwiftPackageReference "Sparkle" */,
105109
);
106110
preferredProjectObjectVersion = 77;
107111
productRefGroup = 31844E342F7210B20074F7B7 /* Products */;
@@ -275,6 +279,7 @@
275279
ENABLE_PREVIEWS = YES;
276280
ENABLE_USER_SELECTED_FILES = readonly;
277281
GENERATE_INFOPLIST_FILE = YES;
282+
INFOPLIST_FILE = Notchy/Info.plist;
278283
INFOPLIST_KEY_CFBundleDisplayName = Notchy;
279284
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
280285
INFOPLIST_KEY_LSUIElement = YES;
@@ -285,7 +290,7 @@
285290
"@executable_path/../Frameworks",
286291
);
287292
MACOSX_DEPLOYMENT_TARGET = 15.6;
288-
MARKETING_VERSION = 1.2.2;
293+
MARKETING_VERSION = 1.2.3;
289294
PRODUCT_BUNDLE_IDENTIFIER = li.luy.notchy;
290295
PRODUCT_NAME = "$(TARGET_NAME)";
291296
REGISTER_APP_GROUPS = YES;
@@ -314,6 +319,7 @@
314319
ENABLE_PREVIEWS = YES;
315320
ENABLE_USER_SELECTED_FILES = readonly;
316321
GENERATE_INFOPLIST_FILE = YES;
322+
INFOPLIST_FILE = Notchy/Info.plist;
317323
INFOPLIST_KEY_CFBundleDisplayName = Notchy;
318324
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
319325
INFOPLIST_KEY_LSUIElement = YES;
@@ -324,7 +330,7 @@
324330
"@executable_path/../Frameworks",
325331
);
326332
MACOSX_DEPLOYMENT_TARGET = 15.6;
327-
MARKETING_VERSION = 1.2.2;
333+
MARKETING_VERSION = 1.2.3;
328334
PRODUCT_BUNDLE_IDENTIFIER = li.luy.notchy;
329335
PRODUCT_NAME = "$(TARGET_NAME)";
330336
REGISTER_APP_GROUPS = YES;
@@ -369,6 +375,14 @@
369375
minimumVersion = 1.0.0;
370376
};
371377
};
378+
BD0000040000000000000004 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
379+
isa = XCRemoteSwiftPackageReference;
380+
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
381+
requirement = {
382+
kind = upToNextMajorVersion;
383+
minimumVersion = 2.9.2;
384+
};
385+
};
372386
/* End XCRemoteSwiftPackageReference section */
373387

374388
/* Begin XCSwiftPackageProductDependency section */
@@ -377,6 +391,11 @@
377391
package = BD0000010000000000000001 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
378392
productName = SwiftTerm;
379393
};
394+
BD0000050000000000000005 /* Sparkle */ = {
395+
isa = XCSwiftPackageProductDependency;
396+
package = BD0000040000000000000004 /* XCRemoteSwiftPackageReference "Sparkle" */;
397+
productName = Sparkle;
398+
};
380399
/* End XCSwiftPackageProductDependency section */
381400
};
382401
rootObject = 31844E2B2F7210B20074F7B7 /* Project object */;

Notchy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Notchy/AppDelegate.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3333
observeScreenChanges()
3434
// Detect in background so launch isn't blocked
3535
sessionStore.detectAllXcodeProjectsAsync()
36+
// Boot Sparkle so the scheduled background check runs
37+
_ = UpdaterController.shared
3638
}
3739

3840
private func setupStatusItem() {
@@ -242,6 +244,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
242244
settingsItem.target = self
243245
menu.addItem(settingsItem)
244246

247+
let checkUpdatesItem = NSMenuItem(
248+
title: "Check for Updates\u{2026}",
249+
action: #selector(checkForUpdates),
250+
keyEquivalent: ""
251+
)
252+
checkUpdatesItem.target = self
253+
menu.addItem(checkUpdatesItem)
254+
245255
menu.addItem(.separator())
246256

247257
let quitItem = NSMenuItem(
@@ -303,6 +313,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
303313
showPanelBelowStatusItem()
304314
}
305315

316+
@objc private func checkForUpdates() {
317+
UpdaterController.shared.checkForUpdates()
318+
}
319+
306320
// MARK: - External display management
307321

308322
private func observeScreenChanges() {

Notchy/Info.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SUEnableAutomaticChecks</key>
6+
<true/>
7+
<key>SUFeedURL</key>
8+
<string>https://github.com/bones7456/notchy/releases/latest/download/appcast.xml</string>
9+
<key>SUPublicEDKey</key>
10+
<string>RicWoTHEBrEzstC0TdtrQ34ZyzJAKoDm6B0v3D7w9gY=</string>
11+
</dict>
12+
</plist>

Notchy/SettingsWindow.swift

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ struct IntegrationsTab: View {
167167
}
168168

169169
struct AboutTab: View {
170-
@State private var updateChecker = UpdateChecker.shared
170+
private let updater = UpdaterController.shared
171+
@State private var automaticChecks: Bool = UpdaterController.shared.automaticallyChecksForUpdates
171172

172173
private var versionString: String {
173174
let info = Bundle.main.infoDictionary
@@ -194,8 +195,8 @@ struct AboutTab: View {
194195
.foregroundStyle(.secondary)
195196
.padding(.top, 2)
196197

197-
updateStatusView
198-
.padding(.top, 8)
198+
updateControls
199+
.padding(.top, 12)
199200

200201
Spacer(minLength: 16)
201202

@@ -223,64 +224,22 @@ struct AboutTab: View {
223224
}
224225
.padding(.vertical, 28)
225226
.frame(maxWidth: .infinity, maxHeight: .infinity)
226-
.task {
227-
if case .idle = updateChecker.state {
228-
await updateChecker.check()
229-
}
230-
}
231227
}
232228

233229
@ViewBuilder
234-
private var updateStatusView: some View {
235-
switch updateChecker.state {
236-
case .idle:
237-
Button("Check for Updates") {
238-
Task { await updateChecker.check() }
239-
}
240-
.font(.caption)
241-
case .checking:
242-
HStack(spacing: 6) {
243-
ProgressView().controlSize(.small)
244-
Text("Checking for updates…")
245-
.font(.caption)
246-
.foregroundStyle(.secondary)
230+
private var updateControls: some View {
231+
VStack(spacing: 6) {
232+
Button("Check for Updates…") {
233+
updater.checkForUpdates()
247234
}
248-
case .upToDate:
249-
HStack(spacing: 6) {
250-
Text("You're on the latest version.")
251-
.font(.caption)
252-
.foregroundStyle(.secondary)
253-
Button("Check Again") {
254-
Task { await updateChecker.check() }
255-
}
256-
.buttonStyle(.link)
235+
.controlSize(.regular)
236+
237+
Toggle("Automatically check for updates", isOn: $automaticChecks)
238+
.toggleStyle(.checkbox)
257239
.font(.caption)
258-
}
259-
case .updateAvailable(let latest, _, let url):
260-
HStack(spacing: 6) {
261-
Image(systemName: "arrow.down.circle.fill")
262-
.foregroundStyle(.tint)
263-
Text("New version \(latest) available")
264-
.font(.caption)
265-
Button("View Release") {
266-
NSWorkspace.shared.open(url)
240+
.onChange(of: automaticChecks) { _, newValue in
241+
updater.automaticallyChecksForUpdates = newValue
267242
}
268-
.buttonStyle(.borderedProminent)
269-
.controlSize(.small)
270-
}
271-
case .failed(let message):
272-
HStack(spacing: 6) {
273-
Text("Update check failed: \(message)")
274-
.font(.caption)
275-
.foregroundStyle(.secondary)
276-
.lineLimit(1)
277-
.truncationMode(.middle)
278-
Button("Retry") {
279-
Task { await updateChecker.check() }
280-
}
281-
.buttonStyle(.link)
282-
.font(.caption)
283-
}
284243
}
285244
}
286245
}

Notchy/UpdateChecker.swift

Lines changed: 0 additions & 81 deletions
This file was deleted.

Notchy/UpdaterController.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import Sparkle
3+
4+
@MainActor
5+
final class UpdaterController {
6+
static let shared = UpdaterController()
7+
8+
let controller: SPUStandardUpdaterController
9+
10+
var automaticallyChecksForUpdates: Bool {
11+
get { controller.updater.automaticallyChecksForUpdates }
12+
set { controller.updater.automaticallyChecksForUpdates = newValue }
13+
}
14+
15+
var currentVersion: String {
16+
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0.0"
17+
}
18+
19+
private init() {
20+
controller = SPUStandardUpdaterController(
21+
startingUpdater: true,
22+
updaterDelegate: nil,
23+
userDriverDelegate: nil
24+
)
25+
}
26+
27+
func checkForUpdates() {
28+
controller.checkForUpdates(nil)
29+
}
30+
}

0 commit comments

Comments
 (0)