Skip to main content

Turn a late Alembic multiple-heads error into an early git conflict.

Project description

alembic-linear-migrations

Turn a late Alembic Multiple head revisions are present error into an early git conflict.

PyPI Python versions License

The problem

Two branches each add a migration from the same down_revision. They are separate files that never touch, so git merges both without complaint. Alembic only objects later, on someone else's machine or in a deploy:

Multiple head revisions are present for given argument 'head'

By then the mistake is already on the shared branch, and someone has to untangle it under time pressure. The failure is late, and far from its cause.

The fix

Record the migration graph's head in one tracked line. Every new migration rewrites it. Two branches then write different values to the same line, so git raises a conflict the moment the branches meet, which is the moment a human can still fix it cheaply:

# alembic-linear-migrations
# Records the current head of the migration graph.
# A conflict here means two branches added a migration from the same parent.
# Resolve with:  alembic-linear rebase
#
<<<<<<< HEAD
8f2a1c9d4e7b
=======
c4e6a8b0d2f1
>>>>>>> main

The header is identical on both sides, so it merges silently and only the revision line conflicts. Whoever hits it is told what it means and how to fix it, in place.

Install

Install it into the same environment as alembic, as a dev dependency:

uv add --dev alembic-linear-migrations
# or: pip install alembic-linear-migrations

Not pipx install / uv tool install. Alembic resolves the post_write_hook's entry point through its own environment's metadata. Installed in isolation it is invisible to alembic, which fails with Could not find entrypoint console_scripts.alembic-linear-update.

Then create the file:

alembic-linear update
git add migrations/head.txt

Wiring

The two triggers overlap on purpose. The alembic hook covers migrations made through the CLI; the pre-commit hook covers the ones that were not.

1. Alembic post_write_hook

Fires on alembic revision and alembic revision --autogenerate. In alembic.ini:

[post_write_hooks]
hooks = alembic_linear
alembic_linear.type = console_scripts
alembic_linear.entrypoint = alembic-linear-update

Already running hooks? Append to the list: hooks = black, alembic_linear.

2. pre-commit

Catches migrations written by hand, which never go through the CLI, so no hook fires. In .pre-commit-config.yaml:

repos:
  - repo: https://github.com/alexsavio/alembic-linear-migrations
    rev: v2026.7.0 # or the latest tag
    hooks:
      - id: alembic-linear
Hook id Behaviour
alembic-linear Rewrites head.txt, fails if it had to. For developers.
alembic-linear-check Read-only, fails if stale. For CI.

If your migrations import application code, run the hook in your project's own environment instead. Alembic imports every migration file to read its revision ids, and pre-commit's isolated environment has alembic but not your app:

  - repo: local
    hooks:
      - id: alembic-linear
        name: alembic-linear
        entry: alembic-linear update --exit-on-change
        language: system
        always_run: true
        pass_filenames: false

Resolving a conflict

When git reports a conflict in head.txt, both migrations are already present, so the graph has two heads. Leave the conflict markers alone and run:

alembic-linear rebase

It re-points your migration to sit on top of theirs, rewrites head.txt, and refuses to finish unless the graph is left with exactly one head:

Re-pointed 8f2a1c9d4e7b onto c4e6a8b0d2f1.

  migrations/versions/8f2a1c9d4e7b_add_orders.py
    down_revision: a1b2c3d4e5f6 -> c4e6a8b0d2f1

  head.txt now records 8f2a1c9d4e7b, and the graph has one head.

Then review both migrations, git add them, and finish the merge or rebase.

Working out which side is yours is the part worth owning. Git labels the two sides of a conflict HEAD and incoming, but which one holds your work inverts between operations: git merge leaves your commit on the HEAD side, while git rebase checks out the upstream first and replays your commit as the incoming side. rebase reads the git state and accounts for that, so the outcome is the same either way. When no merge or rebase is in progress there is nothing to infer from, so name the other branch's head yourself:

alembic-linear rebase --onto c4e6a8b0d2f1

If your branch added several migrations, only the bottom one moves; the rest ride along on top of it.

Commands

Command Purpose
alembic-linear update Recompute head.txt from the graph.
alembic-linear check Exit 1 if the graph has multiple heads, or head.txt is stale.
alembic-linear rebase Resolve a conflict between two branches.

-c/--config points at an alembic.ini; otherwise the working directory and its parents are searched. Reading the graph never executes env.py, so no command needs a database or a running application.

Limitations

  • Linear histories only. Projects that deliberately use Alembic branch labels and expect several heads are out of scope. check says so plainly rather than crashing.
  • Ordering, not semantics. This decides which migration runs first. Two branches that both add the same column are a schema conflict no tool can resolve for you; rebase orders them and tells you to read them.
  • head.txt has to be committed. It is the conflict carrier. Untracked, it cannot conflict, and none of this works.

Where the file lives

<script_location>/head.txt, next to env.py, not inside versions/. One migration graph has one head, so one file describes it exactly, however many version_locations are configured. Keeping it out of versions/ also keeps a non-.py file out of a directory alembic scans.

Prior art

django-linear-migrations, which does this for Django with a max_migration.txt per app. This is the same idea fitted to Alembic, where one migration graph means one head file, and where resolving a conflict means rewriting a down_revision while accounting for git's merge/rebase inversion.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alembic_linear_migrations-2026.7.0.tar.gz (24.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alembic_linear_migrations-2026.7.0-py3-none-any.whl (18.8 kB view details)

Uploaded Python 3

File details

Details for the file alembic_linear_migrations-2026.7.0.tar.gz.

File metadata

File hashes

Hashes for alembic_linear_migrations-2026.7.0.tar.gz
Algorithm Hash digest
SHA256 fb19b7ec87bf16b691c9a331b3345d74745172c681797b6cc84a69e74fb3a0a9
MD5 427821f5820b9debbaf4a89351dbfcce
BLAKE2b-256 ff55afa2096ff49eeaea37b9b9a049da30a3f2e36782443baa190cb51edc1b46

See more details on using hashes here.

Provenance

The following attestation bundles were made for alembic_linear_migrations-2026.7.0.tar.gz:

Publisher: publish.yml on alexsavio/alembic-linear-migrations

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file alembic_linear_migrations-2026.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for alembic_linear_migrations-2026.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3d18f645f0572543158b1cbcf5e0dd4aaa9ced9b7bf1d87f716bd98c5c3d09e2
MD5 249423803a85fc3cd4f0036527b6592b
BLAKE2b-256 207a8b66a9cde5c3468a361373e3971e391ba12ffc43715306c40c7290be183f

See more details on using hashes here.

Provenance

The following attestation bundles were made for alembic_linear_migrations-2026.7.0-py3-none-any.whl:

Publisher: publish.yml on alexsavio/alembic-linear-migrations

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page