You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Alembic migrations have been quietly accumulating placeholder-style revision IDs (a1b2c3d4e5f6, w7x8y9z0a1b2, aa1b2c3d4e5f, โฆ) instead of alembic revision's actual random-hex output (bb43712cae28, 43c07ed25a24, 191a2def08d7, โฆ). The repo has paid for this with repeated rebase/merge conflicts (most recently in #3202, also in #4429 / #4548 / #4555 / #2154 / #4138). We have an existing pre-commit hook and CI harness that checks format but neither rejects placeholder content nor verifies the full chain is linear.
This epic groups the existing-but-unmerged hygiene work under one umbrella, adds the missing guardrails, and re-hashes the 36 affected revisions in a single sweep so this stops happening.
This is an epic / tracking issue. Each bullet in "Children" gets its own PR.
๐งฑ Area Affected
GitHub Actions / CI Pipelines (.github/workflows/alembic-upgrade-validation.yml, .github/workflows/pre-commit.yml)
Build system (Makefiledb-*, migration-test-*, upgrade-validate targets)
Docs (tests/migration/README.md, ADR for migration hash policy)
โ๏ธ Context / Why now
Three concrete symptoms motivate this epic:
1. 36 of 96 migrations on main use placeholder-style revision IDs. The pattern is "letter + digit alternating" ([a-z]\d[a-z]\dโฆ) โ 12 alphanumeric chars that pass the existing pre-commit regex but are clearly hand-picked, not alembic revision-generated.
2. Stacks of pre-allocated prefixes. Two long manual chains exist:
When two PRs each pre-allocate the next letter and merge close in time, both pick the same letter and we get multiple-head merge migrations (bb43712cae28_merge_token_blocklist_with_audit_identity_heads.py is the most recent evidence).
3. Header drift. Six migrations have Revises: docstrings that disagree with the actual down_revision value in code:
These are silent bugs โ alembic uses the code value, but anyone reading the docstring (humans, AI, future maintainers debugging chain shape) gets misled.
4. The CI gap that keeps letting this in. Per audit of current state:
scripts/pre-commit/check_migration_patterns.py validates filename hash format and revision/filename match, plus duplicate revision IDs. It does not reject placeholder-style content.
scripts/ci/run_upgrade_validation.sh enforces "exactly one alembic head". It does not verify the chain is linear (i.e., every revision has exactly one parent except the merge points, and merges are intentional).
.pre-commit-lite.yaml โ the config used by make pre-commit in CI โ does not include the check-migration-patterns hook at all. So the local hook runs only for developers with pre-commit install'd, not in CI.
๐ Proposed Architecture
Guardrail layer (do this first; prevents new placeholder hashes from landing)
Extend check_migration_patterns.py to reject the placeholder pattern. Concrete regex (rejects "alternating letter+digit, โฅ11 chars"):
importre_PLACEHOLDER_RE=re.compile(r"^(?:[a-z]\d){5,}[a-z]?$")
if_PLACEHOLDER_RE.match(revision_id):
raiseSystemExit(f"{path}: revision '{revision_id}' looks placeholder-allocated. Run `alembic revision -m '...'` to get a real hash.")
Also reject revision IDs that contain 0a1b, 1b2c, 2c3d, โฆ, 8h9i, 9i0j substrings (the diagonal chain marker).
Add a down_revision <-> Revises: docstring consistency check to the same hook. Catches the 6 mismatches above and prevents new ones.
Move check-migration-patterns into .pre-commit-lite.yaml so CI's pre-commit job actually runs it. Currently the hook only protects developers running pre-commit install.
Chain-shape verification (do this in CI, not pre-commit; needs the full graph)
Add --check-linear to scripts/ci/run_upgrade_validation.sh (or a sibling script). Walks the alembic graph, asserts:
Exactly one head (already covered).
Every non-merge revision has exactly one down_revision.
Every merge revision is named *_merge_* AND is documented in a CHANGELOG/ADR.
No "orphan" revisions (must be reachable from base to head).
Hook the new check into alembic-upgrade-validation.yml so PRs get blocked on broken chains, not just multi-head.
Cleanup (do last; needs the guardrails first or it'll regress)
Re-hash the 36 placeholder revisions in a single PR per logical batch. Use python -c "import secrets; print(secrets.token_hex(6))" to generate replacements. For each replaced revision, update:
The filename
The revision = "..." line
The Revises: docstring of the next migration
The down_revision = "..." of the next migration
Any merge migrations that reference the old hash
Fix the 6 docstring/code mismatches during the rehash pass.
Add an ADR (docs/docs/architecture/alembic-migration-hash-policy.md) recording the policy: "All revision IDs must come from alembic revision -m '...'. Manual or pre-allocated IDs are not permitted. Merge migrations must be named *_merge_* and explained in commit/ADR."
๐ง๐ปโ๐ป User Story 1 โ Developer
As a: developer adding a new migration I want:pre-commit to fail loudly if I hand-edit a revision ID into something cute So that: I never get into a "two PRs picked m7g8โฆ simultaneously" merge conflict again
Scenario: Placeholder pattern is rejected at commit timeGiven a new file `mcpgateway/alembic/versions/m7g8h9i0j1k2_add_my_thing.py`
When I run `pre-commit run check-migration-patterns --files <file>`
Then the hook fails with "revision 'm7g8h9i0j1k2' looks placeholder-allocated"Scenario: Real alembic-generated hashes passGiven a new file with revision = "bb43712cae28"When I run the hook
Then it passes
๐ง๐ปโ๐ป User Story 2 โ Reviewer / CI
As a: PR reviewer / CI I want: the upgrade-validation workflow to refuse a chain that's secretly branched So that: "exactly one head" is no longer the only graph property we enforce
Scenario: Linear-chain verification catches a stealth branchGiven a migration tree where rev_X and rev_Y both list rev_Z as down_revision but no merge migration unifies them
When alembic-upgrade-validation.yml runs
Then it fails with "non-merge revisions sharing parent rev_Z: rev_X, rev_Y"
๐ง๐ปโ๐ป User Story 3 โ Cleanup
As a: maintainer I want: the existing 36 placeholder revisions re-hashed and the 6 docstring mismatches fixed So that: the codebase stops carrying historical hash debt forward
Scenario: Placeholder rehash PRGiven the cleanup PR replaces every placeholder revision ID with a real random hex hash
And updates every downstream `down_revision` and `Revises:` reference
When `make migration-test-all` runs
Then `alembic upgrade head` succeeds on a fresh DB
And `alembic downgrade base` succeeds from head
And `alembic heads` returns exactly one head
And the new placeholder-pattern check passes
๐ Children (track via this epic)
Already-existing related issues that fit under this umbrella:
New work items to file as children of this epic (after this epic is accepted):
A11. Extend check_migration_patterns.py with placeholder-pattern rejection + docstring-vs-code consistency check
A12. Add check-migration-patterns to .pre-commit-lite.yaml so CI runs it
A13. Add chain-linearity validation to scripts/ci/run_upgrade_validation.sh (or a sibling script) and wire into .github/workflows/alembic-upgrade-validation.yml
A14. Re-hash the 36 placeholder revisions in a sequence of small batched PRs (one per logical chain to keep diffs reviewable)
A15. Fix the 6 Revises: docstring mismatches during the rehash
A16. ADR: document the alembic hash policy in docs/docs/architecture/
๐ฆ Affected migrations (reference data for cleanup PRs)
.pre-commit-lite.yaml includes the migration hook so CI enforces it
CI verifies chain linearity, not just single-head
All 36 placeholder revisions are replaced with real hex hashes
All 6 docstring mismatches are fixed
b77ca9d2de7e header is normalized
ADR documenting the policy is merged
All listed children issues are either closed or explicitly de-scoped from this epic with reasoning
๐ฆ Related Make Targets
make pre-commit โ currently uses .pre-commit-lite.yaml, needs to include the migration hook
make upgrade-validate โ runs scripts/ci/run_upgrade_validation.sh, target for linearity check
make migration-test-all โ sanity gate after rehash PRs land
make db-heads โ already shows count, will be the cleanest single-head reporter post-cleanup
๐ Notes
This epic is not a security issue; it's hygiene that prevents future merge-train collisions and silent bugs.
The rehash work is inherently rebase-y. Recommend doing it during a quiet window and pinning a specific main commit so contributors with in-flight migrations can reparent cleanly.
Audit data above is reproducible from the mcpgateway/alembic/versions/ tree at the time of filing; happy to drop the exact python -c invocations into a comment if reviewers want to verify.
๐ง Epic Summary
Alembic migrations have been quietly accumulating placeholder-style revision IDs (
a1b2c3d4e5f6,w7x8y9z0a1b2,aa1b2c3d4e5f, โฆ) instead ofalembic revision's actual random-hex output (bb43712cae28,43c07ed25a24,191a2def08d7, โฆ). The repo has paid for this with repeated rebase/merge conflicts (most recently in #3202, also in #4429 / #4548 / #4555 / #2154 / #4138). We have an existing pre-commit hook and CI harness that checks format but neither rejects placeholder content nor verifies the full chain is linear.This epic groups the existing-but-unmerged hygiene work under one umbrella, adds the missing guardrails, and re-hashes the 36 affected revisions in a single sweep so this stops happening.
This is an epic / tracking issue. Each bullet in "Children" gets its own PR.
๐งฑ Area Affected
.github/workflows/alembic-upgrade-validation.yml,.github/workflows/pre-commit.yml).pre-commit-config.yaml,.pre-commit-lite.yaml,scripts/pre-commit/check_migration_patterns.py)mcpgateway/alembic/versions/*)Makefiledb-*,migration-test-*,upgrade-validatetargets)tests/migration/README.md, ADR for migration hash policy)โ๏ธ Context / Why now
Three concrete symptoms motivate this epic:
1. 36 of 96 migrations on
mainuse placeholder-style revision IDs. The pattern is "letter + digit alternating" ([a-z]\d[a-z]\dโฆ) โ 12 alphanumeric chars that pass the existing pre-commit regex but are clearly hand-picked, notalembic revision-generated.2. Stacks of pre-allocated prefixes. Two long manual chains exist:
k5e6โฆ โ l6f7โฆ โ m7g8โฆ โ n8h9โฆ โ o9i0โฆ โ p0a1โฆ โ q1b2โฆ โ r2b3โฆ โ s3c4โฆ(9-prefix sequence across PRs Correlation ID for Unified Request Trackingย #1443, Move LLM settings to UI instead of ENVย #1597, Performance tab and locustย #1637, Database Indexing Optimization (Issue #1353)ย #1650, Cache global configย #1717, Metrics aggregationย #1736, Metrics rollup clย #1782, [BUG]: Prompt Namespacing + Name/ID Resolution (Tool-Parity) #1762ย #1876, 1892 get member count n plus oneย #1905)t4d5โฆ โ u5f6โฆ โ v1a2โฆ โ w6g7โฆ โ x7h8โฆ โ y8i9โฆ โ z1a2โฆ(7-prefix sequence across PRs feature/password-enforcement-1843ย #2066, feat: Add EntraID role mapping for SSO authenticationย #2129, Enforce Default RBAC Role Assignment and Migration for Existing Usersย #2699, feat(auth): implement password reset recovery and lockout fixesย #2951, fix(auth): add jwks_uri column to SSOProvider and harden create_providerย #3026, fix: batch 6 hardening for oauth secrets, masking, auth timing, and outbound URL validation (h-batch-6)ย #3115, 1282 force to user to change passwordย #1494)When two PRs each pre-allocate the next letter and merge close in time, both pick the same letter and we get multiple-head merge migrations (
bb43712cae28_merge_token_blocklist_with_audit_identity_heads.pyis the most recent evidence).3. Header drift. Six migrations have
Revises:docstrings that disagree with the actualdown_revisionvalue in code:20a0e0538ac5_add_composite_indexes_for_metrics_time_.py64acf94cb7f2abf8ac3b6008356a2d4eed6f_uuid_change_for_prompt_and_resources.pyz1a2b3c4d5e69e028ecf59c4add_toolops_test_cases_table.pyadd_oauth_tokens_tablez1a2b3c4d5e6ba202ac1665f_migrate_user_roles_to_configurable_.pya31c6ffc2239f9a8b7c6d5e4bb43712cae28_merge_token_blocklist_with_audit_identity_heads.pycae28b15a507, b2c3d4e5f6g7b2c3d4e5f6g7r2b3c4d5e6f7_add_prompt_namespacing_fields.pyk5e6f7g8h9i0, 4f07c116f917, z1a2b3c4d5e64f07c116f917These are silent bugs โ alembic uses the code value, but anyone reading the docstring (humans, AI, future maintainers debugging chain shape) gets misled.
4. The CI gap that keeps letting this in. Per audit of current state:
scripts/pre-commit/check_migration_patterns.pyvalidates filename hash format and revision/filename match, plus duplicate revision IDs. It does not reject placeholder-style content.scripts/ci/run_upgrade_validation.shenforces "exactly one alembic head". It does not verify the chain is linear (i.e., every revision has exactly one parent except the merge points, and merges are intentional)..pre-commit-lite.yamlโ the config used bymake pre-commitin CI โ does not include thecheck-migration-patternshook at all. So the local hook runs only for developers withpre-commit install'd, not in CI.๐ Proposed Architecture
Guardrail layer (do this first; prevents new placeholder hashes from landing)
Extend
check_migration_patterns.pyto reject the placeholder pattern. Concrete regex (rejects "alternating letter+digit, โฅ11 chars"):Also reject revision IDs that contain
0a1b,1b2c,2c3d, โฆ,8h9i,9i0jsubstrings (the diagonal chain marker).Add a
down_revision<->Revises:docstring consistency check to the same hook. Catches the 6 mismatches above and prevents new ones.Move
check-migration-patternsinto.pre-commit-lite.yamlso CI's pre-commit job actually runs it. Currently the hook only protects developers runningpre-commit install.Chain-shape verification (do this in CI, not pre-commit; needs the full graph)
Add
--check-lineartoscripts/ci/run_upgrade_validation.sh(or a sibling script). Walks the alembic graph, asserts:down_revision.*_merge_*AND is documented in a CHANGELOG/ADR.basetohead).Hook the new check into
alembic-upgrade-validation.ymlso PRs get blocked on broken chains, not just multi-head.Cleanup (do last; needs the guardrails first or it'll regress)
Re-hash the 36 placeholder revisions in a single PR per logical batch. Use
python -c "import secrets; print(secrets.token_hex(6))"to generate replacements. For each replaced revision, update:revision = "..."lineRevises:docstring of the next migrationdown_revision = "..."of the next migrationFix the 6 docstring/code mismatches during the rehash pass.
Add an ADR (
docs/docs/architecture/alembic-migration-hash-policy.md) recording the policy: "All revision IDs must come fromalembic revision -m '...'. Manual or pre-allocated IDs are not permitted. Merge migrations must be named*_merge_*and explained in commit/ADR."๐ง๐ปโ๐ป User Story 1 โ Developer
As a: developer adding a new migration
I want:
pre-committo fail loudly if I hand-edit a revision ID into something cuteSo that: I never get into a "two PRs picked
m7g8โฆsimultaneously" merge conflict again๐ง๐ปโ๐ป User Story 2 โ Reviewer / CI
As a: PR reviewer / CI
I want: the upgrade-validation workflow to refuse a chain that's secretly branched
So that: "exactly one head" is no longer the only graph property we enforce
๐ง๐ปโ๐ป User Story 3 โ Cleanup
As a: maintainer
I want: the existing 36 placeholder revisions re-hashed and the 6 docstring mismatches fixed
So that: the codebase stops carrying historical hash debt forward
๐ Children (track via this epic)
Already-existing related issues that fit under this umbrella:
[CHORE][DB]: Standardize Alembic migration revision IDs to auto-generated hex hashes(direct match for cleanup work item 6)Base.metadata.create_all()with Alembic-only bootstrapย #4555 โ[BUG]: Unify schema creation: replace Base.metadata.create_all() with Alembic-only bootstrap(adjacent โ schema ownership)fix(alembic): base schema tables (tools, gateways, servers) not created by migrations(adjacent โ chain-shape correctness)[BUG]: Alembic migration advisory lock hangs when multiple gateway pods start through PgBouncer(adjacent โ startup/locking)[BUG][DB]: bootstrap_db crashes on alembic version mismatch(adjacent โ version drift detection, sibling of guardrail work)[TESTING][UPGRADE]: Version Upgrades, Database Migrations, and Rollback Procedures(adjacent โ broader upgrade testing)[PERFORMANCE]: Optimize Database Migration Performance(adjacent)Already-closed prior art (cite for context, do not reopen):
[CHORE]: Add CI/CD validation for Alembic migration status(landed the existing single-head check; this epic extends it to linearity)[TESTING][DB]: Migration testing gaps โ rollback verification, cross-DB schema validation, and CI coverage(landed via PR Fix/alembic migration safetyย #4479)downgradeis unsafe, environment-dependent, and untested across multiple migrationsย #4429 โ[BUG]: Alembic downgrade is unsafe, environment-dependent, and untested(landed via PR Fix/alembic migration safetyย #4479)[BUG]: Incorrect Alembic migration placement and history(closed as duplicate, but symptom of the same problem this epic targets)New work items to file as children of this epic (after this epic is accepted):
check_migration_patterns.pywith placeholder-pattern rejection + docstring-vs-code consistency checkcheck-migration-patternsto.pre-commit-lite.yamlso CI runs itscripts/ci/run_upgrade_validation.sh(or a sibling script) and wire into.github/workflows/alembic-upgrade-validation.ymlRevises:docstring mismatches during the rehashdocs/docs/architecture/๐ฆ Affected migrations (reference data for cleanup PRs)
36 placeholder-style revisions to be re-hashed:
6
Revises:docstring mismatches to fix:20a0e0538ac5_add_composite_indexes_for_metrics_time_.py356a2d4eed6f_uuid_change_for_prompt_and_resources.pyadd_toolops_test_cases_table.pyba202ac1665f_migrate_user_roles_to_configurable_.pybb43712cae28_merge_token_blocklist_with_audit_identity_heads.pyr2b3c4d5e6f7_add_prompt_namespacing_fields.py1 header anomaly to investigate:
b77ca9d2de7e_uuid_pk_and_slug_refactor.pyhas noRevises:line; uses aCreate Date:header instead.๐ Acceptance criteria (epic-level)
check-migration-patternsrejects placeholder-style revision IDs (regex test + docstring consistency).pre-commit-lite.yamlincludes the migration hook so CI enforces itb77ca9d2de7eheader is normalized๐ฆ Related Make Targets
make pre-commitโ currently uses.pre-commit-lite.yaml, needs to include the migration hookmake upgrade-validateโ runsscripts/ci/run_upgrade_validation.sh, target for linearity checkmake migration-test-allโ sanity gate after rehash PRs landmake db-headsโ already shows count, will be the cleanest single-head reporter post-cleanup๐ Notes
maincommit so contributors with in-flight migrations can reparent cleanly.mcpgateway/alembic/versions/tree at the time of filing; happy to drop the exactpython -cinvocations into a comment if reviewers want to verify.