feat: unified outpost migrate CLI (#675)#816
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add ListMigrations, LatestVersion, and PendingCount methods on the SQL Migrator so a future unified migration CLI can enumerate available migrations and compute pending counts without going through the golang-migrate driver. Stores the raw embedded filesystem and subdirectory on the Migrator at construction time, parses NNNNNN_name.up.sql filenames, and correlates the result with the current schema version. Pure additive change — no existing callers are affected. Refs #675 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ac9a3b6 to
d8d557f
Compare
Introduce internal/migrator/coordinator, a thin orchestration layer over the existing SQL migrator and the migratorredis.Migration interface that exposes a single API for listing, planning, applying, and verifying migrations across both subsystems. The coordinator is designed to be driven from both a CLI (next phase) and from app startup checks (phase after that). Redis state tracking uses the same hash key layout and lock key as migratorredis.Runner and cmd/outpost-migrate-redis so existing state carries over seamlessly. Covered by miniredis-backed unit tests for the Redis path. SQL paths delegate to the introspection helpers added in the previous commit. Refs #675 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop delegating 'outpost migrate' to the outpost-migrate-redis binary and implement a real subcommand tree backed by the unified coordinator: outpost migrate list — list all SQL + Redis migrations outpost migrate plan — show what would be applied outpost migrate apply — apply pending migrations (SQL then Redis) outpost migrate verify — verify applied migrations outpost migrate unlock — clear a stuck Redis migration lock Each subcommand constructs a coordinator from the usual Outpost config and delegates the actual work. Output rendering lives in a separate file so the CLI layer stays thin. This phase does not yet change startup behaviour — auto-migration in app.PreRun() still runs. The old outpost-migrate-redis and migratesql binaries also keep working and will be deprecated in a later commit. Refs #675 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove auto-migration from the startup sequence and replace it with a unified pending-migration check backed by the migration coordinator. Before: runMigration() applied SQL migrations and runRedisMigrations() applied auto-runnable Redis migrations as side effects of app.PreRun(). Operators had no visibility into migration timing and non-auto-runnable Redis migrations would fail startup with a cryptic error. After: PreRun connects to Redis, then asks the coordinator whether any SQL or Redis migrations are pending. If any are, startup fails fast with a clear message telling the operator to run 'outpost migrate apply' first. This is the breaking change flagged in issue #675. Deployment workflows that previously relied on auto-migration now need an explicit 'outpost migrate apply' step before 'outpost serve'. Deletes the obsolete lock-retry logic and its test — retries only mattered for concurrent auto-runs at startup, which no longer exist. Refs #675 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete cmd/migratesql and cmd/outpost-migrate-redis along with every reference in build, Makefile, docs, and container entrypoints. The unified 'outpost migrate' command and the startup gate added in the previous commits cover every workflow these binaries served: migratesql up -> outpost migrate apply outpost-migrate-redis list -> outpost migrate list outpost-migrate-redis plan -> outpost migrate plan outpost-migrate-redis apply -> outpost migrate apply outpost-migrate-redis verify -> outpost migrate verify outpost-migrate-redis unlock -> outpost migrate unlock The production container entrypoint (build/entrypoint.sh) no longer needs the explicit 'migrate init --current' preflight — the server itself refuses to start when migrations are pending, so a single 'exec outpost serve' gives the same behaviour with a clearer error. The dev entrypoint (build/dev/entrypoint.sh) drops the same preflight and just execs 'air serve'. Operators run 'make migrate' from the host to apply pending migrations before starting the container. goreleaser, Dockerfile.example, and Dockerfile.goreleaser are updated to stop building and shipping the removed binaries. Refs #675 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d8d557f to
303c7ad
Compare
Collaborator
Author
Manual QA: v0.15.0 → current branch upgradeWalked through the realistic upgrade path an operator will take to go from a released Outpost version to the unified migration CLI. Scenario
Verified
|
Contributor
|
Very good 👍 |
alexbouchardd
approved these changes
Apr 10, 2026
alexluong
added a commit
that referenced
this pull request
Apr 13, 2026
The unified migration CLI (#816) added a startup gate that refuses to start when migrations are pending. E2e tests create fresh databases but never applied migrations, causing all tests to fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 task
alexluong
added a commit
that referenced
this pull request
Apr 13, 2026
The unified migration CLI (#816) added a startup gate that refuses to start when migrations are pending. E2e tests create fresh databases but never applied migrations, causing all tests to fail. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
alexluong
added a commit
that referenced
this pull request
Apr 17, 2026
PR #816 added cmd/outpost/migrate.go but air.toml still built only cmd/outpost/main.go, causing undefined symbol errors in dev. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the unified migration experience described in #675. Replaces the two separate migration systems (SQL auto-run at startup +
outpost-migrate-redisstandalone CLI) with a singleoutpost migratecommand and an explicit startup gate that refuses to boot when migrations are pending.What ships:
internal/migrator/coordinatorpackage — wraps both the SQL migrator and the Redis migration runner behind a single API (List,Plan,Apply,Verify,PendingSummary,Unlock). Redis state tracking reuses the same hash-key layout asmigratorredis.Runner, so existing Redis state carries over seamlessly.outpost migratesubcommand tree — reallist/plan/apply/verify/unlockcommands backed by the coordinator. Replaces the old passthrough that delegated to the standalone Redis CLI.app.PreRun()no longer auto-applies migrations. Instead it calls the coordinator and refuses to start if anything is pending, with a clear error telling the operator exactly what to run.cmd/migratesqlandcmd/outpost-migrate-redisare deleted along with every reference in the Makefile, goreleaser, Dockerfiles, and container entrypoints. The unified CLI plus the startup gate cover everything they did.Breaking change
Deployment workflows that relied on auto-migration at startup need to move to an explicit two-step flow:
Release notes should flag this and add it to the upgrade guide.
Expected UX — what reviewers should see when testing this locally
The flow below is what an operator gets when starting from a v0.15.0-seeded deployment and upgrading to this branch. Every snippet is real output from a local run.
1. Help output
2. Startup gate fires when anything is pending
Running the server directly without applying pending migrations:
3.
outpost migrate list— unified view of both subsystemsRows the coordinator knows aren't applicable (e.g. a Redis migration whose
IsApplicable()returns false under the current config) render as[not_applicable]with a reason solistand the startup gate never disagree.4.
outpost migrate plan— whatapplywould changeWhen Redis does have pending work,
plancalls each migration's ownPlan()and reports per-migration scope (tenants_need_migration,destinations_need_migration,total_need_migration).5.
outpost migrate apply --yes— applies SQL then Redisapplyalways runs SQL first (schema must be ready before Redis data transformations) and then iterates Redis migrations in version order. Each migration goes through the standard plan → lock → apply → mark-applied loop the old Redis CLI used, so it's a drop-in replacement for the oldoutpost-migrate-redis apply --all --yesflow.Flags:
--yes(skip confirmation),--sql-only,--redis-only.6.
outpost migrate verify— confirms post-apply state7. Server starts cleanly after apply
And the API is fully functional — tenants / destinations / attempts created under the old version are still readable (including
response_datathat survived the JSONB → TEXT migration in 000009), and freshly published events deliver end-to-end.8.
outpost migrate unlockInteractive by default; prompts before clearing the Redis migration lock.
--yesskips the prompt for scripted recovery from a dead migration process.What happened to
initandcleanup?The old
outpost-migrate-redisbinary had two subcommands that are not in the unified CLI. Here's why that's intentional:init— split into two separate concerns:init --current(preflight check) was used by the production container entrypoint to fail fast before starting the server if migrations were pending. This is now handled by the startup gate inapp.PreRun()— the server itself refuses to start and prints a clear error.build/entrypoint.shwas simplified to justexec outpost serve.init— detecting an empty Redis and marking all migrations as applied without running them so a brand-new deployment doesn't waste time scanning empty keys) is not implemented in the coordinator. It's not a correctness issue: on a fresh Redis the coordinator iterates each migration and calls itsPlan()/Apply(), all of which scan for records needing work, find none, and return. Slightly wasteful on first boot of a brand-new cluster; easy to add later as a fast-path.cleanup— niche use case, deferred:Of the three current Redis migrations, only
001_hash_tagsleaves legacy keys behind after running (it renamestenant:*→tenant:{id}:*). The other two transform values in place, so there's nothing to delete. And001_hash_tagsonly has work to do on deployments that don't setDEPLOYMENT_ID, which is a shrinking set.So
cleanupwas mostly dead code in the old CLI too — a safety net for one specific upgrade path. If a future migration genuinely needs post-apply key deletion, it's a small, self-contained addition to the coordinator and the CLI.Test plan
go build ./...passesgo test -short ./...passes, including new unit tests for the SQL introspection helpers and miniredis-backed coordinator testsoutpost migrate --helpshows the new subcommand treeoutpost servefails cleanly when migrations are pending and starts cleanly afteroutpost migrate applyOpen questions for reviewers
OUTPOST_STRICT_MIGRATIONS) for one release to give operators an escape hatch before becoming the default?outpost migrate applyconcurrently from multiple nodes the second will fail fast. Acceptable?🤖 Generated with Claude Code