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/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..23b86b84 --- /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 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..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) @@ -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 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