Skip to content

Commit aeb1e68

Browse files
feat: add auto-updates (#176)
Closes #47. Stable: <img width="615" alt="image" src="https://github.com/user-attachments/assets/e34c8138-dac7-48ab-af76-0feea79c9f7e" /> Preview: <img width="614" alt="image" src="https://github.com/user-attachments/assets/caeb2750-b735-473d-8568-e8f1098954d0" /> Additionally: - Removes the updating of the `coder-desktop-preview` cask. - Marks the `coder-desktop` cask as auto-updating, so brew doesn't attempt to `upgrade` itself. I'll also need to make a PR on the `homebrew-coder` repo to mark it as deprecated in brew. If a user wishes to be on the preview channel, they just need to install the stable version, and switch to the preview channel in settings.
1 parent 3c72ff4 commit aeb1e68

File tree

8 files changed

+128
-40
lines changed

8 files changed

+128
-40
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
update-cask:
109109
name: Update homebrew-coder cask
110110
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
111-
if: ${{ github.repository_owner == 'coder' && !inputs.dryrun }}
111+
if: ${{ github.repository_owner == 'coder' && github.event_name == 'release' }}
112112
needs: build
113113
steps:
114114
- name: Checkout
@@ -124,7 +124,7 @@ jobs:
124124
- name: Update homebrew-coder
125125
env:
126126
GH_TOKEN: ${{ secrets.CODERCI_GITHUB_TOKEN }}
127-
RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || 'preview' }}
127+
RELEASE_TAG: ${{ github.event.release.tag_name }}
128128
ASSIGNEE: ${{ github.actor }}
129129
run: |
130130
git config --global user.email "[email protected]"

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import NetworkExtension
33
import os
44
import SDWebImageSVGCoder
55
import SDWebImageSwiftUI
6+
import Sparkle
67
import SwiftUI
78
import UserNotifications
89
import VPNLib
@@ -26,6 +27,7 @@ struct DesktopApp: App {
2627
.environmentObject(appDelegate.vpn)
2728
.environmentObject(appDelegate.state)
2829
.environmentObject(appDelegate.helper)
30+
.environmentObject(appDelegate.autoUpdater)
2931
}
3032
.windowResizability(.contentSize)
3133
Window("Coder File Sync", id: Windows.fileSync.rawValue) {
@@ -47,11 +49,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4749
let urlHandler: URLHandler
4850
let notifDelegate: NotifDelegate
4951
let helper: HelperService
52+
let autoUpdater: UpdaterService
5053

5154
override init() {
5255
notifDelegate = NotifDelegate()
5356
vpn = CoderVPNService()
5457
helper = HelperService()
58+
autoUpdater = UpdaterService()
5559
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
5660
vpn.onStart = {
5761
// We don't need this to have finished before the VPN actually starts

Coder-Desktop/Coder-Desktop/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,7 @@
3535
<string>Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=</string>
3636
<key>CommitHash</key>
3737
<string>$(GIT_COMMIT_HASH)</string>
38+
<key>SUFeedURL</key>
39+
<string>https://releases.coder.com/coder-desktop/mac/appcast.xml</string>
3840
</dict>
3941
</plist>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Sparkle
2+
import SwiftUI
3+
4+
final class UpdaterService: NSObject, ObservableObject {
5+
private lazy var inner: SPUStandardUpdaterController = .init(
6+
startingUpdater: true,
7+
updaterDelegate: self,
8+
userDriverDelegate: self
9+
)
10+
private var updater: SPUUpdater!
11+
@Published var canCheckForUpdates = true
12+
13+
@Published var autoCheckForUpdates: Bool! {
14+
didSet {
15+
if let autoCheckForUpdates, autoCheckForUpdates != oldValue {
16+
updater.automaticallyChecksForUpdates = autoCheckForUpdates
17+
}
18+
}
19+
}
20+
21+
@Published var updateChannel: UpdateChannel {
22+
didSet {
23+
UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey)
24+
}
25+
}
26+
27+
static let updateChannelKey = "updateChannel"
28+
29+
override init() {
30+
updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey)
31+
.flatMap { UpdateChannel(rawValue: $0) } ?? .stable
32+
super.init()
33+
updater = inner.updater
34+
autoCheckForUpdates = updater.automaticallyChecksForUpdates
35+
updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates)
36+
}
37+
38+
func checkForUpdates() {
39+
guard canCheckForUpdates else { return }
40+
updater.checkForUpdates()
41+
}
42+
}
43+
44+
enum UpdateChannel: String, CaseIterable, Identifiable {
45+
case stable
46+
case preview
47+
48+
var name: String {
49+
switch self {
50+
case .stable:
51+
"Stable"
52+
case .preview:
53+
"Preview"
54+
}
55+
}
56+
57+
var id: String { rawValue }
58+
}
59+
60+
extension UpdaterService: SPUUpdaterDelegate {
61+
func allowedChannels(for _: SPUUpdater) -> Set<String> {
62+
// There's currently no point in subscribing to both channels, as
63+
// preview >= stable
64+
[updateChannel.rawValue]
65+
}
66+
}
67+
68+
extension UpdaterService: SUVersionDisplay {
69+
func formatUpdateVersion(
70+
fromUpdate update: SUAppcastItem,
71+
andBundleDisplayVersion inOutBundleDisplayVersion: AutoreleasingUnsafeMutablePointer<NSString>,
72+
withBundleVersion bundleVersion: String
73+
) -> String {
74+
// Replace CFBundleShortVersionString with CFBundleVersion, as the
75+
// latter shows build numbers.
76+
inOutBundleDisplayVersion.pointee = bundleVersion as NSString
77+
// This is already CFBundleVersion, as that's the only version in the
78+
// appcast.
79+
return update.displayVersionString
80+
}
81+
}
82+
83+
extension UpdaterService: SPUStandardUserDriverDelegate {
84+
func standardUserDriverRequestsVersionDisplayer() -> (any SUVersionDisplay)? {
85+
self
86+
}
87+
}

Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
174174
actionForReplacingExtension existing: OSSystemExtensionProperties,
175175
withExtension extension: OSSystemExtensionProperties
176176
) -> OSSystemExtensionRequest.ReplacementAction {
177-
logger.info("Replacing \(request.identifier) v\(existing.bundleVersion) with v\(`extension`.bundleVersion)")
177+
logger.info("Replacing \(request.identifier) \(existing.bundleVersion) with \(`extension`.bundleVersion)")
178178
// This is counterintuitive, but this function is only called if the
179179
// versions are the same in a dev environment.
180180
// In a release build, this only gets called when the version string is

Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33

44
struct GeneralTab: View {
55
@EnvironmentObject var state: AppState
6+
@EnvironmentObject var updater: UpdaterService
67
var body: some View {
78
Form {
89
Section {
@@ -18,10 +19,20 @@ struct GeneralTab: View {
1819
Text("Start Coder Connect on launch")
1920
}
2021
}
22+
Section {
23+
Toggle(isOn: $updater.autoCheckForUpdates) {
24+
Text("Automatically check for updates")
25+
}
26+
Picker("Update channel", selection: $updater.updateChannel) {
27+
ForEach(UpdateChannel.allCases) { channel in
28+
Text(channel.name).tag(channel)
29+
}
30+
}
31+
HStack {
32+
Spacer()
33+
Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates)
34+
}
35+
}
2136
}.formStyle(.grouped)
2237
}
2338
}
24-
25-
#Preview {
26-
GeneralTab()
27-
}

Coder-Desktop/project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ options:
1111

1212
settings:
1313
base:
14-
MARKETING_VERSION: ${MARKETING_VERSION} # Sets the version number.
15-
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # Sets the build number.
14+
MARKETING_VERSION: ${MARKETING_VERSION} # Sets CFBundleShortVersionString
15+
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # CFBundleVersion
1616
GIT_COMMIT_HASH: ${GIT_COMMIT_HASH}
1717

1818
ALWAYS_SEARCH_USER_PATHS: NO

scripts/update-cask.sh

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ set -euo pipefail
44
usage() {
55
echo "Usage: $0 [--version <version>] [--assignee <github handle>]"
66
echo " --version <version> Set the VERSION variable to fetch and generate the cask file for"
7-
echo " --assignee <github handle> Set the ASSIGNE variable to assign the PR to (optional)"
7+
echo " --assignee <github handle> Set the ASSIGNEE variable to assign the PR to (optional)"
88
echo " -h, --help Display this help message"
99
}
1010

1111
VERSION=""
12-
ASSIGNE=""
12+
ASSIGNEE=""
1313

1414
# Parse command line arguments
1515
while [[ "$#" -gt 0 ]]; do
@@ -19,7 +19,7 @@ while [[ "$#" -gt 0 ]]; do
1919
shift 2
2020
;;
2121
--assignee)
22-
ASSIGNE="$2"
22+
ASSIGNEE="$2"
2323
shift 2
2424
;;
2525
-h | --help)
@@ -39,7 +39,7 @@ done
3939
echo "Error: VERSION cannot be empty"
4040
exit 1
4141
}
42-
[[ "$VERSION" =~ ^v || "$VERSION" == "preview" ]] || {
42+
[[ "$VERSION" =~ ^v ]] || {
4343
echo "Error: VERSION must start with a 'v'"
4444
exit 1
4545
}
@@ -54,55 +54,39 @@ gh release download "$VERSION" \
5454

5555
HASH=$(shasum -a 256 "$GH_RELEASE_FOLDER"/Coder-Desktop.pkg | awk '{print $1}' | tr -d '\n')
5656

57-
IS_PREVIEW=false
58-
if [[ "$VERSION" == "preview" ]]; then
59-
IS_PREVIEW=true
60-
VERSION=$(make 'print-CURRENT_PROJECT_VERSION' | sed 's/CURRENT_PROJECT_VERSION=//g')
61-
fi
62-
6357
# Check out the homebrew tap repo
64-
TAP_CHECHOUT_FOLDER=$(mktemp -d)
58+
TAP_CHECKOUT_FOLDER=$(mktemp -d)
6559

66-
gh repo clone "coder/homebrew-coder" "$TAP_CHECHOUT_FOLDER"
60+
gh repo clone "coder/homebrew-coder" "$TAP_CHECKOUT_FOLDER"
6761

68-
cd "$TAP_CHECHOUT_FOLDER"
62+
cd "$TAP_CHECKOUT_FOLDER"
6963

7064
BREW_BRANCH="auto-release/desktop-$VERSION"
7165

7266
# Check if a PR already exists.
7367
# Continue on a main branch release, as the sha256 will change.
7468
pr_count="$(gh pr list --search "head:$BREW_BRANCH" --json id,closed | jq -r ".[] | select(.closed == false) | .id" | wc -l)"
75-
if [[ "$pr_count" -gt 0 && "$IS_PREVIEW" == false ]]; then
69+
if [[ "$pr_count" -gt 0 ]]; then
7670
echo "Bailing out as PR already exists" 2>&1
7771
exit 0
7872
fi
7973

8074
git checkout -b "$BREW_BRANCH"
8175

82-
# If this is a main branch build, append a preview suffix to the cask.
83-
SUFFIX=""
84-
CONFLICTS_WITH="coder-desktop-preview"
85-
TAG=$VERSION
86-
if [[ "$IS_PREVIEW" == true ]]; then
87-
SUFFIX="-preview"
88-
CONFLICTS_WITH="coder-desktop"
89-
TAG="preview"
90-
fi
91-
92-
mkdir -p "$TAP_CHECHOUT_FOLDER"/Casks
76+
mkdir -p "$TAP_CHECKOUT_FOLDER"/Casks
9377

9478
# Overwrite the cask file
95-
cat >"$TAP_CHECHOUT_FOLDER"/Casks/coder-desktop${SUFFIX}.rb <<EOF
96-
cask "coder-desktop${SUFFIX}" do
79+
cat >"$TAP_CHECKOUT_FOLDER"/Casks/coder-desktop.rb <<EOF
80+
cask "coder-desktop" do
9781
version "${VERSION#v}"
98-
sha256 $([ "$IS_PREVIEW" = true ] && echo ":no_check" || echo "\"${HASH}\"")
82+
sha256 "${HASH}"
9983
100-
url "https://github.com/coder/coder-desktop-macos/releases/download/$([ "$IS_PREVIEW" = true ] && echo "${TAG}" || echo "v#{version}")/Coder-Desktop.pkg"
84+
url "https://github.com/coder/coder-desktop-macos/releases/download/v#{version}/Coder-Desktop.pkg"
10185
name "Coder Desktop"
10286
desc "Native desktop client for Coder"
10387
homepage "https://github.com/coder/coder-desktop-macos"
88+
auto_updates true
10489
105-
conflicts_with cask: "coder/coder/${CONFLICTS_WITH}"
10690
depends_on macos: ">= :sonoma"
10791
10892
pkg "Coder-Desktop.pkg"
@@ -132,5 +116,5 @@ if [[ "$pr_count" -eq 0 ]]; then
132116
--base master --head "$BREW_BRANCH" \
133117
--title "Coder Desktop $VERSION" \
134118
--body "This automatic PR was triggered by the release of Coder Desktop $VERSION" \
135-
${ASSIGNE:+ --assignee "$ASSIGNE" --reviewer "$ASSIGNE"}
119+
${ASSIGNEE:+ --assignee "$ASSIGNEE" --reviewer "$ASSIGNEE"}
136120
fi

0 commit comments

Comments
 (0)