From 6a86c64fec2e806d655c066650c44be401436268 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 2 Jun 2025 12:46:58 +1000 Subject: [PATCH 1/5] feat: add auto-updates --- .../Coder-Desktop/Coder_DesktopApp.swift | 4 + Coder-Desktop/Coder-Desktop/Info.plist | 2 + .../Coder-Desktop/UpdaterService.swift | 86 +++++++++++++++++++ .../VPN/VPNSystemExtension.swift | 2 +- .../Views/Settings/GeneralTab.swift | 19 +++- Coder-Desktop/project.yml | 4 +- scripts/update-cask.sh | 1 + 7 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 Coder-Desktop/Coder-Desktop/UpdaterService.swift diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index 35aed082..3080e8c1 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 bb759f6b..f127b2c0 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 00000000..7b8d9227 --- /dev/null +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -0,0 +1,86 @@ +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 = false { + didSet { + if 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 + 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 6b242020..cb8db684 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 532d0f00..7af41e4b 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 679afad0..166a1570 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 4277184a..3366752c 100755 --- a/scripts/update-cask.sh +++ b/scripts/update-cask.sh @@ -101,6 +101,7 @@ cask "coder-desktop${SUFFIX}" do name "Coder Desktop" desc "Native desktop client for Coder" homepage "https://github.com/coder/coder-desktop-macos" + auto_updates true conflicts_with cask: "coder/coder/${CONFLICTS_WITH}" depends_on macos: ">= :sonoma" From 1c7f4decd4c5a306f20b19500dce29de70de600f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 2 Jun 2025 12:58:52 +1000 Subject: [PATCH 2/5] remove preview cask updating --- .github/workflows/release.yml | 4 ++-- scripts/update-cask.sh | 29 ++++++----------------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 484d89e6..5138fe84 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/scripts/update-cask.sh b/scripts/update-cask.sh index 3366752c..98081ec2 100755 --- a/scripts/update-cask.sh +++ b/scripts/update-cask.sh @@ -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,12 +54,6 @@ 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) @@ -72,38 +66,27 @@ 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 # Overwrite the cask file -cat >"$TAP_CHECHOUT_FOLDER"/Casks/coder-desktop${SUFFIX}.rb <"$TAP_CHECHOUT_FOLDER"/Casks/coder-desktop.rb <= :sonoma" pkg "Coder-Desktop.pkg" From 543390b6cc531243df6ccd3de0f08eb53283299f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 2 Jun 2025 13:00:45 +1000 Subject: [PATCH 3/5] once again I have been owned by the fact that I cannot sync the swiftc versions between local and CI --- Coder-Desktop/Coder-Desktop/UpdaterService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index 7b8d9227..d5015d0a 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -5,7 +5,7 @@ final class UpdaterService: NSObject, ObservableObject { private lazy var inner: SPUStandardUpdaterController = .init( startingUpdater: true, updaterDelegate: self, - userDriverDelegate: self, + userDriverDelegate: self ) private var updater: SPUUpdater! @Published var canCheckForUpdates = true From 134c7bc49dc9e638d7c4a4d649e35bb3e062b003 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 2 Jun 2025 13:08:54 +1000 Subject: [PATCH 4/5] typos in update-cask --- scripts/update-cask.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh index 98081ec2..a679fee4 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) @@ -55,11 +55,11 @@ gh release download "$VERSION" \ HASH=$(shasum -a 256 "$GH_RELEASE_FOLDER"/Coder-Desktop.pkg | awk '{print $1}' | tr -d '\n') # 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" @@ -73,10 +73,10 @@ fi git checkout -b "$BREW_BRANCH" -mkdir -p "$TAP_CHECHOUT_FOLDER"/Casks +mkdir -p "$TAP_CHECKOUT_FOLDER"/Casks # Overwrite the cask file -cat >"$TAP_CHECHOUT_FOLDER"/Casks/coder-desktop.rb <"$TAP_CHECKOUT_FOLDER"/Casks/coder-desktop.rb < Date: Mon, 2 Jun 2025 13:55:42 +1000 Subject: [PATCH 5/5] fixup --- Coder-Desktop/Coder-Desktop/UpdaterService.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index d5015d0a..23b86b84 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -10,9 +10,9 @@ final class UpdaterService: NSObject, ObservableObject { private var updater: SPUUpdater! @Published var canCheckForUpdates = true - @Published var autoCheckForUpdates: Bool = false { + @Published var autoCheckForUpdates: Bool! { didSet { - if autoCheckForUpdates != oldValue { + if let autoCheckForUpdates, autoCheckForUpdates != oldValue { updater.automaticallyChecksForUpdates = autoCheckForUpdates } } @@ -31,6 +31,7 @@ final class UpdaterService: NSObject, ObservableObject { .flatMap { UpdateChannel(rawValue: $0) } ?? .stable super.init() updater = inner.updater + autoCheckForUpdates = updater.automaticallyChecksForUpdates updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy