Skip to content

Commit 456dd5d

Browse files
committed
Sign release builds and add GitHub updater
1 parent e136130 commit 456dd5d

13 files changed

Lines changed: 536 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,19 @@ jobs:
5555
id: version
5656
run: echo "value=$(tr -d '[:space:]' < VERSION)" >> "$GITHUB_OUTPUT"
5757

58+
- name: Import codesigning identity
59+
if: ${{ secrets.MACKEYMAP_SIGNING_CERT_BASE64 != '' && secrets.MACKEYMAP_SIGNING_CERT_PASSWORD != '' && secrets.MACKEYMAP_KEYCHAIN_PASSWORD != '' && secrets.MACKEYMAP_SIGN_IDENTITY != '' }}
60+
env:
61+
MACKEYMAP_SIGNING_CERT_BASE64: ${{ secrets.MACKEYMAP_SIGNING_CERT_BASE64 }}
62+
MACKEYMAP_SIGNING_CERT_PASSWORD: ${{ secrets.MACKEYMAP_SIGNING_CERT_PASSWORD }}
63+
MACKEYMAP_KEYCHAIN_PASSWORD: ${{ secrets.MACKEYMAP_KEYCHAIN_PASSWORD }}
64+
MACKEYMAP_SIGN_IDENTITY: ${{ secrets.MACKEYMAP_SIGN_IDENTITY }}
65+
run: ./scripts/import_codesign_identity.sh
66+
5867
- name: Package release artifact
68+
env:
69+
MACKEYMAP_SIGN_IDENTITY: ${{ secrets.MACKEYMAP_SIGN_IDENTITY }}
70+
MACKEYMAP_REQUIRE_SIGNING: ${{ secrets.MACKEYMAP_SIGNING_CERT_BASE64 != '' && secrets.MACKEYMAP_SIGNING_CERT_PASSWORD != '' && secrets.MACKEYMAP_KEYCHAIN_PASSWORD != '' && secrets.MACKEYMAP_SIGN_IDENTITY != '' && '1' || '0' }}
5971
run: ./scripts/package_release.sh
6072

6173
- name: Upload build artifact

.github/workflows/release.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,30 @@ jobs:
5555
- name: Validate tag matches VERSION
5656
run: test "${GITHUB_REF_NAME#v}" = "${{ steps.version.outputs.value }}"
5757

58+
- name: Require codesigning credentials
59+
env:
60+
MACKEYMAP_SIGNING_CERT_BASE64: ${{ secrets.MACKEYMAP_SIGNING_CERT_BASE64 }}
61+
MACKEYMAP_SIGNING_CERT_PASSWORD: ${{ secrets.MACKEYMAP_SIGNING_CERT_PASSWORD }}
62+
MACKEYMAP_KEYCHAIN_PASSWORD: ${{ secrets.MACKEYMAP_KEYCHAIN_PASSWORD }}
63+
MACKEYMAP_SIGN_IDENTITY: ${{ secrets.MACKEYMAP_SIGN_IDENTITY }}
64+
run: |
65+
test -n "$MACKEYMAP_SIGNING_CERT_BASE64"
66+
test -n "$MACKEYMAP_SIGNING_CERT_PASSWORD"
67+
test -n "$MACKEYMAP_KEYCHAIN_PASSWORD"
68+
test -n "$MACKEYMAP_SIGN_IDENTITY"
69+
70+
- name: Import codesigning identity
71+
env:
72+
MACKEYMAP_SIGNING_CERT_BASE64: ${{ secrets.MACKEYMAP_SIGNING_CERT_BASE64 }}
73+
MACKEYMAP_SIGNING_CERT_PASSWORD: ${{ secrets.MACKEYMAP_SIGNING_CERT_PASSWORD }}
74+
MACKEYMAP_KEYCHAIN_PASSWORD: ${{ secrets.MACKEYMAP_KEYCHAIN_PASSWORD }}
75+
MACKEYMAP_SIGN_IDENTITY: ${{ secrets.MACKEYMAP_SIGN_IDENTITY }}
76+
run: ./scripts/import_codesign_identity.sh
77+
5878
- name: Package release artifact
79+
env:
80+
MACKEYMAP_SIGN_IDENTITY: ${{ secrets.MACKEYMAP_SIGN_IDENTITY }}
81+
MACKEYMAP_REQUIRE_SIGNING: "1"
5982
run: |
6083
./scripts/package_release.sh
6184

App/Sources/MacKeyMapApp/AppSupportPaths.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ enum AppSupportPaths {
1717
applicationSupportDirectory.appendingPathComponent("config.json")
1818
}
1919

20+
static var updatesDirectory: URL {
21+
applicationSupportDirectory.appendingPathComponent("Updates", isDirectory: true)
22+
}
23+
2024
static var appLogURL: URL {
2125
logsDirectory.appendingPathComponent("app.log")
2226
}
@@ -30,5 +34,9 @@ enum AppSupportPaths {
3034
at: logsDirectory,
3135
withIntermediateDirectories: true
3236
)
37+
try FileManager.default.createDirectory(
38+
at: updatesDirectory,
39+
withIntermediateDirectories: true
40+
)
3341
}
3442
}

App/Sources/MacKeyMapApp/DiagnosticsStore.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ enum DiagnosticsStore {
101101
let bundle = Bundle.main
102102
let shortVersion = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
103103
let buildVersion = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
104+
let bundleIdentifier = bundle.bundleIdentifier ?? "unknown"
105+
let bundlePath = bundle.bundleURL.path
106+
let codeSignature = currentCodeSignatureSummary()
104107

105108
let deviceLines = snapshot.devices.map { device in
106109
let mode = device.remapMode ?? "none"
@@ -111,6 +114,9 @@ enum DiagnosticsStore {
111114
"MacKeyMap Diagnostics",
112115
"Generated: \(iso8601Timestamp())",
113116
"Version: \(shortVersion) (\(buildVersion))",
117+
"Bundle identifier: \(bundleIdentifier)",
118+
"Bundle path: \(bundlePath)",
119+
"Code signature: \(codeSignature)",
114120
"Engine status: \(snapshot.engineStatus)",
115121
"Remapping enabled: \(config.enabled)",
116122
"Launch at login: \(config.launchAtLogin)",
@@ -166,4 +172,53 @@ enum DiagnosticsStore {
166172
formatter.dateFormat = "yyyyMMdd-HHmmss"
167173
return formatter.string(from: Date())
168174
}
175+
176+
private static func currentCodeSignatureSummary() -> String {
177+
let process = Process()
178+
let outputPipe = Pipe()
179+
process.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
180+
process.arguments = ["-dv", "--verbose=4", Bundle.main.bundleURL.path]
181+
process.standardError = outputPipe
182+
process.standardOutput = Pipe()
183+
184+
do {
185+
try process.run()
186+
process.waitUntilExit()
187+
} catch {
188+
return "unavailable (\(error.localizedDescription))"
189+
}
190+
191+
guard process.terminationStatus == 0 else {
192+
return "codesign exited with status \(process.terminationStatus)"
193+
}
194+
195+
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
196+
guard let output = String(data: data, encoding: .utf8) else {
197+
return "unavailable"
198+
}
199+
200+
let authority = output
201+
.components(separatedBy: .newlines)
202+
.first(where: { $0.hasPrefix("Authority=") })
203+
.map { String($0.dropFirst("Authority=".count)) }
204+
let teamIdentifier = output
205+
.components(separatedBy: .newlines)
206+
.first(where: { $0.hasPrefix("TeamIdentifier=") })
207+
.map { String($0.dropFirst("TeamIdentifier=".count)) }
208+
let signature = output
209+
.components(separatedBy: .newlines)
210+
.first(where: { $0.hasPrefix("Signature=") })
211+
.map { String($0.dropFirst("Signature=".count)) }
212+
213+
return [signature, authority, teamIdentifier]
214+
.compactMap { $0 }
215+
.joined(separator: " | ")
216+
.ifEmpty("unavailable")
217+
}
218+
}
219+
220+
private extension String {
221+
func ifEmpty(_ fallback: String) -> String {
222+
isEmpty ? fallback : self
223+
}
169224
}

App/Sources/MacKeyMapApp/MenuBarController.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ final class MenuBarController: NSObject, NSApplicationDelegate {
1212

1313
private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
1414
private let engine = RustEngineBridge()
15+
private let updater = ReleaseUpdater()
1516
private let configStore = ConfigStore()
1617
private lazy var baseStatusImage = loadBaseStatusImage()
1718
private var config = AppConfig()
@@ -122,6 +123,14 @@ final class MenuBarController: NSObject, NSApplicationDelegate {
122123

123124
menu.addItem(.separator())
124125

126+
let updateItem = NSMenuItem(
127+
title: "Check for Updates…",
128+
action: #selector(checkForUpdates(_:)),
129+
keyEquivalent: ""
130+
)
131+
updateItem.target = self
132+
menu.addItem(updateItem)
133+
125134
let launchItem = NSMenuItem(
126135
title: "Launch at Login",
127136
action: #selector(toggleLaunchAtLogin(_:)),
@@ -369,6 +378,13 @@ final class MenuBarController: NSObject, NSApplicationDelegate {
369378
refreshState()
370379
}
371380

381+
@objc
382+
private func checkForUpdates(_ sender: NSMenuItem) {
383+
Task { @MainActor in
384+
await updater.checkForUpdates()
385+
}
386+
}
387+
372388
@objc
373389
private func quit(_ sender: NSMenuItem) {
374390
NSApp.terminate(nil)

0 commit comments

Comments
 (0)