Files
stalwart-migrator/README.md
T
jcoffey-dev 56f465d8a9 docs: rewrite the README around what actually works
The old README was wrong in both directions. It called the project
"architected, not yet implemented" while ~6,000 lines of tested code
exist across backup, preflight, checkpointing, validation and recovery;
and it listed six subcommands as a flat menu when three of them return
"not implemented yet" and a fourth refuses unless given --dry-run.

Replaced with a per-command and per-package status table, each entry
checked against the binary rather than the design doc: rollback, confirm
and report were run to confirm they error out, and the package line
counts come from the tree.

Records why `run` refuses without --dry-run -- internal/rollback is a
doc.go and nothing else, and committing to a migration with no working
rollback would break the one guarantee the tool exists to make. That
refusal is correct behaviour today, not a defect to be filed.

Corrects a claim in my own first draft: preflight is not purely
read-only. Its checks against the Stalwart install are, but it records
the run as a checkpoint first, so it fails without write access to
/var/lib/stalwart-migrator -- a compile-time constant with no flag or
env override. Found by running it.
2026-08-22 21:59:57 -07:00

95 lines
3.4 KiB
Markdown

# stalwart-migrator
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.
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 <id>` | **Works** |
| `stalwart-migrate rollback <id>` | Not implemented |
| `stalwart-migrate confirm <id>` | Not implemented |
| `stalwart-migrate report <id>` | 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:
```
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.