Oneport Migrate
The database-migration safety gate for Python teams. Django migrations,
Alembic revisions, and raw .sql — checked by a deterministic rule engine,
judged by an LLM that knows your repo, and wired into CI as a blocking gate.
Migrations are the scariest deploy moment: squawk lints Postgres SQL only,
strong_migrations is Rails only. Nothing covers the Python world's actual
migration formats. Oneport Migrate does.
How it works
Two layers, strictly separated:
-
Deterministic rule engine — parses your migrations (Django via AST, Alembic via AST, raw SQL via regex) into a normalized operation model and matches it against a YAML rule catalog (
OPM001+). This layer alone decides whether an operation is dangerous. No model, no network, no flakiness — the same migration always produces the same findings. -
LLM blast-radius layer (Gemini or Claude) — given the findings, the migration source, and your models/schema files, it judges how dangerous in this repo ("the
userstable is written on every request — a 4-minute lock here is an outage"), writes a safe multi-step rewrite plan (expand → backfill in batches → contract), and a plain-English verdict. It can adjust a finding's severity only when your team guidelines explicitly justify it — never on its own judgment.
If the LLM is unavailable (no key, rate limit, bad output), the gate still works: deterministic findings and the exit code stand on their own.
Install
pip install oneport-migrate
export GEMINI_API_KEY=... # free at https://aistudio.google.com/apikey
Claude instead of Gemini: pip install oneport-migrate[claude] and set
ANTHROPIC_API_KEY.
Usage
# One file, a directory, git state, or a PR
oneport-migrate check app/migrations/0042_drop_legacy.py
oneport-migrate check migrations/
oneport-migrate check --staged
oneport-migrate check --head
oneport-migrate check https://github.com/org/repo/pull/42
# Post findings as an inline PR review (requires GITHUB_TOKEN)
oneport-migrate check https://github.com/org/repo/pull/42 --post
# Options
oneport-migrate check migrations/ --db mysql --format json --min-severity error
oneport-migrate check migrations/ --format sarif > migrate.sarif # GitHub code scanning
oneport-migrate check migrations/ --fail-on critical # only critical blocks CI
oneport-migrate check migrations/ --no-llm # deterministic rules only
oneport-migrate rules list
Exit codes (the CI contract)
| Code | Meaning |
|---|---|
| 0 | clean, or warnings/info only |
| 1 | blocking findings (default: error/critical — tune with --fail-on) |
| 2 | usage / config / auth error |
Blocking is computed from the full finding set before any display
filtering — --min-severity critical can hide an error from the output, but
never from the exit code.
--min-severity and --fail-on are independent: the first controls what you
see, the second controls what blocks. Waived findings never block, at
any threshold.
Tuning what blocks — --fail-on
By default any error or critical finding fails the gate. Raise or lower the
bar per run (or in .oneportmigraterc via fail_on:):
oneport-migrate check --head --fail-on critical # only destructive ops block; errors are advisory
oneport-migrate check --head --fail-on warning # strict — even renames block
Waivers — .oneport/migrate-waivers.yml
Approve a specific finding without ignoring the rule everywhere. A waived
finding is still shown (marked [WAIVED]) but never blocks CI. Expired waivers
auto-reactivate the finding, so an approval can't silently outlive its reason.
waivers:
- file: "**/0042_drop_legacy.py" # glob, or a bare basename
rule_id: OPM001 # optional — omit to waive every rule in that file
reason: "column unused since v3, DBA signed off CHG-2211"
expires: "2026-12-31" # optional — after this date the finding blocks again
Waived findings appear in SARIF under suppressions, so GitHub code scanning
shows them as dismissed rather than dropping them.
SARIF export — --format sarif
Emits SARIF 2.1.0 for GitHub code scanning (or any SARIF viewer). Severity maps
to error/warning/note; each finding links to its file and line, carries
the safe-rewrite suggestion, and waived findings are marked suppressed.
# .github/workflows/migrate.yml
- run: oneport-migrate check --head --format sarif > migrate.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: migrate.sarif }
Rule catalog
| ID | Severity | What it catches |
|---|---|---|
| OPM001 | critical | Dropping a column |
| OPM002 | critical | Dropping a table |
| OPM003 | critical | TRUNCATE / DELETE without WHERE |
| OPM010 | error | ALTER COLUMN TYPE (table rewrite under exclusive lock) |
| OPM011 | error | Adding a NOT NULL column without a default |
| OPM012 | error | CREATE INDEX without CONCURRENTLY (Postgres only) |
| OPM013 | error | Adding NOT NULL to an existing column |
| OPM020 | warning | Missing reverse migration (reverse_code / downgrade / reverse_sql) |
| OPM021 | error | Data migration inside a schema transaction |
| OPM030 | warning | Column rename (breaks rolling deploys) |
| OPM031 | warning | Table rename (breaks rolling deploys) |
Indexes on tables created in the same migration are exempt from OPM012 (the
table is empty). OPM012 only fires for --db postgres.
Project config — .oneportmigraterc
db: postgres
fail_on: error # severity that blocks CI (info | warning | error | critical)
rules:
ignore: [OPM031]
severity:
OPM012: warning # deterministic override, applies before the LLM runs
output:
format: inline # inline | json | sarif
min_severity: warning
Team guidelines — .oneport/guidelines.md
The same file oneport-review uses. The LLM reads it and may downgrade or upgrade a finding's severity only when an entry explicitly covers it, quoting the guideline in its reason:
- our users table is small (<10k rows), downgrade lock severity
- the events table is 400M rows and append-only — never rewrite in place
Honest limits
- Raw SQL parsing is regex-based. Statements are split on
;without full string-literal awareness;$$-quoted function bodies and dynamic SQL (EXECUTE format(...)) are not analyzed. When the parser hits these it attaches a note to the output instead of guessing. Misses are false negatives — it never invents a finding. - Django
AlterField/ Alembicalter_column(type_=...): a single migration file doesn't show the old column definition, so every field alteration is flagged as a potential rewrite. You (or the LLM, with repo context) decide if it's actually a no-op. RunSQL/op.executewith non-literal arguments (variables, f-strings) can't be inspected — recorded as unanalyzed raw SQL with a note.- Table sizes are unknown to the rule engine. "Large-table" judgment is exactly what the LLM layer is for; without an API key you get the deterministic worst-case severities.
--staged/--headread file contents from the working tree.
CI
See examples/workflows/migrate-gate.yml:
- run: pip install oneport-migrate
- run: oneport-migrate check --head --post "$PR_URL"
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--post submits one inline PR review per revision; a hidden marker in the
review body prevents duplicate reviews when CI re-runs on the same commit.
Privacy
Serverless: your migration files go directly from your machine/CI to the model API using your key. No Oneport server ever sees your code. See PRIVACY.md.
Part of the Oneport suite
- oneport-review — CodeRabbit-class AI code review
- oneport-migrate — this tool
License
MIT
Metadata
Release files for oneport-migrate 0.3.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 | Provenance | Actions |
|---|---|---|---|---|
| oneport_migrate-0.3.0.tar.gz | 51.4 kB | Without provenance | Details for oneport_migrate-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| oneport_migrate-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for oneport_migrate-0.3.0-py3-none-any.whl |
Total release size: 110.6 kB
Release files / oneport_migrate-0.3.0.tar.gz
| Download URL | oneport_migrate-0.3.0.tar.gz |
|---|---|
| Size | 51.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d057e62c18326ad86ede62c2338cd6943bee0fcd6307720d5f5312bb37ba2d09
|
|
BLAKE2b-256 checksum How to use checksums |
17dbd8115cdbb0351bbaccbb9cc99a5beb83b04674ab54b7054a643e011ffe9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.0
|
Release files / oneport_migrate-0.3.0-py3-none-any.whl
| Download URL | oneport_migrate-0.3.0-py3-none-any.whl |
|---|---|
| Size | 59.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4bd6f6561e1bcebd17cdc30045fd104fcb9ed6b9814f32e1904ded319187657
|
|
BLAKE2b-256 checksum How to use checksums |
302dd8a921716b2452359aa671e81925ac2f961f5d1c05b1aaa3a308b80f1307
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.0
|
Security for oneport-migrate 0.3.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.