alembic-linear-migrations
Turn a late Alembic Multiple head revisions are present error into an early git conflict.
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 withCould 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.
checksays 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;
rebaseorders them and tells you to read them. head.txthas 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
Metadata
Release files for alembic-linear-migrations 2026.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| alembic_linear_migrations-2026.7.0.tar.gz | 24.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| alembic_linear_migrations-2026.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.1 kB
Release files / alembic_linear_migrations-2026.7.0.tar.gz
| Download URL | alembic_linear_migrations-2026.7.0.tar.gz |
|---|---|
| Size | 24.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fb19b7ec87bf16b691c9a331b3345d74745172c681797b6cc84a69e74fb3a0a9
|
|
BLAKE2b-256 checksum How to use checksums |
ff55afa2096ff49eeaea37b9b9a049da30a3f2e36782443baa190cb51edc1b46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logRelease files / alembic_linear_migrations-2026.7.0-py3-none-any.whl
| Download URL | alembic_linear_migrations-2026.7.0-py3-none-any.whl |
|---|---|
| Size | 18.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d18f645f0572543158b1cbcf5e0dd4aaa9ced9b7bf1d87f716bd98c5c3d09e2
|
|
BLAKE2b-256 checksum How to use checksums |
207a8b66a9cde5c3468a361373e3971e391ba12ffc43715306c40c7290be183f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency log