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:
parent
b952568441
commit
4f900f1ea4
|
|
@ -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.
|
- 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**.
|
- “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`:**
|
**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.
|
`./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) |
|
| 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 full install from DMG and state stamp | `./build-test.sh` |
|
||||||
| Test, push release, and prove update state | `./build-test.sh --push` |
|
| Test, push release, and prove update state | `./build-test.sh --push` |
|
||||||
|
| Reinstall from release DMG so local matches server | `./redeploy-local.sh` |
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ fi
|
||||||
REMOTE_SHA=""
|
REMOTE_SHA=""
|
||||||
REMOTE_TS="0"
|
REMOTE_TS="0"
|
||||||
if [ -n "${OUR_SHA}" ]; then
|
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')"
|
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
|
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")"
|
REMOTE_TS="$(date -j -f "%a, %d %b %Y %H:%M:%S %Z" "${LAST_MOD}" "+%s" 2>/dev/null || echo "0")"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ enum AutoUpdater {
|
||||||
private static let remoteDMGURL = URL(
|
private static let remoteDMGURL = URL(
|
||||||
string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg"
|
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 {
|
private static var supportDir: URL {
|
||||||
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
|
@ -33,6 +39,8 @@ enum AutoUpdater {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func performCheck(silent: Bool) {
|
private static func performCheck(silent: Bool) {
|
||||||
|
if !silent { showAlert(title: "Check for Updates", message: "Auto-update is disabled.") }
|
||||||
|
if false {
|
||||||
let local = readLocalState()
|
let local = readLocalState()
|
||||||
if local?.sha == devSentinel {
|
if local?.sha == devSentinel {
|
||||||
if !silent { showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.") }
|
if !silent { showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.") }
|
||||||
|
|
@ -44,7 +52,9 @@ enum AutoUpdater {
|
||||||
return
|
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.") }
|
if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +84,7 @@ enum AutoUpdater {
|
||||||
|
|
||||||
DispatchQueue.main.async { NSApp.terminate(nil) }
|
DispatchQueue.main.async { NSApp.terminate(nil) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Fetch from git server (HTTPS, no login)
|
// MARK: - Fetch from git server (HTTPS, no login)
|
||||||
|
|
||||||
|
|
@ -86,8 +97,8 @@ enum AutoUpdater {
|
||||||
guard let data = data,
|
guard let data = data,
|
||||||
let http = response as? HTTPURLResponse,
|
let http = response as? HTTPURLResponse,
|
||||||
http.statusCode == 200,
|
http.statusCode == 200,
|
||||||
let sha = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
let raw = String(data: data, encoding: .utf8),
|
||||||
!sha.isEmpty else { return }
|
let sha = Self.normalizeSHA(raw) else { return }
|
||||||
let ts: TimeInterval
|
let ts: TimeInterval
|
||||||
if let lastMod = http.value(forHTTPHeaderField: "Last-Modified"),
|
if let lastMod = http.value(forHTTPHeaderField: "Last-Modified"),
|
||||||
let date = Self.parseHTTPDate(lastMod) {
|
let date = Self.parseHTTPDate(lastMod) {
|
||||||
|
|
@ -148,7 +159,16 @@ enum AutoUpdater {
|
||||||
private static func readLocalState() -> (sha: String, timestamp: TimeInterval)? {
|
private static func readLocalState() -> (sha: String, timestamp: TimeInterval)? {
|
||||||
guard let raw = try? String(contentsOf: statePath, encoding: .utf8) else { return nil }
|
guard let raw = try? String(contentsOf: statePath, encoding: .utf8) else { return nil }
|
||||||
let lines = raw.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
|
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
|
let ts: TimeInterval = lines.count > 1 ? (TimeInterval(lines[1]) ?? 0) : 0
|
||||||
return (sha, ts)
|
return (sha, ts)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -1 +1 @@
|
||||||
4133c79557d5991f1a7fef19852a22cabf8b42c5f21496b20ba2ac8886e6315b
|
674ca5f9d765089eacb0b61bfc546ba1146e3e8a0f5380ba4f66ad6ac2bd5ed8
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue