diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 714811d..35ee3ce 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,6 +23,7 @@ env:
APP_NAME: PureMac
BUNDLE_ID: com.puremac.app
TEAM_ID: H3WXHVTP97
+ SPARKLE_VERSION: 2.9.2
SCHEME: PureMac
PROJECT: PureMac.xcodeproj
CONFIGURATION: Release
@@ -129,13 +130,23 @@ jobs:
run: |
set -euo pipefail
APP="build/export/PureMac.app"
- codesign --verify --deep --strict --verbose=2 "${APP}"
+ codesign --verify --deep --strict --verbose=4 "${APP}"
codesign -dvv "${APP}" 2>&1 | tee /tmp/cs-info.txt
grep -q "flags=0x10000(runtime)" /tmp/cs-info.txt || { echo "::error::Hardened runtime missing"; exit 1; }
spctl --assess --type execute --verbose=4 "${APP}" || true
# Universal arch check
lipo -archs "${APP}/Contents/MacOS/PureMac" | grep -q "x86_64" && lipo -archs "${APP}/Contents/MacOS/PureMac" | grep -q "arm64"
+ - name: Verify Sparkle helper bundles
+ run: |
+ set -euo pipefail
+ APP="build/export/PureMac.app"
+ while IFS= read -r helper; do
+ if [[ -n "${helper}" ]]; then
+ codesign --verify --deep --strict --verbose=4 "${helper}"
+ fi
+ done < <(find "${APP}" -type d \( -name 'Autoupdate.app' -o -name 'Updater.app' \) | sort)
+
- name: Build DMG
env:
VERSION: ${{ steps.ver.outputs.version }}
@@ -202,6 +213,39 @@ jobs:
# Final shippable zip with stapled ticket
ditto -c -k --keepParent --sequesterRsrc build/export/PureMac.app "${ZIP}"
+ - name: Download Sparkle release tools
+ run: |
+ set -euo pipefail
+ SPARKLE_TARBALL="${RUNNER_TEMP}/Sparkle-${SPARKLE_VERSION}.tar.xz"
+ SPARKLE_DIR="${RUNNER_TEMP}/Sparkle-${SPARKLE_VERSION}"
+ curl -L -o "${SPARKLE_TARBALL}" "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
+ rm -rf "${SPARKLE_DIR}"
+ tar -xf "${SPARKLE_TARBALL}" -C "${RUNNER_TEMP}"
+ echo "SPARKLE_BIN_DIR=${SPARKLE_DIR}/bin" >> "$GITHUB_ENV"
+
+ - name: Generate Sparkle appcast
+ env:
+ VERSION: ${{ steps.ver.outputs.version }}
+ TAG: ${{ steps.ver.outputs.tag }}
+ SPARKLE_ED25519_PRIVATE_KEY: ${{ secrets.SPARKLE_ED25519_PRIVATE_KEY }}
+ run: |
+ set -euo pipefail
+ APPCAST_DIR="build/appcast"
+ DMG="build/PureMac-${VERSION}.dmg"
+ rm -rf "${APPCAST_DIR}"
+ mkdir -p "${APPCAST_DIR}"
+ cp "${DMG}" "${APPCAST_DIR}/"
+ # Sign the DMG explicitly (sign_update) so the signature is available
+ # for auditing and log output. Then generate the signed appcast.
+ printf '%s' "${SPARKLE_ED25519_PRIVATE_KEY}" | "${SPARKLE_BIN_DIR}/sign_update" --ed-key-file - "${APPCAST_DIR}/$(basename "${DMG}")" || true
+
+ printf '%s' "${SPARKLE_ED25519_PRIVATE_KEY}" | "${SPARKLE_BIN_DIR}/generate_appcast" \
+ --ed-key-file - \
+ --output-path "${APPCAST_DIR}/appcast.xml" \
+ --download-url-prefix "https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/" \
+ --full-release-notes-url "https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}" \
+ "${APPCAST_DIR}"
+
- name: Compute SHA256 checksums
id: sha
env:
@@ -228,6 +272,7 @@ jobs:
path: |
build/PureMac-${{ steps.ver.outputs.version }}.dmg
build/PureMac-${{ steps.ver.outputs.version }}.zip
+ build/appcast/appcast.xml
build/CHECKSUMS.md
retention-days: 14
@@ -259,11 +304,13 @@ jobs:
gh release upload "${TAG}" \
"build/PureMac-${VERSION}.dmg" \
"build/PureMac-${VERSION}.zip" \
+ "build/appcast/appcast.xml" \
--repo "${GITHUB_REPOSITORY}" --clobber
else
gh release create "${TAG}" \
"build/PureMac-${VERSION}.dmg" \
"build/PureMac-${VERSION}.zip" \
+ "build/appcast/appcast.xml" \
--title "PureMac ${TAG}" \
--notes-file "${NOTES_FILE}" \
--repo "${GITHUB_REPOSITORY}"
diff --git a/PureMac.xcodeproj/project.pbxproj b/PureMac.xcodeproj/project.pbxproj
index c82ca72..05847ce 100644
--- a/PureMac.xcodeproj/project.pbxproj
+++ b/PureMac.xcodeproj/project.pbxproj
@@ -555,6 +555,8 @@
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = PureMac/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 13.0;
+ PUREMAC_FEED_URL = "http://127.0.0.1:8000/appcast.xml";
+ PUREMAC_PUBLIC_ED_KEY = "G2pCmpU2grTTNsigrEmNsPmZX2LjiMBkvMP1fvMCuig=";
MARKETING_VERSION = 2.6.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -673,6 +675,8 @@
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = PureMac/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 13.0;
+ PUREMAC_FEED_URL = "https://github.com/momenbasel/PureMac/releases/latest/download/appcast.xml";
+ PUREMAC_PUBLIC_ED_KEY = "IEOLcqFwQpIXmRrnmBzgjxIR2L9bnh+C+Oye6cMkePk=";
MARKETING_VERSION = 2.6.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
diff --git a/PureMac/Info.plist b/PureMac/Info.plist
index e9c7374..81dc3aa 100644
--- a/PureMac/Info.plist
+++ b/PureMac/Info.plist
@@ -22,6 +22,14 @@
$(MACOSX_DEPLOYMENT_TARGET)
LSApplicationCategoryType
public.app-category.utilities
+ SUFeedURL
+ $(PUREMAC_FEED_URL)
+ SUPublicEDKey
+ $(PUREMAC_PUBLIC_ED_KEY)
+ SUEnableAutomaticChecks
+
+ SUScheduledCheckInterval
+ 86400
NSHumanReadableCopyright
Copyright 2026. MIT License.
NSDesktopFolderUsageDescription
diff --git a/PureMac/PureMacApp.swift b/PureMac/PureMacApp.swift
index 7faa6a1..797e7e4 100644
--- a/PureMac/PureMacApp.swift
+++ b/PureMac/PureMacApp.swift
@@ -9,6 +9,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Touch TCC-protected paths so macOS registers PureMac in the
// Full Disk Access pane on first launch (fixes issue #75).
FullDiskAccessManager.shared.triggerRegistration()
+
+ if ProcessInfo.processInfo.environment["PUREMAC_AUTO_CHECK_UPDATES"] == "1" {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ UpdateService.shared.checkForUpdates()
+ }
+ }
}
}
diff --git a/PureMac/Services/UpdateService.swift b/PureMac/Services/UpdateService.swift
index a46db1a..47336da 100644
--- a/PureMac/Services/UpdateService.swift
+++ b/PureMac/Services/UpdateService.swift
@@ -14,11 +14,115 @@ final class UpdateService: ObservableObject {
private init() {
#if canImport(Sparkle)
- // Do not start automatic checking by default; keep control minimal.
+ // Create the standard updater controller but do not start it automatically.
updaterController = SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil)
+
+ // Install lightweight runtime observers to surface Sparkle notifications to the app console.
+ installDebugObservers()
+
+ // If the updater's stored preference indicates automatic checks, start the updater so Sparkle can schedule checks.
+ if let updater = updaterController?.updater, updater.automaticallyChecksForUpdates {
+ updaterController?.startUpdater()
+ }
#endif
}
+ #if canImport(Sparkle)
+ private var debugObservers: [Any] = []
+
+ private func installDebugObservers() {
+ let nc = NotificationCenter.default
+
+ debugObservers.append(nc.addObserver(forName: NSNotification.Name("SUUpdaterDidFinishLoadingAppCastNotification"), object: nil, queue: .main) { note in
+ print("NOTIFICATION_APPCAST")
+ if let appcast = note.userInfo?["appcast"] as? SUAppcast {
+ print("APPCAST_LOADED \(appcast.items.count)")
+ }
+ })
+
+ debugObservers.append(nc.addObserver(forName: NSNotification.Name("SUUpdaterDidFindValidUpdateNotification"), object: nil, queue: .main) { _ in
+ print("NOTIFICATION_FOUND")
+ })
+
+ debugObservers.append(nc.addObserver(forName: NSNotification.Name("SUUpdaterDidNotFindUpdateNotification"), object: nil, queue: .main) { _ in
+ print("NOTIFICATION_NOT_FOUND")
+ })
+
+ debugObservers.append(nc.addObserver(forName: NSNotification.Name("SUUpdaterDidAbortWithErrorNotification"), object: nil, queue: .main) { note in
+ let msg = "NOTIFICATION_ABORTED \(String(describing: note.userInfo?["error"]))"
+ print(msg)
+ self.appendDebugLog(msg)
+ })
+ // Global observer: capture any Sparkle-related notifications for debugging.
+ debugObservers.append(nc.addObserver(forName: nil, object: nil, queue: .main) { note in
+ let name = note.name.rawValue
+ if name.contains("SU") || name.lowercased().contains("sparkle") {
+ let msg = "NOTIF: \(name) userInfo=\(String(describing: note.userInfo))"
+ print(msg)
+ self.appendDebugLog(msg)
+ }
+ })
+ }
+
+ private func appendDebugLog(_ message: String) {
+ let fm = FileManager.default
+ let logDir = fm.homeDirectoryForCurrentUser.appendingPathComponent("Library/Logs/PureMac")
+ try? fm.createDirectory(at: logDir, withIntermediateDirectories: true)
+ let logFile = logDir.appendingPathComponent("update-debug.log")
+ let ts = ISO8601DateFormatter().string(from: Date())
+ let line = "[\(ts)] \(message)\n"
+ if let data = line.data(using: .utf8) {
+ if fm.fileExists(atPath: logFile.path) {
+ if let fh = try? FileHandle(forWritingTo: logFile) {
+ fh.seekToEndOfFile()
+ fh.write(data)
+ try? fh.close()
+ }
+ } else {
+ try? data.write(to: logFile)
+ }
+ }
+ }
+ #endif
+
+ #if canImport(Sparkle)
+ var updater: SPUUpdater? { updaterController?.updater }
+
+ /// Set whether Sparkle should automatically check for updates.
+ func setAutomaticallyChecks(_ enabled: Bool) {
+ DispatchQueue.main.async {
+ guard let updater = self.updaterController?.updater else {
+ UserDefaults.standard.set(enabled, forKey: "settings.updates.checkAutomatically")
+ return
+ }
+ updater.automaticallyChecksForUpdates = enabled
+ if enabled {
+ self.updaterController?.startUpdater()
+ }
+ updater.resetUpdateCycleAfterShortDelay()
+ }
+ }
+
+ /// Set the Sparkle update check interval (seconds).
+ func setUpdateInterval(_ seconds: TimeInterval) {
+ DispatchQueue.main.async {
+ if let updater = self.updaterController?.updater {
+ updater.updateCheckInterval = seconds
+ updater.resetUpdateCycleAfterShortDelay()
+ } else {
+ // Fallback: persist to UserDefaults for older code paths.
+ let raw: String
+ switch Int(seconds) {
+ case 60 * 60 * 24: raw = "Daily"
+ case 60 * 60 * 24 * 30: raw = "Monthly"
+ default: raw = "Weekly"
+ }
+ UserDefaults.standard.set(raw, forKey: "settings.updates.checkInterval")
+ }
+ }
+ }
+ #endif
+
func checkForUpdates() {
#if canImport(Sparkle)
DispatchQueue.main.async {
diff --git a/PureMac/Views/Settings/SettingsView.swift b/PureMac/Views/Settings/SettingsView.swift
index 71e2520..1e9a21d 100644
--- a/PureMac/Views/Settings/SettingsView.swift
+++ b/PureMac/Views/Settings/SettingsView.swift
@@ -7,6 +7,8 @@ struct SettingsView: View {
TabView {
GeneralSettingsView()
.tabItem { Label("General", systemImage: "gear") }
+ UpdatesSettingsView()
+ .tabItem { Label("Updates", systemImage: "arrow.triangle.2.circlepath") }
CleaningSettingsView()
.tabItem { Label("Cleaning", systemImage: "trash") }
ScheduleSettingsView()
@@ -18,6 +20,91 @@ struct SettingsView: View {
}
}
+// MARK: - Updates
+
+enum UpdateInterval: String, CaseIterable, Identifiable, Codable {
+ case daily = "Daily"
+ case weekly = "Weekly"
+ case monthly = "Monthly"
+
+ var id: String { rawValue }
+
+ var timeInterval: TimeInterval {
+ switch self {
+ case .daily: return 60 * 60 * 24
+ case .weekly: return 60 * 60 * 24 * 7
+ case .monthly: return 60 * 60 * 24 * 30
+ }
+ }
+}
+
+struct UpdatesSettingsView: View {
+ @State private var automaticallyChecks: Bool = true
+ @State private var interval: UpdateInterval = .weekly
+
+ init() {
+ if let updater = UpdateService.shared.updater {
+ _automaticallyChecks = State(initialValue: updater.automaticallyChecksForUpdates)
+ let seconds = updater.updateCheckInterval
+ if let matched = UpdateInterval.allCases.first(where: { $0.timeInterval == seconds }) {
+ _interval = State(initialValue: matched)
+ } else {
+ _interval = State(initialValue: .weekly)
+ }
+ } else {
+ let defaults = UserDefaults.standard
+ let enabled = defaults.object(forKey: "settings.updates.checkAutomatically") as? Bool ?? true
+ let raw = defaults.string(forKey: "settings.updates.checkInterval") ?? UpdateInterval.weekly.rawValue
+ _automaticallyChecks = State(initialValue: enabled)
+ _interval = State(initialValue: UpdateInterval(rawValue: raw) ?? .weekly)
+ }
+ }
+
+ var body: some View {
+ Form {
+ Section("Updates") {
+ Toggle("Check automatically", isOn: $automaticallyChecks)
+ .onChange(of: automaticallyChecks) { newValue in
+ UpdateService.shared.setAutomaticallyChecks(newValue)
+ }
+
+ Picker("Check interval", selection: Binding(
+ get: { interval },
+ set: { newValue in
+ interval = newValue
+ UpdateService.shared.setUpdateInterval(newValue.timeInterval)
+ }
+ )) {
+ ForEach(UpdateInterval.allCases) { value in
+ Text(LocalizedStringKey(value.rawValue)).tag(value)
+ }
+ }
+ .pickerStyle(.radioGroup)
+ .disabled(!automaticallyChecks)
+
+ HStack {
+ Spacer()
+ Button("Check Now") {
+ UpdateService.shared.checkForUpdates()
+ }
+ .keyboardShortcut("u", modifiers: [.command])
+ }
+ }
+ }
+ .formStyle(.grouped)
+ .onAppear {
+ // Sync UI from updater in case external changes occurred
+ if let updater = UpdateService.shared.updater {
+ automaticallyChecks = updater.automaticallyChecksForUpdates
+ let seconds = updater.updateCheckInterval
+ if let matched = UpdateInterval.allCases.first(where: { $0.timeInterval == seconds }) {
+ interval = matched
+ }
+ }
+ }
+ }
+}
+
// MARK: - General
enum SearchSensitivity: String, CaseIterable, Identifiable, Codable {
diff --git a/project.yml b/project.yml
index ccb459c..331d102 100644
--- a/project.yml
+++ b/project.yml
@@ -26,6 +26,8 @@ settings:
MARKETING_VERSION: "2.6.1"
CURRENT_PROJECT_VERSION: "13"
GENERATE_INFOPLIST_FILE: "NO"
+ PUREMAC_FEED_URL: "https://github.com/momenbasel/PureMac/releases/latest/download/appcast.xml"
+ PUREMAC_PUBLIC_ED_KEY: "IEOLcqFwQpIXmRrnmBzgjxIR2L9bnh+C+Oye6cMkePk="
ASSETCATALOG_COMPILER_APPICON_NAME: "AppIcon"
COMBINE_HIDPI_IMAGES: "YES"
@@ -47,6 +49,8 @@ targets:
Debug:
CODE_SIGNING_ALLOWED: "NO"
CODE_SIGNING_REQUIRED: "NO"
+ PUREMAC_FEED_URL: "http://127.0.0.1:8000/appcast.xml"
+ PUREMAC_PUBLIC_ED_KEY: "G2pCmpU2grTTNsigrEmNsPmZX2LjiMBkvMP1fvMCuig="
PureMacTests:
type: bundle.unit-test
diff --git a/scripts/SECRETS.md b/scripts/SECRETS.md
index 726fcbe..7f4e789 100644
--- a/scripts/SECRETS.md
+++ b/scripts/SECRETS.md
@@ -8,7 +8,7 @@ The pipeline uses an **App Store Connect API key** for notarization (modern,
no rotation, scoped to one team) instead of the legacy
`APPLE_ID + app-specific-password` flow.
-## Required secrets (6)
+## Required secrets (7)
| Secret | Source | Notes |
|--------|--------|-------|
@@ -18,6 +18,7 @@ no rotation, scoped to one team) instead of the legacy
| `APP_STORE_CONNECT_KEY_ID` | The 10-char ID from the `.p8` filename (`AuthKey_XXXXXXXXXX.p8`) | e.g. `5G7R52L8RK` |
| `APP_STORE_CONNECT_ISSUER_ID` | UUID from | e.g. `5de3898a-cd31-4061-850f-ae17b389e46a` |
| `APP_STORE_CONNECT_PRIVATE_KEY` | Full contents of the `.p8` file (`-----BEGIN PRIVATE KEY-----` ... `-----END PRIVATE KEY-----`) | Paste raw, including the BEGIN/END lines |
+| `SPARKLE_ED25519_PRIVATE_KEY` | The private Ed25519 key exported from Sparkle's `generate_keys -x` flow | Used by `generate_appcast` to sign `appcast.xml` |
## Optional secret (1)
@@ -112,6 +113,24 @@ rm -P ~/Desktop/PureMac-secrets/PureMac-DeveloperID.p12* \
~/Desktop/PureMac-secrets/KEYCHAIN_PASSWORD.txt
```
+## Storing the Sparkle Ed25519 key locally
+
+Generate the public/private pair once with Sparkle's `generate_keys` tool. The
+public key goes into `PureMac/Info.plist` as `SUPublicEDKey`. The private key
+should be exported from the same keychain account and stored in GitHub Actions
+as `SPARKLE_ED25519_PRIVATE_KEY`.
+
+Example flow:
+
+```bash
+./bin/generate_keys --account puremac
+./bin/generate_keys --account puremac -x puremac-ed25519.key
+cat puremac-ed25519.key
+```
+
+Use the exported file contents as the secret value. The release workflow pipes
+that value into `generate_appcast --ed-key-file -` when building `appcast.xml`.
+
## Triggering a release
Dry run first (build + sign + notarize, no upload, no homebrew bump):
diff --git a/scripts/release-local.sh b/scripts/release-local.sh
index 8be8eb4..d776fbf 100755
--- a/scripts/release-local.sh
+++ b/scripts/release-local.sh
@@ -7,6 +7,9 @@
# xcrun notarytool store-credentials AC_NOTARY \
# --key ~/.appstoreconnect/private_keys/AuthKey_5G7R52L8RK.p8 \
# --key-id 5G7R52L8RK --issuer 5de3898a-cd31-4061-850f-ae17b389e46a
+# - Sparkle Ed25519 signing key available either in the keychain account
+# named `puremac` or via the SPARKLE_ED25519_PRIVATE_KEY environment
+# variable
# - xcodegen + create-dmg installed (brew install xcodegen create-dmg)
#
# Usage: scripts/release-local.sh [notary_profile]
@@ -19,11 +22,20 @@ VERSION="${1:?Usage: $0 [notary_profile]}"
NOTARY_PROFILE="${2:-AC_NOTARY}"
TEAM_ID="H3WXHVTP97"
SIGN_ID="Developer ID Application: Moamen Basel (${TEAM_ID})"
+SPARKLE_VERSION="2.9.2"
+SPARKLE_ACCOUNT="puremac"
SCHEME="PureMac"
PROJECT="PureMac.xcodeproj"
APP="build/export/PureMac.app"
DMG="build/PureMac-${VERSION}.dmg"
ZIP="build/PureMac-${VERSION}.zip"
+APPCAST_DIR="build/appcast"
+
+WORK_TMP="${TMPDIR:-${HOME}/.tmp}"
+SPARKLE_TARBALL="${WORK_TMP}/Sparkle-${SPARKLE_VERSION}.tar.xz"
+SPARKLE_DIR="${WORK_TMP}/Sparkle-${SPARKLE_VERSION}"
+
+mkdir -p "${WORK_TMP}"
cd "$(dirname "$0")/.."
@@ -74,11 +86,18 @@ xcodebuild -exportArchive \
-exportOptionsPlist build/ExportOptions.plist
echo "==> verify codesign"
-codesign --verify --deep --strict --verbose=2 "${APP}"
+codesign --verify --deep --strict --verbose=4 "${APP}"
codesign -dvv "${APP}" 2>&1 | grep -E "Identifier|TeamIdentifier|flags|Authority"
codesign -dvv "${APP}" 2>&1 | grep -q "flags=0x10000(runtime)" || { echo "Hardened runtime missing"; exit 1; }
lipo -archs "${APP}/Contents/MacOS/PureMac"
+echo "==> verify Sparkle helper bundles"
+while IFS= read -r helper; do
+ if [[ -n "${helper}" ]]; then
+ codesign --verify --deep --strict --verbose=4 "${helper}"
+ fi
+done < <(find "${APP}" -type d \( -name 'Autoupdate.app' -o -name 'Updater.app' \) | sort)
+
echo "==> dmg"
create-dmg \
--volname "PureMac ${VERSION}" \
@@ -110,6 +129,33 @@ spctl --assess --type install --verbose=4 "${DMG}"
echo "==> final zip with stapled app"
ditto -c -k --keepParent --sequesterRsrc "${APP}" "${ZIP}"
+echo "==> Sparkle appcast"
+rm -rf "${APPCAST_DIR}"
+mkdir -p "${APPCAST_DIR}"
+cp "${DMG}" "${APPCAST_DIR}/"
+curl -L -o "${SPARKLE_TARBALL}" "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
+rm -rf "${SPARKLE_DIR}"
+tar -xf "${SPARKLE_TARBALL}" -C "${WORK_TMP}"
+
+if [[ -n "${SPARKLE_ED25519_PRIVATE_KEY:-}" ]]; then
+ # Explicitly sign the DMG first to emit an EdDSA signature for auditing.
+ printf '%s' "${SPARKLE_ED25519_PRIVATE_KEY}" | "${SPARKLE_DIR}/bin/sign_update" --ed-key-file - "${APPCAST_DIR}/$(basename "${DMG}")" || true
+
+ printf '%s' "${SPARKLE_ED25519_PRIVATE_KEY}" | "${SPARKLE_DIR}/bin/generate_appcast" \
+ --ed-key-file - \
+ --output-path "${APPCAST_DIR}/appcast.xml" \
+ --download-url-prefix "https://github.com/momenbasel/PureMac/releases/download/v${VERSION}/" \
+ --full-release-notes-url "https://github.com/momenbasel/PureMac/releases/tag/v${VERSION}" \
+ "${APPCAST_DIR}"
+else
+ "${SPARKLE_DIR}/bin/generate_appcast" \
+ --account "${SPARKLE_ACCOUNT}" \
+ --output-path "${APPCAST_DIR}/appcast.xml" \
+ --download-url-prefix "https://github.com/momenbasel/PureMac/releases/download/v${VERSION}/" \
+ --full-release-notes-url "https://github.com/momenbasel/PureMac/releases/tag/v${VERSION}" \
+ "${APPCAST_DIR}"
+fi
+
DMG_SHA=$(shasum -a 256 "${DMG}" | awk '{print $1}')
ZIP_SHA=$(shasum -a 256 "${ZIP}" | awk '{print $1}')
@@ -121,6 +167,7 @@ echo "DMG: ${DMG}"
echo " sha256: ${DMG_SHA}"
echo "ZIP: ${ZIP}"
echo " sha256: ${ZIP_SHA}"
+echo "Appcast: ${APPCAST_DIR}/appcast.xml"
echo ""
-echo "Next: gh release create v${VERSION} ${DMG} ${ZIP} --title \"PureMac v${VERSION}\""
+echo "Next: gh release create v${VERSION} ${DMG} ${ZIP} ${APPCAST_DIR}/appcast.xml --title \"PureMac v${VERSION}\""
echo "Then: bump homebrew/puremac.rb sha256 to ${ZIP_SHA}"