Update README and build-test script

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-08 16:25:34 -05:00
parent 49287c15eb
commit 3af2c79326
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
2 changed files with 123 additions and 15 deletions

112
README.md
View File

@ -77,6 +77,7 @@ The actionable suggestions matter too. "Take a break" is an instruction with no
- **Debug mode** for testing (1-minute cycles) - **Debug mode** for testing (1-minute cycles)
- **Launch at Login** via macOS LaunchAgent - **Launch at Login** via macOS LaunchAgent
- **Native macOS app** — no Electron, no web views, just AppKit - **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 ## Changelog
@ -111,27 +112,112 @@ Open **Applications** (or use Spotlight: **Cmd+Space**, type "Pommedoro") and do
Requires macOS 13+ and Swift 5.9+. Requires macOS 13+ and Swift 5.9+.
| 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`) |
For day-to-day development and releasing to users, see **Development & Release** below.
---
## Development & Release
This section describes how to work on Pommedoro locally, how releases and auto-update work, and how to publish a new version.
### Local development
- **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 DMGs SHA and, if it matches the remote, the remotes 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 servers Last-Modified is **newer** than the stored timestamp (so we dont 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 ```bash
make bundle ./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.
To install to `/Applications`: 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**.
```bash **Without `--push`:**
make install `./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.
```
To build the DMG: ### When to run what
```bash | Goal | Command |
make dmg |------|--------|
``` | 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` |
To run directly: ### Troubleshooting
```bash - **“Update available” even though I just pushed / same version**
make run 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 scripts 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.
```
- **Im a dev; I dont 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 ## Requirements

View File

@ -84,3 +84,25 @@ echo ""
echo "==> All checks passed." echo "==> All checks passed."
echo "==> DMG at: .build/Pommedoro.dmg" echo "==> DMG at: .build/Pommedoro.dmg"
echo "==> Release at: releases/Pommedoro.dmg + releases/Pommedoro.dmg.sha256" 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