diff --git a/README.md b/README.md index 42de1c4..3cc9a46 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ The actionable suggestions matter too. "Take a break" is an instruction with no - **Debug mode** for testing (1-minute cycles) - **Launch at Login** via macOS LaunchAgent - **Native macOS app** — no Electron, no web views, just AppKit +- **Self-update** — checks the repo over HTTPS (no login); offers an update only when the server has a newer release (SHA + timestamp) ## Changelog @@ -111,27 +112,112 @@ Open **Applications** (or use Spotlight: **Cmd+Space**, type "Pommedoro") and do Requires macOS 13+ and Swift 5.9+. -```bash -make bundle -``` +| Command | What it does | +|---------|--------------| +| `make bundle` | Build the `.app` (default target) | +| `make install` | Build and install to `/Applications`; stamps as **dev build** (auto-update disabled) | +| `make run` | Build and launch the app from `.build/` (does not install) | +| `make dmg` | Build the `.app` and create `.build/Pommedoro.dmg` | +| `make release` | Build DMG and copy it + SHA256 into `releases/` for publishing | +| `make clean` | Remove build artifacts and the app bundle | +| `make icons` | Regenerate `Pommedoro.icns` from `Resources/boot-logo.svg` | +| `make install-agent` | Install LaunchAgent for Launch at Login (after `make install`) | -To install to `/Applications`: +For day-to-day development and releasing to users, see **Development & Release** below. -```bash -make install -``` +--- -To build the DMG: +## Development & Release -```bash -make dmg -``` +This section describes how to work on Pommedoro locally, how releases and auto-update work, and how to publish a new version. -To run directly: +### Local development -```bash -make run -``` +- **Quick run (no install)** + `make run` — builds and opens the app from `.build/Pommedoro.app`. Good for quick tests. + +- **Install and run from /Applications** + `make install` — builds, installs to `/Applications/Pommedoro.app`, and stamps the app as a **dev build** by writing `dev` into the update state file. The app will **not** prompt you to “update” to the release version; the menu item “Check for Updates” will show “Auto-update is disabled for local dev builds.” + +- **After code changes** + Run `make install` again (or `make run`). There is no need to touch the update state for normal dev; the dev stamp persists until you install from a DMG or run a release flow. + +- **Launch at Login (optional)** + `make install-agent` — installs a LaunchAgent that starts Pommedoro at login. Run after `make install`. Use `make uninstall-agent` to remove it. + +### How releases and auto-update work + +- **Release artifacts** + A release consists of: + - `releases/Pommedoro.dmg` — the disk image users install from. + - `releases/Pommedoro.dmg.sha256` — the SHA256 hash of that DMG (one line, 64 hex chars). + +- **Where the app checks for updates** + The app fetches **over HTTPS with no login**: + - SHA: `https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256` + - DMG: `https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg` + It uses the response **Last-Modified** header as the release “timestamp” so it only offers an update when the server has a **newer** release, not just a different one. + +- **Local state (what version am I?)** + The app stores its idea of the current release in: + - **File:** `~/Library/Application Support/Pommedoro/current.sha256` + - **Format:** two lines: `SHA256` (64 hex chars) and a Unix timestamp (or `0`). + - **Set when:** + - Installing from the DMG (via `Install.command`) — writes the DMG’s SHA and, if it matches the remote, the remote’s Last-Modified timestamp. + - After a successful in-app update — the swap script writes the new SHA and timestamp. + - After `make install` — writes `dev` so the app never nags for updates. + +- **When “Update available” is shown** + Only if **both** are true: + 1. The release SHA on the server is **different** from the one in the state file. + 2. The server’s Last-Modified is **newer** than the stored timestamp (so we don’t offer “updates” to an older release). + + If the SHA in the state file matches the server, the app always reports “Up to Date,” regardless of timestamp. + +### Full release workflow (recommended) + +Use this to build, test, and publish a release so the installed app and the server stay in sync and “Check for Updates” behaves correctly. + +1. **Run the full test and push the release** + ```bash + ./build-test.sh --push + ``` + This will: + - Stop any running Pommedoro, clean the build, and run `make release`. + - Create the DMG and `releases/Pommedoro.dmg.sha256`. + - Simulate a user install (quarantine, mount DMG, run `Install.command`), then verify the app runs and the local state file is stamped with the release SHA. + - **Push:** `git add releases/Pommedoro.dmg releases/Pommedoro.dmg.sha256`, commit, and push. + - **Prove:** Fetch the remote SHA and confirm it equals the first line of the local state file. If not, the script exits with failure. + +2. **Result** + - 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**. + +**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. + +### When to run what + +| Goal | Command | +|------|--------| +| Code and run locally without installing | `make run` | +| Install to /Applications and develop (no update prompts) | `make install` | +| 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` | + +### Troubleshooting + +- **“Update available” even though I just pushed / same version** + The app compares the **first line** of `~/Library/Application Support/Pommedoro/current.sha256` to the SHA from the server. If you installed from a DMG and then pushed that same build, run `./build-test.sh --push` so the script pushes and then verifies remote SHA = local state. If the script’s proof passes but the app still nags, the app may be reading a different path (e.g. different user); the script and app both use the same state file path. + +- **I’m a dev; I don’t want update prompts** + Use `make install`. It writes `dev` into the state file so the app never offers to update. Installing from a DMG (or updating in-app) overwrites that with a real SHA + timestamp. + +- **State file location** + `~/Library/Application Support/Pommedoro/current.sha256`. Two lines: SHA, then timestamp (or `0`). For a dev build, the first line is the literal string `dev`. + +--- ## Requirements diff --git a/build-test.sh b/build-test.sh index 7e5e401..aae1790 100755 --- a/build-test.sh +++ b/build-test.sh @@ -84,3 +84,25 @@ echo "" echo "==> All checks passed." echo "==> DMG at: .build/Pommedoro.dmg" echo "==> Release at: releases/Pommedoro.dmg + releases/Pommedoro.dmg.sha256" + +# Optional: push release and prove remote matches launched app +if [ "${1:-}" = "--push" ]; then + echo "" + echo "==> Pushing release to remote..." + git add releases/Pommedoro.dmg releases/Pommedoro.dmg.sha256 + if git diff --staged --quiet 2>/dev/null; then + echo "==> No release changes to push." + else + RELEASE_SHA="$(cat releases/Pommedoro.dmg.sha256)" + git commit -m "Release: ${RELEASE_SHA}" + git push + fi + echo "==> Proving remote matches launched app..." + REMOTE_SHA="$(curl -sL "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256" | tr -d '\r\n' | head -c 64)" + LOCAL_SHA="$(head -1 "${SHA_FILE}" | tr -d '\r\n' | head -c 64)" + if [ -z "${REMOTE_SHA}" ] || [ "${REMOTE_SHA}" != "${LOCAL_SHA}" ]; then + echo "==> FAIL: remote SHA (${REMOTE_SHA}) != local state (${LOCAL_SHA}). Update check would not show Up to Date." + exit 1 + fi + echo "==> PROVEN: remote SHA = local state. Launched app is identical to release; Check for Updates will show Up to Date." +fi