# Release

`@mac-sync` ships three artefacts: the Mac client, the server, and the web
SPA. They are deployed independently.

## Version source

`VERSION.json` at the repo root is the canonical version. `app.manifest.yaml:5`
duplicates it for the manage-apps CLI. Bump both together.

## Mac client — `MacSyncApp`

Build (release):

```sh
make build-release   # Makefile:63-65 -> swift build -c release --product MacSyncApp
```

Output: `.build/release/MacSyncApp`. The binary is a menu-bar executable; for
distribution wrap it as a `.app` bundle including:

- The binary itself
- A `webapp/` resource directory built from `web/dist/`
  (`LocalWebServer.swift:138-141` looks here first)
- Info.plist with `LSUIElement=true` (menu-bar only)
- Privacy usage strings: `NSCalendarsFullAccessUsageDescription`,
  `NSRemindersFullAccessUsageDescription`, `NSAppleEventsUsageDescription`,
  `NSPhotoLibraryUsageDescription`, plus Full Disk Access via signed entitlement

The user instals through `deploy/install.sh` and a LaunchAgent
(`app.manifest.yaml:20-22`):

```sh
ssh plum 'bash -s' < deploy/install.sh
```

Or via the manage-apps CLI (`app.manifest.yaml:24-30`):

```sh
manage-apps start mac-sync plum    # runs deploy/deploy-remote.sh
manage-apps stop mac-sync plum     # launchctl unload
manage-apps status mac-sync        # ssh plum 'pgrep -x MacSyncApp'
manage-apps logs mac-sync          # tail of ~/Library/Application Support/MacSync/stderr.log
```

## Server — `mac-sync-server`

Build is implicit (Bun runs TypeScript directly). The systemd unit lives in
`deploy/systemd/` and is referenced by `app.manifest.yaml:42-44`.

```sh
manage-apps start mac-sync black     # deploy/deploy-server.sh
manage-apps status mac-sync black    # curl http://localhost:3201/health
manage-apps logs mac-sync black      # journalctl -u mac-sync-server
manage-apps stop mac-sync black      # sudo systemctl stop mac-sync-server
```

Migrations apply automatically on boot
(`src/server/src/app/server.ts:36-52`). To verify a fresh deploy:

```sh
curl http://10.0.0.11:3201/health/deep
# {"ok":true,"checks":{"db":"ok"}}
```

Environment is loaded by `loadConfig()` (`src/server/src/app/config.ts`);
required vars are documented in [dev-setup](./dev-setup.md#boot-the-server).

## Web SPA

Two distribution paths:

1. **Bundled with the Mac client.** Run `cd web && bun run build` before
   `make build-release`; copy `web/dist/` into the `.app` bundle's
   `webapp/` resource directory. Served at `http://localhost:8765/` by
   `LocalWebServer` (`@packages/shared/Sources/MacSyncShared/WebServer/LocalWebServer.swift:94-109`).

2. **Hosted by the server.** The nginx config in `deploy/nginx/` (referenced
   from the deploy scripts) can serve `web/dist/` from the same origin as the
   API.

```sh
cd web
bun install
bun run build           # tsc --noEmit && vite build  (web/package.json:8)
```

Output goes to `web/dist/`. Verify with `bun run preview`.

## Rollback

- Mac client: previous `.app` bundle is preserved alongside the new one on
  `plum`; relaunch via `launchctl load`.
- Server: `sudo systemctl restart mac-sync-server` after reverting the
  source tree. Migrations are forward-only — see Caveats below.
- Web: redeploy the previous `web/dist/`. The bundle is fingerprinted by
  Vite, so cache invalidation is automatic.

## Caveats

- **Migrations are forward-only.** `runMigrations(db, [...])`
  (`src/server/src/app/server.ts:36-52`) appends; there is no `down` script.
  A schema-breaking change requires manual SQL on the database.
- **iMessage `icloud.send_queue` is on a bespoke schema.** Versioning it is
  independent of the generic per-module tables. Don't drop and recreate it on
  a release — outgoing messages will be lost.
- **The Mac client retains `UserDefaults` watermarks across releases.** If a
  release changes the inbound payload shape, bump `SyncManager.syncSchemaVersion`
  (`@packages/imessage/Sources/IMessageSync/SyncManager.swift:47` for iMessage)
  so the bootstrap resets the watermark.
- **Don't commit from a release script.** External tooling owns commits
  (`CLAUDE.md:46`).
