diff --git a/README.md b/README.md index 60c9b1c..2bc4dab 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,94 @@ # stalwart-migrator -In-place upgrade tool for Stalwart Mail Server: 0.15.5 → latest, with no -data loss, checkpointed rollback at every step, and automated post-migration -validation. +In-place upgrade tool for Stalwart Mail Server, 0.15.5 → latest: no data +loss, a checkpoint at every step so a failure can be undone, and automated +validation that the server still works afterwards. -Design status: architected, not yet implemented — see -[ARCHITECTURE.md](ARCHITECTURE.md) for the full design, the phase-by-phase -plan, and the research it's grounded in. +Go, standard library only — no external dependencies. + +## Status + +Partially implemented. Roughly 6,000 lines of tested code across backup, +preflight, checkpointing, validation and recovery; two packages are still +stubs, and that gap is what gates the rest. + +| Command | State | +|---|---| +| `stalwart-migrate preflight` | **Works** — read-only checks and a migration plan | +| `stalwart-migrate run --dry-run` | **Works** — preflight, real backup, sandboxed trial conversion | +| `stalwart-migrate run` | **Refuses on purpose** — see below | +| `stalwart-migrate status ` | **Works** | +| `stalwart-migrate rollback ` | Not implemented | +| `stalwart-migrate confirm ` | Not implemented | +| `stalwart-migrate report ` | Not implemented | + +**`run` without `--dry-run` deliberately refuses to proceed.** A real cutover +needs `internal/rollback` — currently a `doc.go` and nothing else — plus real +systemd/Docker service control. Committing to a migration with no working +rollback would break the single guarantee the tool exists to make, so it +stops rather than going partway. That refusal is the correct behaviour today, +not a bug. + +Package state: + +| Package | Lines | Tests | +|---|---|---| +| `internal/backup` | 1805 | yes | +| `internal/preflight` | 1324 | yes | +| `internal/validate` | 792 | yes | +| `internal/stalwartapi` | 716 | yes | +| `internal/recovery` | 702 | yes | +| `internal/checkpoint` | 559 | yes | +| `internal/plan` | 196 | yes | +| `internal/rollback` | stub | — | +| `internal/config` | stub | — | + +## Why not a shell script + +Stalwart's 0.15 → 0.16 boundary is not a drop-in binary swap: settings move, +and the data directory has to be migrated rather than merely copied. The +failure mode that matters is a half-migrated mail store with no way back — +which is why backup verification, checkpointing, and rollback are the design +centre rather than conveniences bolted on afterwards. + +[`ARCHITECTURE.md`](ARCHITECTURE.md) covers this in full: §1 on why a thin +wrapper is insufficient, §4 on the migration phases, §5 on the checkpoint +state machine, §6 on the CLI surface, and §8 on what is still open. + +## Build and test + +```sh +go build ./... +go test ./... +``` + +Requires Go 1.26 or newer. + +## Trying it safely + +`preflight` is the sensible starting point — its checks against the Stalwart +installation are read-only: + +```sh +sudo go run ./cmd/stalwart-migrate preflight +``` + +It still needs write access, because it records the run as a checkpoint +before doing anything else: ``` -stalwart-migrate preflight # read-only checks + plan -stalwart-migrate run # execute the migration -stalwart-migrate status -stalwart-migrate rollback -stalwart-migrate confirm -stalwart-migrate report +create run: checkpoint: create run directory: +mkdir /var/lib/stalwart-migrator: permission denied ``` + +That path is `checkpoint.DefaultBaseDir`, a compile-time constant with no +flag or environment override — so preflight needs either root or a +pre-created writable `/var/lib/stalwart-migrator`. (`run` takes `--work-dir` +for its scratch space, but that is a different directory and does not move +the checkpoint store.) + +`run --dry-run` performs a **real backup**, which touches the live data +directory — read the caveat the command prints before using it on anything +you care about. Where the plan crosses the 0.15/0.16 boundary it clones that +verified backup into a disposable sandbox and converts the copy, leaving the +original untouched.