Record that this migrated a production server, and what it took

On 2026-08-25 the tool migrated a live mail server - nine domains, six
accounts, a 2.4 GB RocksDB store - from 0.15.5 to 0.16.19 with 8 seconds of
downtime and every phase green, validation included.

The practice worth copying is the dress rehearsal on a clone that preceded
it. `rehearse` is read-only and stops before applying, which is where a real
migration fails; a clone closes that gap for about six seconds of production
downtime. It caught four faults that would each have reached production, and
three of them only appear against a real instance. The README now describes
how to build one, including the parts that are easy to get wrong: a
consistent copy of a single-writer store, and a guest with no route out so a
copy of a live mail server cannot renew real certificates or deliver real
queued mail.

It also documents what Stalwart's own converter drops without saying so.
migrate_v016.py consumes every acme.* setting, emits nothing, and does not
report them as unmigrated - so a migration reports success while removing
certificate renewal, and nothing looks wrong until the certificate expires
about ninety days later. The AcmeProvider that had to be rebuilt by hand is
recorded in ARCHITECTURE.md, shape confirmed against a live 0.16.19, as the
specification for generating it in the supplemental plan.
This commit is contained in:
2026-08-24 17:15:18 -07:00
parent 019696bce7
commit 3342662b33
2 changed files with 75 additions and 2 deletions
+16
View File
@@ -323,6 +323,22 @@ schema-migration flag is set for the target version, and the plan skips
is intentionally the same engine with a shorter plan, not a separate
code path, so it doesn't rot independently.
### 4.6a What the converter drops without saying so
`migrate_v016.py` consumes every `acme.*` setting and emits nothing for it,
and does not list those keys as unmigrated either. The effect is a migration
that reports complete success while quietly removing certificate renewal: the
certificate itself carries over, so nothing looks wrong until it expires
about ninety days later. Observed on a production migration, 2026-08-25.
The supplemental plan already generates what the converter leaves behind for
listeners (§4.6). An `AcmeProvider` generator belongs alongside it. The
object shape is known-good, having been applied to a live 0.16.19 server:
`challengeType` and `renewBefore` are enums (`TlsAlpn01`, `R23`), `contact`
is a value-keyed set rather than a list, and `accountKey`/`accountUri` are
server-set and must be omitted — the server registers a fresh ACME account,
since the v0.15 account key cannot be carried across.
### 4.7 Post-migration validation
**What this suite can assert depends on the boundary being crossed, and on