Upgrade and Rollback¶
There are three escalating change classes. Pick the lightest one that
satisfies your change. Each section below is a literal runbook — copy/paste
the commands as written, substituting <inst> for your systemd instance
(e.g. default) and <NEW_VERSION> for the release tag.
| Class | Operator action | Player-visible impact |
|---|---|---|
| 1. Hot reload (content only) | sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops hot-reload --templates '<glob>' --instance <inst> (via ops UDS — not systemctl reload) |
In-place; sessions kept open |
| 2. Restart (engine code, no schema) | Stage new release (manual until M1.4) + systemctl restart |
≤ 60s disconnect; players reconnect automatically |
| 3. Migration (schema change) | Maintenance window + maid db migrate |
Planned outage; broadcast required |
⚠️ Tooling status — read before copy/pasting. Parts of this runbook depend on automation that is not implemented yet. Verify against your installed version:
install.sh --upgrade <ver>is not available. The installer has no--upgradeflag and rejects it (unknown option). Automated forward-upgrade (release tarball staging + atomic/opt/maid/currentsymlink swap) is deferred to M1.4. Until it lands there is no supported in-place upgrade command — stage the new release with your provisioning process (see bare_metal.md). The rollback flagsinstall.sh --rollback-version --yes(swap back to the previous version) andinstall.sh --rollback(full uninstall) do exist.systemctl reloaddoes NOT do an in-place reload. The unit'sExecReloadsendsSIGHUP, which the engine does not handle — the process terminates and systemd restarts it (Restart=on-failure), the same disconnect as a Class-2 restart. For a true in-place content reload that keeps sessions open, usemaid ops hot-reload(Class 1 below).maid ops maintenance on|offis an M9 stub — it printsNOT YET IMPLEMENTED (M9)and does not gate logins. There is no working login gate today. To notify players during a window, combinemaid ops announce(webhooks) with an in-game admin broadcast (POST /api/v1/admin/broadcast); see player_comms.md.maid ops doctoris an M9 stub. For a health check today usesystemctl status maid-engine@<inst>.service,curl -sf http://localhost:8080/health, and the repo-only doctor scriptscripts/maid-doctor.sh --phase runtime(see bare_metal.md → Step 14). The installer does not stagescripts/under/opt/maid/current(staging is deferred to M1.4), so run the repo scripts (scripts/maid-doctor.sh,scripts/install.sh) from a source checkout. Set this once for the session before running the class runbooks below:export MAID_SRC=/path/to/maid-checkout, then invoke e.g.sudo "${MAID_SRC}/scripts/maid-doctor.sh" --phase runtime --instance <inst>.maid ops announce(webhook-only;--channelsis metadata, not routing),maid ops hot-reload,systemctl restart/stop/start,maid db migrate, anddeploy/scripts/backup.share functional.- The same
MAID_SRCcheckout also provides themaidCLI (there is no system-widemaiduntil M1.4, wheninstall.shwill stage the app). Build its venv once —sudo -u maid-engine -H uv sync --frozen --project "${MAID_SRC}"— and the commands below run it as"${MAID_SRC}/.venv/bin/maid".
1. Hot reload (preferred for content)¶
Use hot reload for content-only edits that do not change persistence schema, command protocol, or engine internals.
Suitable for:
- NPC dialogue prompt/template tweaks
- Item template adjustments (numeric stats, descriptions)
- Room descriptions and atmosphere text
- Quest text and dialogue scripting
NOT suitable for:
- Engine code (anything under
packages/maid-engine/src/) - Persistence schema changes (new columns, new tables)
- Command protocol changes (new args, removed args)
- Configuration shape changes (new sub-settings)
- Anything that adds or removes a content pack
# In-place reload through the ops UDS (gated by maid-ops group membership).
# Reloads templates/packs/systems/modules in the RUNNING engine without
# dropping sessions. Do NOT use `systemctl reload` — that SIGHUPs the
# engine, which is unhandled and causes a full restart (disconnect).
sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops hot-reload --templates 'items/*' --instance <inst>
# ...or reload a whole content pack:
# sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops hot-reload --pack <pack_name> --instance <inst>
# Verify runtime health (maid ops doctor is an M9 stub; use these instead).
curl -sf http://localhost:8080/health && echo OK
sudo "${MAID_SRC}/scripts/maid-doctor.sh" --phase runtime --instance <inst>
If the reload reports FAILED, or the health check fails, escalate
immediately to Class 2 — restart. Do not leave a hot-reload in a
half-applied state.
2. Restart (engine code, no schema)¶
Use restart for engine binary upgrades that do not require a database migration.
# 0. Wire the webhook target into THIS shell's environment (announce reads
# its own process env; it is not the engine's EnvironmentFile).
export MAID_BRIDGES_WEBHOOK_URLS="discord:https://discord.com/api/webhooks/<id>/<token>"
# 1a. Pre-announce to webhooks (≥ 15 minutes). --channels is metadata only.
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity info --eta "$(date -u -d '+15 minutes' --iso-8601=seconds)" \
--message "Scheduled restart for v<NEW_VERSION>; expected outage ~60s."
# 1b. Notify CONNECTED players in-game (announce does not reach them).
# maid ops maintenance/broadcast are M9 stubs; use the admin REST API.
curl -fsS -X POST http://127.0.0.1:8080/api/v1/admin/broadcast \
-H "X-API-Key: ${MAID_ADMIN_API_KEY}" -H 'Content-Type: application/json' \
-d '{"prefix":"[MAINTENANCE]","message":"Restart for v<NEW_VERSION> in ~15 min; auto-reconnect when the server returns."}'
# 2. There is NO working login gate (maid ops maintenance is an M9 stub).
# Proceed directly; the restart itself is the only enforcement.
# 3. Stage the new release.
# PLANNED (M1.4): the automated forward-upgrade command
# `install.sh --upgrade <NEW_VERSION>` (release tarball staging + atomic
# /opt/maid/current symlink swap) is NOT implemented yet — the installer
# rejects `--upgrade`. Until M1.4 lands there is no supported in-place
# upgrade command; stage the new release using your provisioning process
# (see docs/deployment/bare_metal.md) before continuing.
# 4. Restart the service.
sudo systemctl restart maid-engine@<inst>.service
# 5. Verify runtime health (maid ops doctor is an M9 stub).
curl -sf http://localhost:8080/health && echo OK
sudo "${MAID_SRC}/scripts/maid-doctor.sh" --phase runtime --instance <inst>
If verification fails:
- Do not announce "all clear".
- Swap the
currentsymlink back to the previous release:sudo bash "${MAID_SRC}/scripts/install.sh" <inst> --rollback-version --yes. (Do not use bare--rollback— that flag is a destructive full uninstall, not a version swap-back.) sudo systemctl restart maid-engine@<inst>.service.- Re-run the health checks.
- Open a postmortem; do not retry the upgrade until the failure mode is understood.
3. Migration (schema change)¶
Use this path whenever any migration in the release diff touches the database schema. Take a fresh backup first, always. A failed migration is recoverable only if you have a backup taken AFTER the engine stopped writing.
# 0. Wire the webhook target into THIS shell's environment.
export MAID_BRIDGES_WEBHOOK_URLS="discord:https://discord.com/api/webhooks/<id>/<token>"
# 1a. Pre-announce to webhooks (≥ 1 hour for any migration; ≥ 24h if
# irreversible). Valid severities are info|warn|critical (not "warning").
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity warn --eta "$(date -u -d '+1 hour' --iso-8601=seconds)" \
--message "Scheduled maintenance for v<NEW_VERSION>; planned outage ~10m."
# 1b. Notify CONNECTED players in-game via the admin REST API.
curl -fsS -X POST http://127.0.0.1:8080/api/v1/admin/broadcast \
-H "X-API-Key: ${MAID_ADMIN_API_KEY}" -H 'Content-Type: application/json' \
-d '{"prefix":"[MAINTENANCE]","message":"Maintenance for v<NEW_VERSION> in ~1h; planned outage ~10m."}'
# 2. There is NO working login gate (maid ops maintenance is an M9 stub).
# The stop in step 3 is the enforcement.
# 3. Stop the engine (so the backup captures a quiescent state).
sudo systemctl stop maid-engine@<inst>.service
# 4. Take a fresh backup and verify THAT backup, fail-closed. Capture the
# id the backup actually emitted instead of listing the newest one:
# `backup.sh full --json` prints exactly one JSON object on success
# ({"backup_id":...,"result":"success"}); all logs go to stderr. On ANY
# failure `full` exits non-zero and prints no id, so BID stays empty and
# we abort — we must NOT fall back to verifying an older, pre-upgrade
# backup (that would give false confidence). verify needs an explicit
# --backup-id (no --latest/--label); it re-downloads and sha256-checks
# every file and is fail-closed (non-zero exit on any mismatch).
# backup.sh reads MAID_DEPLOY_BACKUP_REMOTE from its OWN process env; a
# bare sudo does NOT load the instance EnvironmentFile, so source it with
# auto-export inside the maid-engine shell (else source_transport aborts:
# "MAID_DEPLOY_BACKUP_REMOTE is not set").
# MAID_SRC must point at a MAID source checkout on the host: backup.sh is
# repo-only until M1.4 (it is NOT staged under /opt/maid/current). Set it
# first — `export MAID_SRC=/path/to/maid-checkout` — and forward it through
# sudo with --preserve-env=MAID_SRC (env_reset would otherwise drop it
# before the inner shell expands the path).
BID="$(sudo -u maid-engine --preserve-env=MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/deploy/scripts/backup.sh" full --instance <inst> --json' \
| jq -r '.backup_id // empty')"
test -n "${BID}" || { echo "backup did not complete — aborting upgrade"; exit 1; }
export BID
sudo -u maid-engine --preserve-env=BID,MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/deploy/scripts/backup.sh" verify --instance <inst> --backup-id "${BID}"' \
|| { echo "verify of ${BID} failed — aborting upgrade"; exit 1; }
# 5. Stage the new release.
# PLANNED (M1.4): `install.sh --upgrade <NEW_VERSION>` is NOT implemented
# yet (the installer rejects `--upgrade`); stage the new release via your
# provisioning process (docs/deployment/bare_metal.md) before continuing.
# 6. Run migrations under the service account. The db CLI has no --instance
# flag; it reads its MAID_DB_* target from the instance env file (the same
# file the systemd unit loads via EnvironmentFile=), so source it first.
# NOTE (M1.1–M1.3): /opt/maid/venv is created empty by install.sh (no
# maid_engine); build a runnable venv in the checkout first with
# `sudo -u maid-engine -H uv sync --frozen --project "${MAID_SRC}"`
# (use the host's uv, e.g. /opt/maid/current/.uv/bin/uv on installer
# hosts), then run the CLI from "${MAID_SRC}/.venv/bin/maid". Forward
# MAID_SRC through sudo because env_reset drops it (see backup.sh above).
sudo -u maid-engine --preserve-env=MAID_SRC bash -c 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" db migrate'
# 7. Restart engine.
sudo systemctl start maid-engine@<inst>.service
# 8. Verify (maid ops doctor is an M9 stub).
curl -sf http://localhost:8080/health && echo OK
sudo "${MAID_SRC}/scripts/maid-doctor.sh" --phase runtime --instance <inst>
# 9. Announce all-clear to webhooks. `maid ops announce` reads
# MAID_BRIDGES_WEBHOOK_URLS from its own process environment, so source
# the instance env with auto-export (a bare --preserve-env forwards
# nothing unless the var was already exported into this shell):
sudo -u maid-engine --preserve-env=MAID_SRC bash -c \
'set -a; . /etc/maid/<inst>.env; set +a;
exec "${MAID_SRC}/.venv/bin/maid" ops announce --instance <inst> --severity info \
--message "Maintenance complete; v<NEW_VERSION> is live."'
If maid db migrate fails, follow docs/runbooks/failed_migration.md.
Rollback matrix¶
| Migration kind | Rollback path |
|---|---|
| Additive (new nullable column or table) | Code rollback only; new column ignored by old binary. |
| Backfill with default | Code rollback; column tolerated by old binary. |
| Rename phase 1 (add new name, dual-write) | Code rollback; both names tolerated. |
| Rename phase 2 (drop old name) | Restore from backup. See docs/runbooks/restore.md. |
| Type change in place | Forbidden. Must be expressed as expand-contract (add new column, dual-write, backfill, drop old). |
Irreversible (irreversible=True) |
Restore from backup. Requires explicit operator confirmation. See migration policy. |
The migration framework refuses to run an irreversible=True migration
unless all of: (a) MAID_MIGRATION_ALLOW_IRREVERSIBLE=1 is set in the
environment, (b) a fresh backup manifest exists with mtime ≤ 1 hour, and
© the migration advisory lock is held. See
docs/contributing/migrations.md for the runtime preconditions and the CI
gate that enforces reviewer sign-off.
Player communications¶
See docs/deployment/player_comms.md for the literal
broadcast cadence (T-24h / T-1h / T-15m), the in-game banner contents, the
Discord webhook payload, and the post-incident summary template.