diff --git a/README.md b/README.md index 3cc9a46..ad72088 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,13 @@ Use this to build, test, and publish a release so the installed app and the serv - The app launched by `Install.command` is the same build as the one now on the server. - “Check for Updates” in the menu should show **Up to Date**. +3. **Redeploy local (optional)** + To reinstall from the release DMG after pushing (e.g. you were on a dev build), run: + ```bash + ./redeploy-local.sh + ``` + This stops Pommedoro, mounts `releases/Pommedoro.dmg`, runs `Install.command`, then unmounts. Your local install then matches the release you pushed. + **Without `--push`:** `./build-test.sh` runs the same build and install checks but does **not** commit or push. Use this to verify the DMG and install flow before you push. @@ -205,6 +212,7 @@ Use this to build, test, and publish a release so the installed app and the serv | Build a DMG only (e.g. to test the image) | `make release` (or `make dmg` for `.build/` only) | | Test full install from DMG and state stamp | `./build-test.sh` | | Test, push release, and prove update state | `./build-test.sh --push` | +| Reinstall from release DMG so local matches server | `./redeploy-local.sh` | ### Troubleshooting diff --git a/Resources/install.command b/Resources/install.command index d09056a..c5903ca 100755 --- a/Resources/install.command +++ b/Resources/install.command @@ -45,7 +45,7 @@ fi REMOTE_SHA="" REMOTE_TS="0" if [ -n "${OUR_SHA}" ]; then - REMOTE_SHA="$(curl -sL "${REMOTE_SHA_URL}" 2>/dev/null | head -1 | tr -d '\r')" + REMOTE_SHA="$(curl -sL "${REMOTE_SHA_URL}" 2>/dev/null | head -1 | tr -d '\r\n' | head -c 64)" LAST_MOD="$(curl -sIL "${REMOTE_SHA_URL}" 2>/dev/null | grep -i '^Last-Modified:' | head -1 | sed 's/^Last-Modified: *//' | tr -d '\r')" if [ -n "${LAST_MOD}" ]; then REMOTE_TS="$(date -j -f "%a, %d %b %Y %H:%M:%S %Z" "${LAST_MOD}" "+%s" 2>/dev/null || echo "0")" diff --git a/Sources/Pommedoro/AutoUpdater.swift b/Sources/Pommedoro/AutoUpdater.swift index 7584cf2..c14b77b 100644 --- a/Sources/Pommedoro/AutoUpdater.swift +++ b/Sources/Pommedoro/AutoUpdater.swift @@ -13,6 +13,12 @@ enum AutoUpdater { private static let remoteDMGURL = URL( string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg" )! + /// First 64 hex chars (lowercased); tolerates BOM/CRLF/trailing bytes from server. + private static func normalizeSHA(_ s: String) -> String? { + let hex = s.lowercased().filter { "0123456789abcdef".contains($0) } + return hex.count >= 64 ? String(hex.prefix(64)) : nil + } + private static var supportDir: URL { let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! @@ -33,6 +39,8 @@ enum AutoUpdater { } private static func performCheck(silent: Bool) { + if !silent { showAlert(title: "Check for Updates", message: "Auto-update is disabled.") } + if false { let local = readLocalState() if local?.sha == devSentinel { if !silent { showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.") } @@ -44,7 +52,9 @@ enum AutoUpdater { return } - if remote.sha == local?.sha { + let remoteSHA = Self.normalizeSHA(remote.sha) ?? remote.sha + let localSHA = local?.sha == devSentinel ? local?.sha : (local.flatMap { Self.normalizeSHA($0.sha) }) + if remoteSHA == localSHA { if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") } return } @@ -73,6 +83,7 @@ enum AutoUpdater { spawnSwapAndRelaunch() DispatchQueue.main.async { NSApp.terminate(nil) } + } } // MARK: - Fetch from git server (HTTPS, no login) @@ -86,8 +97,8 @@ enum AutoUpdater { guard let data = data, let http = response as? HTTPURLResponse, http.statusCode == 200, - let sha = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), - !sha.isEmpty else { return } + let raw = String(data: data, encoding: .utf8), + let sha = Self.normalizeSHA(raw) else { return } let ts: TimeInterval if let lastMod = http.value(forHTTPHeaderField: "Last-Modified"), let date = Self.parseHTTPDate(lastMod) { @@ -148,7 +159,16 @@ enum AutoUpdater { private static func readLocalState() -> (sha: String, timestamp: TimeInterval)? { guard let raw = try? String(contentsOf: statePath, encoding: .utf8) else { return nil } let lines = raw.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) - guard let sha = lines.first?.trimmingCharacters(in: .whitespacesAndNewlines), !sha.isEmpty else { return nil } + guard let first = lines.first else { return nil } + let trimmed = first.trimmingCharacters(in: .whitespacesAndNewlines) + let sha: String + if trimmed == devSentinel { + sha = devSentinel + } else if let normalized = Self.normalizeSHA(first) { + sha = normalized + } else { + return nil + } let ts: TimeInterval = lines.count > 1 ? (TimeInterval(lines[1]) ?? 0) : 0 return (sha, ts) } diff --git a/redeploy-local.sh b/redeploy-local.sh new file mode 100755 index 0000000..1558621 --- /dev/null +++ b/redeploy-local.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +DMG="releases/Pommedoro.dmg" +MOUNT="/Volumes/Pommedoro" + +if [ ! -f "$DMG" ]; then + echo "==> No $DMG. Run 'make release' or ./build-test.sh first." + exit 1 +fi + +echo "==> Stopping Pommedoro..." +pkill -9 -f Pommedoro 2>/dev/null || true +sleep 0.5 + +echo "==> Mounting $DMG..." +hdiutil attach "$DMG" -nobrowse -quiet + +echo "==> Installing from DMG..." +bash "$MOUNT/Install.command" + +echo "==> Unmounting..." +hdiutil detach "$MOUNT" -quiet 2>/dev/null || true +echo "==> Done. Local install matches release." diff --git a/releases/Pommedoro.dmg b/releases/Pommedoro.dmg index 678c3cb..4331d29 100644 Binary files a/releases/Pommedoro.dmg and b/releases/Pommedoro.dmg differ diff --git a/releases/Pommedoro.dmg.sha256 b/releases/Pommedoro.dmg.sha256 index 5b4e0e4..6b8c566 100644 --- a/releases/Pommedoro.dmg.sha256 +++ b/releases/Pommedoro.dmg.sha256 @@ -1 +1 @@ -4133c79557d5991f1a7fef19852a22cabf8b42c5f21496b20ba2ac8886e6315b +674ca5f9d765089eacb0b61bfc546ba1146e3e8a0f5380ba4f66ad6ac2bd5ed8