Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ uv run flask db upgrade
- Use idempotent SQL (`IF NOT EXISTS`, `IF EXISTS`) when writing raw SQL in migrations.
- Never edit a migration that has already been applied to production.

### Keeping a Single Head

Each migration hardcodes its parent in `down_revision`, so two branches that both add a
migration will both claim the same parent. Git merges them cleanly because they touch
different files, and the break only surfaces as `Multiple head revisions are present`
when `flask db upgrade` runs on deploy.

`tests/test_migrations.py` fails the build when a branch has more than one head. If it
fires after merging `main` into your branch:

- **Your migration has not been applied anywhere yet** (the normal case): edit its
`down_revision` to point at the new head. Check the head with
`uv run flask db heads`.
- **It has already been applied on staging or production**, or the two migrations touch
the same tables and order matters: do not re-parent. Run `uv run flask db merge heads`
to create a merge revision instead.

Note that only the first branch to merge keeps a valid parent. If another migration lands
on `main` before yours, re-parent again onto the new head.

# Tests

Bayanat comes with e2e tests using pytest and pydantic models. To run the tests, install the dependencies with
Expand Down
11 changes: 9 additions & 2 deletions enferno/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,13 +558,20 @@ def fail(msg):

config = migrate_ext.get_config()
script = ScriptDirectory.from_config(config)
head = script.get_current_head()
# get_current_head() raises when a branch merge left two heads, which would be
# swallowed as a warning below. Read them all so the break is reported as a failure.
heads = script.get_heads()

context = MigrationContext.configure(db.session.connection())
current = context.get_current_heads()
current_rev = current[0] if current else None
head = heads[0] if heads else None

if current_rev is None:
if len(heads) > 1:
fail(
f"Multiple migration heads ({', '.join(h[:8] for h in heads)}): db upgrade will abort"
)
elif current_rev is None:
warn("No Alembic revision stamped (run: flask db upgrade)")
elif current_rev == head:
ok("Migrations up to date")
Expand Down
20 changes: 20 additions & 0 deletions tests/test_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pathlib import Path

from alembic.config import Config
from alembic.script import ScriptDirectory


def test_single_alembic_head():
"""Guard against two branches each parenting a migration on the same revision.

Git merges such branches cleanly (different files), but the result has two heads
and `flask db upgrade` aborts mid-deploy. Fix by re-parenting the newer migration
onto the current head, or `alembic merge heads` if both have already been applied.
"""
config = Config()
config.set_main_option(
"script_location", str(Path(__file__).resolve().parent.parent / "migrations")
)
heads = ScriptDirectory.from_config(config).get_heads()

assert len(heads) == 1, f"Expected a single Alembic head, found {len(heads)}: {heads}"
Loading