Skip to content

feat: safe database upgrades from any release (PRD-016 Phase 0) - #29

Open
khalid-offset wants to merge 2 commits into
mainfrom
feat/upgrade-safety
Open

khalid-offset wants to merge 2 commits into
mainfrom
feat/upgrade-safety

Conversation

@khalid-offset

Copy link
Copy Markdown
Contributor

Phase 0 of PRD-016: make database upgrades provably safe, from any earlier release, however many versions were skipped. The server still upgrades its database at launch; this adds the guard rails and the operator commands around it.

What changes for users

  • One server per database. serve holds <db>.lock for its life; a second server on the same file is refused with the holder's pid and version. Other commands take the lock only when they would upgrade, so db backup still works while a server runs.
  • Upgrade at start: free-space check, backup, migrations, then verification (quick_check, no new foreign-key violations). If a step or the check fails, the pre-upgrade backup is put back, one failed copy is kept for diagnosis, and the server refuses to start.
  • Upgrade history: upgrade_log table (migration 16).
  • New commands: db status, db check (exit 2 on a problem), db migrate [--dry-run], db restore BACKUP (the replaced database is kept). serve --migrate auto|refuse for change-controlled sites.

How skipped versions are proven

  • internal/store/migrations.lock: steps 1–14 hashed from the v0.1.0 tag, 15 from v0.4.0. TestMigrationsLock refuses an edited, reordered or removed step; prepare-release.sh locks a release's steps.
  • internal/store/testdata/upgrade/v0.{1,2,3,4}.0.db: written by the real released binaries via testing/upgrade/make-fixture.sh (TLS, PQ cert, SSH, STARTTLS, dead port, two scans each, triage, link, host inventory, admin account). TestUpgradeFromEveryRelease checks every table keeps its rows, evidence, accounts and triage are byte-identical, and the result matches a fresh install's structure. Mutation-checked: a step that deletes data fails it.
  • Compiled-in schema fingerprint + readable testdata/schema.txt (regen with -update-schema).
  • New upgrade CI job: the latest release (built from its tag) refuses a database this commit upgraded, and reads it again after db restore.

Latent bugs fixed

  • Migration 10 iterated the shared timestampColumns map: adding a later table to it would break upgrades from before step 10. Now timestampColumnsV10.
  • Migration 6 called the live recordManualLinkTx (uses internal/estate). Now recordConfirmedLinkV6, verified to write identical rows.
  • TestPostMigrationsUseOnlyFrozenCode stops this pattern returning.

Notes for review

  • Structure mismatch after an upgrade warns, does not refuse (pre-0.1.0 databases could otherwise be locked out); integrity/FK failures do restore.
  • New platform layer internal/osfile (flock / LockFileEx, free space): archtest only lets platform layers import x/sys/windows.
  • Fixtures add ~3 MB of SQLite test data (.gitignore exception); test passwords and loopback scans only.
  • A database upgraded by this branch is refused by 0.4.0 (schema 16); changelog Upgrade notes explain db restore.

Testing

  • Windows: go test ./..., golangci-lint run ./... clean; Linux/macOS: build, vet and lint clean locally, tests run in CI.
  • CI upgrade job rehearsed locally (old binary refuses, restore, old binary reads).

The server already upgrades its database at launch; this makes that provably safe and gives operators
the tools around it.

- Lock released migrations (internal/store/migrations.lock, built from the v0.1.0 and v0.4.0 sources);
  TestMigrationsLock refuses an edited, reordered or removed step.
- Databases written by the real v0.1.0, v0.2.0, v0.3.0 and v0.4.0 binaries (testing/upgrade/make-fixture.sh)
  and TestUpgradeFromEveryRelease: every table keeps its rows, evidence, accounts and triage are unchanged,
  and the result matches a fresh install's structure.
- Upgrade flow: database lock (one server per file, no upgrade under a running process), free-space check,
  backup, steps, verification; a failed step or check restores the pre-upgrade backup and refuses to start.
  Each upgrade is recorded in upgrade_log (migration 16).
- Schema fingerprint compiled in, with a readable dump (testdata/schema.txt) for review.
- db status, db check, db migrate [--dry-run], db restore; serve --migrate auto|refuse.
- Migration Go steps may use only frozen code (TestPostMigrationsUseOnlyFrozenCode); migration 6 and 10
  now use frozen copies (recordConfirmedLinkV6, timestampColumnsV10), with identical output.
- New platform layer internal/osfile (file lock, free space) so store stays OS-free.
- CI: an upgrade job checks the latest release refuses, then after db restore reads, a database this
  commit upgraded. Unreleased migrations need "### Upgrade notes" in the changelog.
- prepare-release.sh locks a release's migrations; RELEASING, CONTRIBUTING, INSTALL and deploy docs updated.
Comment thread testing/upgrade/seed.mjs Fixed
CodeQL js/file-access-to-http flagged seed.mjs: it read the sample host inventory
and sent it over HTTP. That was only ever to the throwaway local server
make-fixture.sh starts, but nothing enforced it. seed.mjs now refuses any --base
that is not http:// on loopback, and the inventory is uploaded by make-fixture.sh
(curl to the same local server), so the script reads no files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants