Migration Helper
migration-helper is a Python project for helping migrate Apache Airflow code
from Airflow 2 to Airflow 3.
The current implementation is focused on static analysis and migration assistance for DAG code. It includes checks based on Ruff AIR rules, Airflow configuration linting, DAG import checks, dependency compatibility checks, base reporting, shared subprocess helpers, file selection helpers, and tests.
Expected Output
After running migration-helper, you get a practical migration report for the
selected Airflow project. The report lists compatibility issues by file and rule,
shows error/warning severity, and explains whether each finding can be handled
automatically or needs manual review.
Reports can be written in JSON and HTML formats. The JSON report is intended for automation and CI, while the HTML report is intended for manual review. Both formats expose the same issue details.
The JSON report has this top-level structure:
generated_at: UTC timestamp when the report was created.target: analyzed file or directory.summary: total number of findings, split intoerrorsandwarnings.by_file: findings grouped by affected file path.by_type: findings grouped byrule_id.
Each finding contains:
file: affected file or logical target, for exampleairflow.cfgorrequirements.txt.lineandcol: source location when the checker can provide it; otherwisenull.rule_id: checker-specific rule or error identifier, for exampleAIR301,CFG001, orDEP_RESOLUTION_FAILED.message: short explanation of the problem.severity:error,warning, orinfo.source: checker that produced the finding, for exampleruff,airflow_config,dag_check,dep_check, ordb_migrate.
Reports use these fields to describe fixability:
fix_safety: "safe"means the checker knows a safe mechanical fix or safe migration action. Safe Ruff fixes can be applied withmigration-helper fix TARGET --safe-only; Airflow config fixes require the config fix path and are still blocked by--dry-run.fix_safety: "unsafe"means the tool knows the likely change, but a human should review it before applying.fix_safety: null(orfix_safety: "manual"in HTML report) means there is no automatic fix path in the current implementation. Treat it as manual work.fix_descriptionis the human-readable hint for what to change or which command/path is relevant.
Today, DAG import errors, dependency conflicts, and DB migration findings are diagnostic/manual. Source-level DAG fixes outside Ruff are still planned.
Requirements and setup
- Python
>=3.10 gitavailable inPATH
Install from PyPI:
pip install migration-helper
Offline installation
To install on a machine without access to PyPI, build a wheelhouse on a machine with internet access and install from it:
# On a machine with internet access
pip download migration-helper==1.0.1 -d wheelhouse/
# On the offline machine
pip install --no-index --find-links ./wheelhouse migration-helper
Note: ruff is a platform-specific binary package. Download the wheelhouse on
a machine with the same OS and the same Python version as the target machine.
If the wheelhouse must be prepared for a different platform than the download
machine, pass the target platform tags explicitly. This works only with
pre-built wheels, so --only-binary=:all: is required:
# Build a wheelhouse for manylinux x86_64 + CPython 3.11 from any machine
pip download migration-helper==1.0.1 -d wheelhouse/ \
--only-binary=:all: \
--platform manylinux2014_x86_64 \
--python-version 311 \
--implementation cp \
--abi cp311
ruff ships platform-specific wheels (py3-none-manylinux2014_x86_64,
macosx_11_0_arm64, win_amd64, ...) so the --platform tag matters most;
packaging and tomli are pure Python and work everywhere. Repeat the command
per target platform (for example macosx_11_0_arm64 or win_amd64) or build
the wheelhouse on a machine matching the target configuration.
Alternatives: copy a prepared virtual environment as a whole (same OS /
architecture / Python required), or serve the packages from an internal PyPI
mirror (devpi, Nexus, Artifactory) and use pip config set global.index-url http://<mirror>/.
Offline usage notes: Airflow 3.2.1 constraints for Python 3.10-3.14 are bundled
with the package, so the default dependency check works offline. Other Airflow
versions require network access to download constraints, and the deep
dependency check (pip install --dry-run / uv pip compile) needs access to a
package index; point it at an internal one via PIP_INDEX_URL / UV_INDEX_URL
if needed. git must still be available in PATH.
For development, install with test and Airflow extras instead:
pip install -e ".[dev]"
This editable install is required if you want to run the CLI as
migration-helper from the repository checkout. Without it, use
python -m migration_helper.cli ... from the repository root.
Usage
migration-helper COMMAND [TARGET] [OPTIONS]
Use the built-in help commands to see available options:
migration-helper --help
migration-helper analyze --help
migration-helper fix --help
Commands
analyze: scanTARGETfor Airflow 3 compatibility issues in read-only mode.fix: scanTARGETand apply or dry-run available auto-fix paths.
Shared Options
TARGET: directory or file to scan. Defaults to the current directory.--report-dir PATH,-r PATH: writemigration_report.jsonand/ormigration_report.htmlintoPATH.--output-format json|html|all: report format when--report-diris set. Default:all.--modified-only: only check files reported bygit status --porcelainunder the target path. If git state cannot be resolved, all files are checked.--target-airflow-version VERSION: Airflow version for dependency checks. Default:3.2.1.--target-python-version VERSION: Python major/minor version for dependency constraints. Defaults to the current interpreter version.--dependency-file FILE: dependency file to check at the target root. Supports requirements-style.txtfiles andpyproject.toml. Default:requirements.txt.--use-uv: useuvinstead ofpipfor dependency resolver checks.--db-migrate: enable the DB migration stage. The current implementation is still a placeholder, so this flag is part of the CLI contract but does not yet produce a real DB migration report.--quiet,-q: suppress progress and summary output. Exit codes are still set.
Fix Options
--safe-only: enable safe mechanical fixes where they are currently implemented. Today this mainly drives Ruff/config fix paths; the source-level DAG fixer is still a placeholder.--unsafe: also enable unsafe fixes that require human review. Implies safe fixes.--dry-run: guarantee that files and the database are not modified. In the current implementation, dry-run disables Ruff/config fix application before the pipeline runs, so it is mostly a scan/report safety check rather than a full diff preview.--fix-config: request Airflow config updates. Config update still respects--dry-run.
Exit Codes
0: no error-severity issues found. Warnings may still be present.1: one or more error-severity issues found.2: invalid arguments or theTARGETpath does not exist.
Examples
# Read-only scan of all DAGs in ./dags/
migration-helper analyze ./dags/
# Scan and write HTML + JSON reports
migration-helper analyze ./dags/ --report-dir ./reports/
# Check dependencies against a specific Airflow/Python target
migration-helper analyze ./dags/ --target-airflow-version 3.2.1 --target-python-version 3.10
# Only report issues in files changed in the current git working tree
migration-helper analyze ./dags/ --modified-only
# Dry-run the current fix command path without writing files
migration-helper fix ./dags/ --safe-only --dry-run
# Apply currently available safe fixes in-place
migration-helper fix ./dags/ --safe-only
# Quiet mode for CI
migration-helper analyze ./dags/ --quiet
echo $? # 0 = clean, 1 = errors
License
Licensed under the Apache License, Version 2.0.
See the LICENSE file for the full license text.
Common Commands
Run unit tests:
pytest tests/unit
Run integration tests:
pytest -m integration tests/integration
Run all regular tests, excluding integration and e2e by default:
pytest
Run e2e tests explicitly:
python -m pytest tests/e2e -m e2e
If you would like to test the tool with your own DAGs, place them in test_dags
or another local folder and run the tool based on the examples above.
E2E Testing
The project keeps a small Airflow 2 DAG corpus under
tests/e2e/airflow2-dag-corpus/ and separate runtime provider fixtures under
tests/e2e/airflow2-provider-fixtures/. The corpus is the migration-helper
target; provider fixtures are import support only.
Run all e2e checks explicitly:
python -m pytest tests/e2e -m e2e
Run the Airflow 2 baseline import smoke:
cd tests/e2e
docker compose -f docker-compose.airflow2.yml run --rm airflow2-smoke
For the current e2e layout, DAG corpus contents, dry-run artifacts under
tests/e2e/runs/, and the difference between analyze and fix --dry-run, see
tests/e2e/README.md.
Docker
The project includes a multi-stage Dockerfile and docker-compose.yml.
Two images are built:
- production: runtime dependencies used for running
migration-helpercommands. - dev: development/test dependencies used for running tests.
Build
docker compose build
Prepare DAG files
Place your DAG files in test_dags/ (this directory is git-ignored):
mkdir -p test_dags
cp path/to/your_dag.py test_dags/
Run analysis (read-only)
The migration-helper-analyze service mounts ./test_dags at /dags as
read-only, which is appropriate for analyze:
docker compose run --rm migration-helper-analyze
Or pass any command and options directly:
docker compose run --rm migration-helper-analyze analyze /dags
docker compose run --rm migration-helper-analyze analyze /dags --modified-only
docker compose run --rm migration-helper-analyze analyze /dags/demo.py
Apply fixes (writable)
The migration-helper-fix service mounts ./test_dags at /dags as writable
so Ruff can rewrite .py files in place:
docker compose run --rm migration-helper-fix
Examples:
docker compose run --rm migration-helper-fix fix /dags --safe-only --dry-run
docker compose run --rm migration-helper-fix fix /dags/demo.py --safe-only
docker compose run --rm migration-helper-fix fix /dags/demo.py --unsafe
Run tests
Unit tests:
docker compose run --rm test
Integration tests:
docker compose run --rm test -m integration tests/integration
E2E tests:
docker compose run --rm test tests/e2e -m e2e
E2E dry-run reports are written to:
tests/e2e/runs/<UTC timestamp>/
Open the latest run directory and check
analyze/analyze-report/migration_report.html,
fix-dry-run/fix-dry-run-report/migration_report.html,
fix-safe/fix-safe-report/migration_report.html, or
fix-unsafe/fix-unsafe-report/migration_report.html.
The main docker-compose.yml mounts ./tests to /app/tests, so reports
created in the test container are available on the host immediately. No
docker cp is needed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file migration_helper-1.0.1.tar.gz.
File metadata
- Download URL: migration_helper-1.0.1.tar.gz
- Upload date:
- Size: 215.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a546e452289548f7dea6dc5777df423a86a87a5267240f402e42443172fe3cf2
|
|
| MD5 |
1d45b6ef80f315d8cb5a82753f953e16
|
|
| BLAKE2b-256 |
2e1e3b769651038e1c0cbe862065e0d49637e47c857de2dda8a4ec6cdda6fcdb
|
File details
Details for the file migration_helper-1.0.1-py3-none-any.whl.
File metadata
- Download URL: migration_helper-1.0.1-py3-none-any.whl
- Upload date:
- Size: 78.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68535e87e840a4c85bf936b464d90daf6c8a17f5ac613948f27e2d38a16928ee
|
|
| MD5 |
5e0b8454ab018a6006c838496ac4b0ee
|
|
| BLAKE2b-256 |
67a8526ad1da89692ba4ccf3ebb0811c91a28203f9a8b5b26035fa79e878f947
|