47 lines
3.1 KiB
Markdown
47 lines
3.1 KiB
Markdown
# Phase 9 migration and rollback runbook
|
|
|
|
This workflow is local and operator-driven. It adds no HTTP endpoint and does not start, stop, or reconfigure a service.
|
|
|
|
## Safety rules
|
|
|
|
- Stop the API before restore activation. Managed open database connections cause activation to fail closed.
|
|
- Use only an explicitly selected legacy file. The command never searches for or reads a default/root `config.json`.
|
|
- Preview output contains only `credentialPresent`; legacy password values are neither printed nor persisted. Instances with passwords remain disabled for password authentication (`auth_mode=none`) until the separate secret activation workflow succeeds.
|
|
- Keep snapshots and candidates on trusted local storage. Files are created mode `0600`.
|
|
|
|
## Preview and confirm legacy import
|
|
|
|
```sh
|
|
corepack pnpm --filter @multi-simadmin/api migration -- \
|
|
preview --source /explicit/path/legacy.json --database /local/data/app.sqlite
|
|
```
|
|
|
|
Review every planned instance and ensure `conflict` is zero. Copy the printed `planDigest`, then confirm exactly that plan:
|
|
|
|
```sh
|
|
corepack pnpm --filter @multi-simadmin/api migration -- \
|
|
confirm --source /explicit/path/legacy.json --database /local/data/app.sqlite \
|
|
--plan-digest <64-hex-plan-digest>
|
|
```
|
|
|
|
Confirmation safely re-reads the file and database state. A changed file identity, changed source bytes, changed plan, or database conflict aborts without partial import. Repeating preview/confirm is idempotent. Each confirmation writes a `release.legacy-import.*` reconciliation record in `app_settings`, including only digests, counts, and pending-secret status.
|
|
|
|
## Snapshot and restore foundation
|
|
|
|
Use `backupDatabase(openManagedDatabase, snapshotPath)` while the database is open. It uses SQLite's online backup API, so committed WAL pages are included in one consistent snapshot. The temporary snapshot is integrity checked, synced, and atomically renamed.
|
|
|
|
Restore is intentionally two-stage:
|
|
|
|
1. `prepareRestoreCandidate(snapshotPath, candidatePath)` copies to a **separate** candidate, verifies SQLite integrity, syncs it, and returns its SHA-256 digest.
|
|
2. Inspect/drill the candidate and explicitly retain that digest.
|
|
3. Close all managed live connections.
|
|
4. `activateRestoreCandidate(livePath, candidateArtifact, confirmedDigest)` rechecks candidate bytes and requires the exact digest before atomic replacement. Stale WAL/SHM sidecars are removed only after replacement.
|
|
|
|
Never activate directly from an unverified snapshot.
|
|
|
|
## Deterministic rollback drill
|
|
|
|
Run `runRollbackDrill(snapshotPath, freshCandidatePath)`. It never activates the candidate. The returned record contains snapshot digest, integrity status, deterministic schema digest, and per-table row counts—no row values or secrets. Repeating against the same snapshot must produce the same schema digest and counts.
|
|
|
|
After an actual rollback, reconcile operationally by comparing the drill record and expected release digest, then record the operator decision in the deployment/release system. Secret material is outside SQLite and must be reconciled separately through `SecretStore`; do not copy secrets into logs or records.
|