diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 484d89e..5138fe8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -108,7 +108,7 @@ jobs:
update-cask:
name: Update homebrew-coder cask
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
- if: ${{ github.repository_owner == 'coder' && !inputs.dryrun }}
+ if: ${{ github.repository_owner == 'coder' && github.event_name == 'release' }}
needs: build
steps:
- name: Checkout
@@ -124,7 +124,7 @@ jobs:
- name: Update homebrew-coder
env:
GH_TOKEN: ${{ secrets.CODERCI_GITHUB_TOKEN }}
- RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || 'preview' }}
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
ASSIGNEE: ${{ github.actor }}
run: |
git config --global user.email "ci@coder.com"
diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 35aed08..3080e8c 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -3,6 +3,7 @@ import NetworkExtension
import os
import SDWebImageSVGCoder
import SDWebImageSwiftUI
+import Sparkle
import SwiftUI
import UserNotifications
import VPNLib
@@ -26,6 +27,7 @@ struct DesktopApp: App {
.environmentObject(appDelegate.vpn)
.environmentObject(appDelegate.state)
.environmentObject(appDelegate.helper)
+ .environmentObject(appDelegate.autoUpdater)
}
.windowResizability(.contentSize)
Window("Coder File Sync", id: Windows.fileSync.rawValue) {
@@ -47,11 +49,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let urlHandler: URLHandler
let notifDelegate: NotifDelegate
let helper: HelperService
+ let autoUpdater: UpdaterService
override init() {
notifDelegate = NotifDelegate()
vpn = CoderVPNService()
helper = HelperService()
+ autoUpdater = UpdaterService()
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
vpn.onStart = {
// We don't need this to have finished before the VPN actually starts
diff --git a/Coder-Desktop/Coder-Desktop/Info.plist b/Coder-Desktop/Coder-Desktop/Info.plist
index bb759f6..f127b2c 100644
--- a/Coder-Desktop/Coder-Desktop/Info.plist
+++ b/Coder-Desktop/Coder-Desktop/Info.plist
@@ -35,5 +35,7 @@
Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=
CommitHash
$(GIT_COMMIT_HASH)
+ SUFeedURL
+ https://releases.coder.com/coder-desktop/mac/appcast.xml
diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift
new file mode 100644
index 0000000..23b86b8
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift
@@ -0,0 +1,87 @@
+import Sparkle
+import SwiftUI
+
+final class UpdaterService: NSObject, ObservableObject {
+ private lazy var inner: SPUStandardUpdaterController = .init(
+ startingUpdater: true,
+ updaterDelegate: self,
+ userDriverDelegate: self
+ )
+ private var updater: SPUUpdater!
+ @Published var canCheckForUpdates = true
+
+ @Published var autoCheckForUpdates: Bool! {
+ didSet {
+ if let autoCheckForUpdates, autoCheckForUpdates != oldValue {
+ updater.automaticallyChecksForUpdates = autoCheckForUpdates
+ }
+ }
+ }
+
+ @Published var updateChannel: UpdateChannel {
+ didSet {
+ UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey)
+ }
+ }
+
+ static let updateChannelKey = "updateChannel"
+
+ override init() {
+ updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey)
+ .flatMap { UpdateChannel(rawValue: $0) } ?? .stable
+ super.init()
+ updater = inner.updater
+ autoCheckForUpdates = updater.automaticallyChecksForUpdates
+ updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates)
+ }
+
+ func checkForUpdates() {
+ guard canCheckForUpdates else { return }
+ updater.checkForUpdates()
+ }
+}
+
+enum UpdateChannel: String, CaseIterable, Identifiable {
+ case stable
+ case preview
+
+ var name: String {
+ switch self {
+ case .stable:
+ "Stable"
+ case .preview:
+ "Preview"
+ }
+ }
+
+ var id: String { rawValue }
+}
+
+extension UpdaterService: SPUUpdaterDelegate {
+ func allowedChannels(for _: SPUUpdater) -> Set {
+ // There's currently no point in subscribing to both channels, as
+ // preview >= stable
+ [updateChannel.rawValue]
+ }
+}
+
+extension UpdaterService: SUVersionDisplay {
+ func formatUpdateVersion(
+ fromUpdate update: SUAppcastItem,
+ andBundleDisplayVersion inOutBundleDisplayVersion: AutoreleasingUnsafeMutablePointer,
+ withBundleVersion bundleVersion: String
+ ) -> String {
+ // Replace CFBundleShortVersionString with CFBundleVersion, as the
+ // latter shows build numbers.
+ inOutBundleDisplayVersion.pointee = bundleVersion as NSString
+ // This is already CFBundleVersion, as that's the only version in the
+ // appcast.
+ return update.displayVersionString
+ }
+}
+
+extension UpdaterService: SPUStandardUserDriverDelegate {
+ func standardUserDriverRequestsVersionDisplayer() -> (any SUVersionDisplay)? {
+ self
+ }
+}
diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
index 6b24202..cb8db68 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
@@ -174,7 +174,7 @@ class SystemExtensionDelegate:
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension extension: OSSystemExtensionProperties
) -> OSSystemExtensionRequest.ReplacementAction {
- logger.info("Replacing \(request.identifier) v\(existing.bundleVersion) with v\(`extension`.bundleVersion)")
+ logger.info("Replacing \(request.identifier) \(existing.bundleVersion) with \(`extension`.bundleVersion)")
// This is counterintuitive, but this function is only called if the
// versions are the same in a dev environment.
// In a release build, this only gets called when the version string is
diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
index 532d0f0..7af41e4 100644
--- a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
@@ -3,6 +3,7 @@ import SwiftUI
struct GeneralTab: View {
@EnvironmentObject var state: AppState
+ @EnvironmentObject var updater: UpdaterService
var body: some View {
Form {
Section {
@@ -18,10 +19,20 @@ struct GeneralTab: View {
Text("Start Coder Connect on launch")
}
}
+ Section {
+ Toggle(isOn: $updater.autoCheckForUpdates) {
+ Text("Automatically check for updates")
+ }
+ Picker("Update channel", selection: $updater.updateChannel) {
+ ForEach(UpdateChannel.allCases) { channel in
+ Text(channel.name).tag(channel)
+ }
+ }
+ HStack {
+ Spacer()
+ Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates)
+ }
+ }
}.formStyle(.grouped)
}
}
-
-#Preview {
- GeneralTab()
-}
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index 679afad..166a157 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -11,8 +11,8 @@ options:
settings:
base:
- MARKETING_VERSION: ${MARKETING_VERSION} # Sets the version number.
- CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # Sets the build number.
+ MARKETING_VERSION: ${MARKETING_VERSION} # Sets CFBundleShortVersionString
+ CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # CFBundleVersion
GIT_COMMIT_HASH: ${GIT_COMMIT_HASH}
ALWAYS_SEARCH_USER_PATHS: NO
diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh
index 4277184..a679fee 100755
--- a/scripts/update-cask.sh
+++ b/scripts/update-cask.sh
@@ -4,12 +4,12 @@ set -euo pipefail
usage() {
echo "Usage: $0 [--version ] [--assignee ]"
echo " --version Set the VERSION variable to fetch and generate the cask file for"
- echo " --assignee Set the ASSIGNE variable to assign the PR to (optional)"
+ echo " --assignee Set the ASSIGNEE variable to assign the PR to (optional)"
echo " -h, --help Display this help message"
}
VERSION=""
-ASSIGNE=""
+ASSIGNEE=""
# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
@@ -19,7 +19,7 @@ while [[ "$#" -gt 0 ]]; do
shift 2
;;
--assignee)
- ASSIGNE="$2"
+ ASSIGNEE="$2"
shift 2
;;
-h | --help)
@@ -39,7 +39,7 @@ done
echo "Error: VERSION cannot be empty"
exit 1
}
-[[ "$VERSION" =~ ^v || "$VERSION" == "preview" ]] || {
+[[ "$VERSION" =~ ^v ]] || {
echo "Error: VERSION must start with a 'v'"
exit 1
}
@@ -54,55 +54,39 @@ gh release download "$VERSION" \
HASH=$(shasum -a 256 "$GH_RELEASE_FOLDER"/Coder-Desktop.pkg | awk '{print $1}' | tr -d '\n')
-IS_PREVIEW=false
-if [[ "$VERSION" == "preview" ]]; then
- IS_PREVIEW=true
- VERSION=$(make 'print-CURRENT_PROJECT_VERSION' | sed 's/CURRENT_PROJECT_VERSION=//g')
-fi
-
# Check out the homebrew tap repo
-TAP_CHECHOUT_FOLDER=$(mktemp -d)
+TAP_CHECKOUT_FOLDER=$(mktemp -d)
-gh repo clone "coder/homebrew-coder" "$TAP_CHECHOUT_FOLDER"
+gh repo clone "coder/homebrew-coder" "$TAP_CHECKOUT_FOLDER"
-cd "$TAP_CHECHOUT_FOLDER"
+cd "$TAP_CHECKOUT_FOLDER"
BREW_BRANCH="auto-release/desktop-$VERSION"
# Check if a PR already exists.
# Continue on a main branch release, as the sha256 will change.
pr_count="$(gh pr list --search "head:$BREW_BRANCH" --json id,closed | jq -r ".[] | select(.closed == false) | .id" | wc -l)"
-if [[ "$pr_count" -gt 0 && "$IS_PREVIEW" == false ]]; then
+if [[ "$pr_count" -gt 0 ]]; then
echo "Bailing out as PR already exists" 2>&1
exit 0
fi
git checkout -b "$BREW_BRANCH"
-# If this is a main branch build, append a preview suffix to the cask.
-SUFFIX=""
-CONFLICTS_WITH="coder-desktop-preview"
-TAG=$VERSION
-if [[ "$IS_PREVIEW" == true ]]; then
- SUFFIX="-preview"
- CONFLICTS_WITH="coder-desktop"
- TAG="preview"
-fi
-
-mkdir -p "$TAP_CHECHOUT_FOLDER"/Casks
+mkdir -p "$TAP_CHECKOUT_FOLDER"/Casks
# Overwrite the cask file
-cat >"$TAP_CHECHOUT_FOLDER"/Casks/coder-desktop${SUFFIX}.rb <"$TAP_CHECKOUT_FOLDER"/Casks/coder-desktop.rb <= :sonoma"
pkg "Coder-Desktop.pkg"
@@ -132,5 +116,5 @@ if [[ "$pr_count" -eq 0 ]]; then
--base master --head "$BREW_BRANCH" \
--title "Coder Desktop $VERSION" \
--body "This automatic PR was triggered by the release of Coder Desktop $VERSION" \
- ${ASSIGNE:+ --assignee "$ASSIGNE" --reviewer "$ASSIGNE"}
+ ${ASSIGNEE:+ --assignee "$ASSIGNEE" --reviewer "$ASSIGNEE"}
fi