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

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 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

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:

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.

S
Description
Imported from github.com during the 2026-09-20 standup (local dir: stalwart-migrator)
Readme GPL-3.0
547 KiB
v2026.9.15
Latest
2026-09-21 05:10:39 +00:00
Languages
Go 99.8%
Shell 0.2%