From b2da4901f7ca6a27c8c96b9dc6836cec2f02403f Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 27 May 2025 13:29:52 +1000
Subject: [PATCH 1/3] fix: don't create http client if signed out (#166)

If the session item in the keychain is missing but `hasSession` is true, the app will force unwrap the session token optional and crash on launch. Encountered this today.
---
 Coder-Desktop/Coder-Desktop/State.swift | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift
index 902d5a12..faf15e05 100644
--- a/Coder-Desktop/Coder-Desktop/State.swift
+++ b/Coder-Desktop/Coder-Desktop/State.swift
@@ -120,6 +120,7 @@ class AppState: ObservableObject {
             _sessionToken = Published(initialValue: keychainGet(for: Keys.sessionToken))
             if sessionToken == nil || sessionToken!.isEmpty == true {
                 clearSession()
+                return
             }
             client = Client(
                 url: baseAccessURL!,

From 7af0cdc24934196c1ff35cbe657c04f36cc0fcdb Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 28 May 2025 15:25:02 +1000
Subject: [PATCH 2/3] fix: conform CFBundleVersion to documentation (#167)

Second PR for #47.

Previously, we were setting [`CFBundleVersion`](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion) to the output of `git describe --tags` (`vX.Y.Z` or  `vX.Y.Z-N-gHASH` for preview builds).

To support Sparkle, and potentially to avoid a breakage with macOS failing to update an installed `LaunchDaemon` when it can't parse `CFBundleVersion`, we'll conform the string to the specification.

Given that:
> You can include more integers but the system ignores them.

We set `CFBundleVersion` to a value of the form `X.Y.Z[.N]` where N is the number of commits since the `X.Y.Z` tag (omitted if 0)

Sparkle did previously allow you to supply a manual version comparator, but it was deprecated to help require `CFBundleVersion` start with `X.Y.Z` https://github.com/sparkle-project/Sparkle/issues/2585

That issue recommends instead putting marketing version information in `CFBundleShortVersionString`, but that actually has even stricter requirements: https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleshortversionstring

Though not documented, from testing & reading the [Sparkle source](https://github.com/sparkle-project/Sparkle/blob/2.x/Sparkle/SUStandardVersionComparator.m), I discovered that `X.Y.Z.N+1` will be deemed a later version than `X.Y.Z.N`, which is what we'll do for the preview stream of auto-updates.

For non-preview builds (i.e. builds on a tag), both version strings will be `X.Y.Z`.

Since we're no longer including the commit hash in a version string, we instead embed it separately in the `Info.plist` so we can continue to display it in the UI:
<img width="284" alt="image" src="https://github.com/user-attachments/assets/a9b2359d-6e2f-4f7d-979c-ba69ee01e69c" />
---
 Coder-Desktop/Coder-Desktop/About.swift |  7 +++
 Coder-Desktop/Coder-Desktop/Info.plist  |  4 +-
 Coder-Desktop/project.yml               |  1 +
 Makefile                                | 15 ++++++-
 scripts/version.sh                      | 60 +++++++++++++++++++++++++
 5 files changed, 84 insertions(+), 3 deletions(-)
 create mode 100755 scripts/version.sh

diff --git a/Coder-Desktop/Coder-Desktop/About.swift b/Coder-Desktop/Coder-Desktop/About.swift
index 8849c9bd..902ef409 100644
--- a/Coder-Desktop/Coder-Desktop/About.swift
+++ b/Coder-Desktop/Coder-Desktop/About.swift
@@ -31,11 +31,18 @@ enum About {
         return coder
     }
 
+    private static var version: NSString {
+        let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
+        let commitHash = Bundle.main.infoDictionary?["CommitHash"] as? String ?? "Unknown"
+        return "Version \(version) - \(commitHash)" as NSString
+    }
+
     @MainActor
     static func open() {
         appActivate()
         NSApp.orderFrontStandardAboutPanel(options: [
             .credits: credits,
+            .applicationVersion: version,
         ])
     }
 }
diff --git a/Coder-Desktop/Coder-Desktop/Info.plist b/Coder-Desktop/Coder-Desktop/Info.plist
index c1bf929a..bb759f6b 100644
--- a/Coder-Desktop/Coder-Desktop/Info.plist
+++ b/Coder-Desktop/Coder-Desktop/Info.plist
@@ -32,6 +32,8 @@
 		<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop.VPN</string>
 	</dict>
 	<key>SUPublicEDKey</key>
-    <string>Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=</string>
+	<string>Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=</string>
+	<key>CommitHash</key>
+	<string>$(GIT_COMMIT_HASH)</string>
 </dict>
 </plist>
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index 224add81..679afad0 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -13,6 +13,7 @@ settings:
   base:
     MARKETING_VERSION: ${MARKETING_VERSION} # Sets the version number.
     CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # Sets the build number.
+    GIT_COMMIT_HASH: ${GIT_COMMIT_HASH}
 
     ALWAYS_SEARCH_USER_PATHS: NO
     ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
diff --git a/Makefile b/Makefile
index e50b060c..4172f04d 100644
--- a/Makefile
+++ b/Makefile
@@ -32,19 +32,29 @@ $(error MUTAGEN_VERSION must be a valid version)
 endif
 
 ifndef CURRENT_PROJECT_VERSION
-CURRENT_PROJECT_VERSION:=$(shell git describe --match 'v[0-9]*' --dirty='.devel' --always --tags)
+# Must be X.Y.Z[.N]
+CURRENT_PROJECT_VERSION:=$(shell ./scripts/version.sh)
 endif
 ifeq ($(strip $(CURRENT_PROJECT_VERSION)),)
 $(error CURRENT_PROJECT_VERSION cannot be empty)
 endif
 
 ifndef MARKETING_VERSION
-MARKETING_VERSION:=$(shell git describe --match 'v[0-9]*' --tags --abbrev=0 | sed 's/^v//' | sed 's/-.*$$//')
+# Must be X.Y.Z
+MARKETING_VERSION:=$(shell ./scripts/version.sh --short)
 endif
 ifeq ($(strip $(MARKETING_VERSION)),)
 $(error MARKETING_VERSION cannot be empty)
 endif
 
+ifndef GIT_COMMIT_HASH
+# Must be a valid git commit hash
+GIT_COMMIT_HASH := $(shell ./scripts/version.sh --hash)
+endif
+ifeq ($(strip $(GIT_COMMIT_HASH)),)
+$(error GIT_COMMIT_HASH cannot be empty)
+endif
+
 # Define the keychain file name first
 KEYCHAIN_FILE := app-signing.keychain-db
 # Use shell to get the absolute path only if the file exists
@@ -70,6 +80,7 @@ $(XCPROJECT): $(PROJECT)/project.yml
 		EXT_PROVISIONING_PROFILE_ID=${EXT_PROVISIONING_PROFILE_ID} \
 		CURRENT_PROJECT_VERSION=$(CURRENT_PROJECT_VERSION) \
 		MARKETING_VERSION=$(MARKETING_VERSION) \
+		GIT_COMMIT_HASH=$(GIT_COMMIT_HASH) \
 		xcodegen
 
 $(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
diff --git a/scripts/version.sh b/scripts/version.sh
new file mode 100755
index 00000000..3ca8d03f
--- /dev/null
+++ b/scripts/version.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+    echo "Usage: $0 [--short] [--hash]"
+    echo "  --short     Output a CFBundleShortVersionString compatible version (X.Y.Z)"
+    echo "  --hash      Output only the commit hash"
+    echo "  -h, --help  Display this help message"
+    echo ""
+    echo "With no flags, outputs: X.Y.Z[.N]"
+}
+
+SHORT=false
+HASH_ONLY=false
+
+while [[ "$#" -gt 0 ]]; do
+    case $1 in
+        --short)
+            SHORT=true
+            shift
+            ;;
+        --hash)
+            HASH_ONLY=true
+            shift
+            ;;
+        -h | --help)
+            usage
+            exit 0
+            ;;
+        *)
+            echo "Unknown parameter passed: $1"
+            usage
+            exit 1
+            ;;
+    esac
+done
+
+if [[ "$HASH_ONLY" == true ]]; then
+    current_hash=$(git rev-parse --short=7 HEAD)
+    echo "$current_hash"
+    exit 0
+fi
+
+describe_output=$(git describe --tags)
+
+# Of the form `vX.Y.Z-N-gHASH`
+if [[ $describe_output =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-([0-9]+)-g[a-f0-9]+)?$ ]]; then
+    version=${BASH_REMATCH[1]}  # X.Y.Z
+    commits=${BASH_REMATCH[3]}  # number of commits since tag
+
+    if [[ "$SHORT" == true ]]; then
+        echo "$version"
+        exit 0
+    fi
+
+    echo "${version}.${commits}"
+else
+    echo "Error: Could not parse git describe output: $describe_output" >&2
+    exit 1
+fi
\ No newline at end of file

From 5785faeffa663cb3e9e5762e25b1102b0ac25d03 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 28 May 2025 16:44:39 +1000
Subject: [PATCH 3/3] ci: remove trailing dot from release versions (#170)

---
 scripts/version.sh | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/scripts/version.sh b/scripts/version.sh
index 3ca8d03f..602a8001 100755
--- a/scripts/version.sh
+++ b/scripts/version.sh
@@ -48,7 +48,9 @@ if [[ $describe_output =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-([0-9]+)-g[a-f0-9]+)?$ ]];
     version=${BASH_REMATCH[1]}  # X.Y.Z
     commits=${BASH_REMATCH[3]}  # number of commits since tag
 
-    if [[ "$SHORT" == true ]]; then
+    # If we're producing a short version string, or this is a release version
+    # (no commits since tag)
+    if [[ "$SHORT" == true ]] || [[ -z "$commits" ]]; then
         echo "$version"
         exit 0
     fi
