Release: 674ca5f9d765089eacb0b61bfc546ba1146e3e8a0f5380ba4f66ad6ac2bd5ed8

Harden SHA normalization in AutoUpdater and install.command.
Add redeploy-local.sh convenience script and update README.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-08 16:53:40 -05:00
parent b952568441
commit 4f900f1ea4
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
6 changed files with 58 additions and 6 deletions

View File

@ -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

View File

@ -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")"

View File

@ -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
}
@ -74,6 +84,7 @@ enum AutoUpdater {
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)
}

24
redeploy-local.sh Executable file
View File

@ -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."

Binary file not shown.

View File

@ -1 +1 @@
4133c79557d5991f1a7fef19852a22cabf8b42c5f21496b20ba2ac8886e6315b
674ca5f9d765089eacb0b61bfc546ba1146e3e8a0f5380ba4f66ad6ac2bd5ed8