Files
airflow/AGENTS.md
T
Jarek Potiuk 4f3b228be1 Isolate non-provider mypy hooks per distribution with dedicated .build/ venvs (#65492)
* Isolate mypy prek hooks, cover all non-provider dirs, and clean up type errors

Each non-provider mypy prek hook now builds and caches its own virtualenv
at .build/mypy-venvs/<hook>/ and its own mypy cache at
.build/mypy-caches/<hook>/. UV_PROJECT_ENVIRONMENT redirects uv away from
the project's .venv, so running the hook never mutates a contributor's
regular development environment while still matching CI's frozen
dependency set. Mypy runs with --follow-imports=silent so each hook only
reports errors for files it owns; transitive code is covered by its own
hook and different venvs no longer produce divergent results on shared
code.

Adds mypy hooks for the non-provider directories that were previously
uncovered: airflow-ctl-tests, helm-tests, airflow-e2e-tests,
task-sdk-integration-tests, docker-tests, kubernetes-tests, and shared.
The mypy-shared hook iterates every shared/<dist> workspace distribution
and builds a separate venv + cache per distribution so each shared
library is type-checked against its own dependency set.

breeze down --cleanup-mypy-cache additionally removes
.build/mypy-venvs/ and .build/mypy-caches/ so all per-hook state is
wiped alongside the existing .mypy_cache and mypy-cache-volume.

Also fixes pre-existing type errors surfaced by the newly added and
cleaned-up checks: platform-specific ignores for Linux-only
os.posix_fadvise in the shared logging helper, narrower types and
type: ignore where appropriate in shared configuration/observability/
timezones/secrets_backend/secrets_masker, Liskov override markers on
the AirflowConfigParser subclass methods, and small correctness fixes
in dev/breeze and the docker-tests / kubernetes-tests helpers so the
full non-provider mypy suite runs clean on macOS and in CI.

* Move mypy prek hooks to their respective distribution configs

The new mypy hooks for airflow-ctl-tests, helm-tests, airflow-e2e-tests,
task-sdk-integration-tests, docker-tests, and kubernetes-tests now live
in each distribution's own .pre-commit-config.yaml, matching the pattern
already used by airflow-core, task-sdk, and airflow-ctl. New .pre-commit-
config.yaml files are added to distributions that didn't have one. prek
auto-discovers nested configs, so the hooks remain part of the default
check set.

mypy-dev (covers dev + scripts), mypy-devel-common, and mypy-shared stay
at the repo root: dev/scripts/devel-common don't have their own configs,
and mypy-shared iterates every shared/<dist> distribution so has no
single home.

* Split mypy-dev and mypy-scripts, each with its own pyproject.toml config

Previously the mypy-dev prek hook ran mypy against dev/ and scripts/ in
a single invocation under the dev project's virtualenv. The two now get
independent hooks — mypy-dev in dev/.pre-commit-config.yaml and
mypy-scripts in scripts/.pre-commit-config.yaml — so each can evolve its
own dependency set and check its own folder.

Copy the full [tool.mypy] section from the root pyproject.toml into both
dev/pyproject.toml and scripts/pyproject.toml so each sub-project owns its
mypy configuration. Paths inside mypy_path are rewritten from
$MYPY_CONFIG_FILE_DIR/ to $MYPY_CONFIG_FILE_DIR/../ so they still resolve
to the repo-root siblings from the sub-project location. The decorator/
outputs plugins are scoped to dev only (scripts does not author DAG code).

mypy_local_folder.py now passes --config-file <project>/pyproject.toml
when the folder maps to one of these sub-project configs, so mypy uses
the sub-project's configuration rather than the root one.

* Teach selective-checks about the new non-provider mypy hooks

Add FileGroupForCi entries and regex patterns for helm-tests,
airflow-e2e-tests, docker-tests, kubernetes-tests, scripts, and shared
Python files, then wire them into skip_prek_hooks so the corresponding
mypy-* prek hook is only kept when its folder changed:

- mypy-scripts (split off from the old combined mypy-dev)
- mypy-airflow-ctl-tests, mypy-helm-tests, mypy-airflow-e2e-tests,
  mypy-task-sdk-integration-tests, mypy-docker-tests, mypy-kubernetes-tests
- mypy-shared

Update test_selective_checks.py skip-list constants and per-case inline
skip lists to include the new hooks. Targeted test cases for files under
the new-hook directories override skip-prek-hooks to leave the matching
hook out of the skip set, confirming it will run when its folder changes.

* Trim dev/scripts pyproject mypy_path to just relevant distributions

Drop the 200+ provider path entries that were blindly copied from the root
pyproject.toml. dev and scripts only import from other non-provider
workspace members, so listing every provider src/tests directory under
mypy_path just adds noise. The remaining non-provider entries cover
everything dev or scripts plausibly import from.

* Install mypy into per-hook venvs from uv.lock via a `mypy` dep group

Each non-provider distribution with a mypy prek hook now declares a
`mypy` dependency group in its pyproject.toml resolving to
`apache-airflow-devel-common[mypy]`. mypy_local_folder.py syncs each
dedicated virtualenv with `uv sync --frozen --project <X> --group mypy`
and runs mypy with `uv run --frozen --project <X> --group mypy` — so
mypy and its type stubs come from the workspace uv.lock, not from an
ephemeral `--with` overlay whose resolution is independent of the main
lockfile. uv.lock is refreshed to include the new group.

Covers airflow-core, task-sdk, airflow-ctl, devel-common, dev, scripts,
airflow-ctl-tests, helm-tests, airflow-e2e-tests, task-sdk-integration-
tests, docker-tests, kubernetes-tests, and every shared/<dist>
workspace member.

* Drop mypy_path from dev/scripts pyprojects — venv site-packages is enough

After the switch to installing mypy (and every transitive workspace
dependency) directly into each hook's virtualenv via the `mypy` dep
group, workspace packages like airflow, airflow.sdk, airflowctl,
airflow_breeze, tests_common are all available via the venv's
site-packages. mypy resolves them without needing mypy_path entries,
so drop the copied list and leave a short comment explaining why.

* Split mypy-shared into per-distribution hooks and enforce the pattern

Each shared/<dist> workspace member now owns a mypy-shared-<dist> prek hook
backed by its own shared/<dist>/.pre-commit-config.yaml. The single
mypy-shared iterator is gone — mypy_local_folder.py accepts shared/<dist>
as a first-class folder and the per-hook virtualenv now lives at
.build/mypy-venvs/shared-<dist>/ (slash in the folder name is replaced
with a dash in the venv/cache path).

Adds a new check-shared-mypy-hooks prek hook that fails when a
shared/<dist> workspace member is missing its dedicated .pre-commit-
config.yaml, printing the exact YAML to add. Selective-checks emits one
skip entry per dist, enumerated from shared/ at run time. Contributing
docs cover the two-step process for adding a new shared library.

* Pin minimum_prek_version to 0.3.4 consistently across all configs

All .pre-commit-config.yaml files now require prek >= 0.3.4 (the version
already declared by the root config). Previously the nested configs
pinned a mix of 0.2.0, 0.3.2, and 0.3.4, so a contributor could pass the
root's version check and still trip on stale subproject pins as they
moved between directories.

* Refresh uv.lock after rebase to reflect the `mypy` dep groups

The rebase onto main resolved the uv.lock conflict by taking main's
version, so `uv sync --group mypy` would fail against uv.lock until
the groups added to the per-distribution pyprojects were re-resolved.
Regenerates the lockfile to include them.

* Add explicit selective-checks test for per-shared-dist mypy hook skipping

Verifies that when a file under shared/logging/ changes, only
mypy-shared-logging is kept among the thirteen mypy-shared-* hooks;
all other shared distributions' hooks land in the skip list. Pins the
contract that the runtime enumeration over shared/*/pyproject.toml
works as intended.

* Refresh mypy docs to match the per-hook venv + --group mypy workflow

Fills in the docs that still referenced the pre-split workflow:

- AGENTS.md: mentions `mypy-shared-<dist>` per shared workspace member and
  the `uv sync --group mypy` install path for mypy itself.
- scripts/ci/prek/AGENTS.md: clarifies that non-provider mypy hooks run
  locally through mypy_local_folder.py (Breeze image only needed for the
  providers hook).
- dev/breeze/doc/03_developer_tasks.rst: renames stale `mypy-airflow` to
  `mypy-airflow-core`, and expands the cache note to cover the per-hook
  virtualenvs and caches under .build/.
- dev/breeze/doc/ci/04_selective_checks.md: expands the file-group and
  skip-reason lists so every new mypy hook (scripts, task-sdk, airflow-ctl,
  the six test-dir hooks, and mypy-shared-<dist> enumerated at runtime)
  is documented.

* Rename mypy_local_folder.py to run_mypy_full_dist_local_venv_or_breeze_in_ci.py

Updates every .pre-commit-config.yaml entry and prose references so they
point at the new script name. Two shared configs use YAML folded-scalar
entries to stay under the 110-char yamllint limit; updates the validation
script's expected template to match.
2026-04-20 13:04:32 +02:00

16 KiB

AGENTS instructions

Environment Setup

  • Install prek: uv tool install prek
  • Enable commit hooks: prek install
  • Never run pytest, python, or airflow commands directly on the host — always use breeze.
  • Place temporary scripts in dev/ (mounted as /opt/airflow/dev/ inside Breeze).

Commands

<PROJECT> is folder where pyproject.toml of the package you want to test is located. For example, airflow-core or providers/amazon. <target_branch> is the branch the PR will be merged into — usually main, but could be v3-1-test when creating a PR for the 3.1 branch.

  • Run a single test: uv run --project <PROJECT> pytest path/to/test.py::TestClass::test_method -xvs
  • Run a test file: uv run --project <PROJECT> pytest path/to/test.py -xvs
  • Run all tests in package: uv run --project <PROJECT> pytest path/to/package -xvs
  • If uv tests fail with missing system dependencies, run the tests with breeze: breeze run pytest <tests> -xvs
  • Run a Python script: uv run --project <PROJECT> python dev/my_script.py
  • Run core or provider tests suite in parallel: breeze testing <test_group> --run-in-parallel (test groups: core-tests, providers-tests)
  • Run core or provider db tests suite in parallel: breeze testing <test_group> --run-db-tests-only --run-in-parallel (test groups: core-tests, providers-tests)
  • Run core or provider non-db tests suite in parallel: breeze testing <test_group> --skip-db-tests --use-xdist (test groups: core-tests, providers-tests)
  • Run single provider complete test suite: breeze testing providers-tests --test-type "Providers[PROVIDERS_LIST]" (e.g., Providers[google] or Providers[amazon] or "Providers[amazon,google]")
  • Run Helm tests in parallel with xdist breeze testing helm-tests --use-xdist
  • Run Helm tests with specific K8s version: breeze testing helm-tests --use-xdist --kubernetes-version 1.35.0
  • Run specific Helm test type: breeze testing helm-tests --use-xdist --test-type <type> (types: airflow_aux, airflow_core, apiserver, dagprocessor, other, redis, security, statsd, webserver)
  • Run other suites of tests breeze testing <test_group> (test groups: airflow-ctl-tests, docker-compose-tests, task-sdk-tests)
  • Run scripts tests: uv run --project scripts pytest scripts/tests/ -xvs
  • Run Airflow CLI: breeze run airflow dags list
  • Type-check (non-providers): run the prek hook — prek run mypy-<project> --all-files (e.g. mypy-airflow-core, mypy-task-sdk, mypy-shared-logging; each shared/<dist> workspace member has its own mypy-shared-<dist> hook). The hook uses a dedicated virtualenv and mypy cache under .build/mypy-venvs/<hook>/ and .build/mypy-caches/<hook>/; mypy itself is installed from uv.lock via the mypy dependency group (uv sync --group mypy), so it never mutates your project .venv. Clear with breeze down --cleanup-mypy-cache.
  • Type-check (providers): breeze run mypy path/to/code
  • Lint with ruff only: prek run ruff --from-ref <target_branch>
  • Format with ruff only: prek run ruff-format --from-ref <target_branch>
  • Run regular (fast) static checks: prek run --from-ref <target_branch> --stage pre-commit
  • Run manual (slower) checks: prek run --from-ref <target_branch> --stage manual
  • Build docs: breeze build-docs
  • Determine which tests to run based on changed files: breeze selective-checks --commit-ref <commit_with_squashed_changes>

SQLite is the default backend. Use --backend postgres or --backend mysql for integration tests that need those databases. If Docker networking fails, run docker network prune.

Repository Structure

UV workspace monorepo. Key paths:

  • airflow-core/src/airflow/ — core scheduler, API, CLI, models
    • models/ — SQLAlchemy models (DagModel, TaskInstance, DagRun, Asset, etc.)
    • jobs/ — scheduler, triggerer, Dag processor runners
    • api_fastapi/core_api/ — public REST API v2, UI endpoints
    • api_fastapi/execution_api/ — task execution communication API
    • dag_processing/ — Dag parsing and validation
    • cli/ — command-line interface
    • ui/ — React/TypeScript web interface (Vite)
  • task-sdk/ — lightweight SDK for Dag authoring and task execution runtime
    • src/airflow/sdk/execution_time/ — task runner, supervisor
  • providers/ — 100+ provider packages, each with its own pyproject.toml
  • airflow-ctl/ — management CLI tool
  • chart/ — Helm chart for Kubernetes deployment
  • dev/ — development utilities and scripts used to bootstrap the environment, releases, breeze dev env
  • scripts/ — utility scripts for CI, Docker, and prek hooks (workspace distribution apache-airflow-scripts)
    • ci/prek/ — prek (pre-commit) hook scripts; shared utilities in common_prek_utils.py
    • tests/ — pytest tests for the scripts; run with uv run --project scripts pytest scripts/tests/

The uv.lock file is generated by uv lock, uv sync and is committed to the repo - it contains snapshot of versions of all dependencies used for development of Airflow. If at any point in time you have a conflict with uv.lock, simply delete it and run uv lock to regenerate it.

Architecture Boundaries

  1. Users author Dags with the Task SDK (airflow.sdk).
  2. Dag File Processor parses Dag files in separate processes and stores serialized Dags in the metadata DB. Software guards prevent individual parsing processes from accessing the database directly and enforce use of the Execution API, but these guards do not protect against intentional bypassing by malicious or misconfigured code.
  3. Scheduler reads serialized Dags — never runs user code — and creates Dag runs / task instances.
  4. Workers execute tasks via Task SDK and communicate with the API server through the Execution API — never access the metadata DB directly. Each task receives a short-lived JWT token scoped to its task instance ID.
  5. API Server serves the React UI and handles all client-database interactions.
  6. Triggerer evaluates deferred tasks/sensors in separate processes. Like the Dag File Processor, software guards steer it through the Execution API rather than direct database access, but these guards do not protect against intentional bypassing by malicious or misconfigured code.
  7. Shared libraries that are symbolically linked to different Python distributions are in shared folder.
  8. Airflow uses uv workspace feature to keep all the distributions sharing dependencies and venv
  9. Each of the distributions should declare other needed distributions: uv --project <FOLDER> sync command acts on the selected project in the monorepo with only dependencies that it has

Security Model

When reviewing code, writing security documentation, or performing security research, keep in mind the following aspects of Airflow's security model. The authoritative reference is airflow-core/docs/security/security_model.rst and airflow-core/docs/security/jwt_token_authentication.rst.

Particularly, the intentional design choices, that are not security vulnerabilities and should not be reported as such are described in "What is NOT considered a security vulnerability" chapter of the security model.

When flagging security concerns, distinguish between:

  1. Actual vulnerabilities — code that violates the documented security model (e.g., a worker gaining database access it shouldn't have, a Scheduler executing user code, an unauthenticated user accessing protected endpoints).
  2. Known limitations — documented gaps where the current implementation doesn't provide full isolation (e.g., DFP/Triggerer database access, shared Execution API resources, multi-team not enforcing task-level isolation). These are tracked for improvement in future versions and should not be reported as new findings.
  3. Deployment hardening opportunities — measures a Deployment Manager can take to improve isolation beyond what Airflow enforces natively (e.g., per-component configuration, asymmetric JWT keys, network policies). These belong in deployment guidance, not as code-level issues.

Shared libraries

  • shared libraries provide implementation of some common utilities like logging, configuration where the code should be reused in different distributions (potentially in different versions)
  • we have a number of shared libraries that are separate, small Python distributions located under shared folder
  • each of the libraries has it's own src, tests, pyproject.toml and dependencies
  • sources of those libraries are symbolically linked to the distributions that are using them (airflow-core, task-sdk for example)
  • tests for the libraries (internal) are in the shared distribution's test and can be run from the shared distributions
  • tests of the consumers using the shared libraries are present in the distributions that use the libraries and can be run from there

Coding Standards

  • Always format and check Python files with ruff immediately after writing or editing them: uv run ruff format <file_path> and uv run ruff check --fix <file_path>. Do this for every Python file you create or modify, before moving on to the next step.
  • No assert in production code.
  • time.monotonic() for durations, not time.time().
  • In airflow-core, functions with a session parameter must not call session.commit(). Use keyword-only session parameters.
  • Imports at top of file. Valid exceptions: circular imports, lazy loading for worker isolation, TYPE_CHECKING blocks.
  • Guard heavy type-only imports (e.g., kubernetes.client) with TYPE_CHECKING in multi-process code paths.
  • Define dedicated exception classes or use existing exceptions such as ValueError instead of raising the broad AirflowException directly. Each error case should have a specific exception type that conveys what went wrong.
  • Apache License header on all new files (prek enforces this).
  • Newsfragments are only added if a major change or breaking change is applied. This is usually coordinate during review. Please do not add newsfragments per default as in most cases this needs a reversion during review.

Testing Standards

  • Add tests for new behavior — cover success, failure, and edge cases.
  • Use pytest patterns, not unittest.TestCase.
  • Use spec/autospec when mocking.
  • Use time_machine for time-dependent tests. Do not use datetime.now()
  • Use @pytest.mark.parametrize for multiple similar inputs.
  • Use @pytest.mark.db_test for tests that require database access.
  • Test fixtures: devel-common/src/tests_common/pytest_plugin.py.
  • Test location mirrors source: airflow/cli/cli_parser.py → tests/cli/test_cli_parser.py.
  • Do not use caplog in tests, prefer checking logic and not log output.

Commits and PRs

Write commit messages focused on user impact, not implementation details.

  • Good: Fix airflow dags test command failure without serialized Dags
  • Good: UI: Fix Grid view not refreshing after task actions
  • Bad: Initialize DAG bundles in CLI get_dag function

Add a newsfragment for user-visible changes: echo "Brief description" > airflow-core/newsfragments/{PR_NUMBER}.{bugfix|feature|improvement|doc|misc|significant}.rst

  • NEVER add Co-Authored-By with yourself as co-author of the commit. Agents cannot be authors, humans can be, Agents are assistants.

Creating Pull Requests

Always push to the user's fork, not to the upstream apache/airflow repo. Never push directly to main.

Before pushing, determine the fork remote. Check git remote -v — if origin does not point to apache/airflow, use origin (it's the user's fork). If origin points to apache/airflow, look for another remote that points to the user's fork. If no fork remote exists, create one:

gh repo fork apache/airflow --remote --remote-name fork

Before pushing, perform a self-review of your changes following the Gen-AI review guidelines in contributing-docs/05_pull_requests.rst and the code review checklist in .github/instructions/code-review.instructions.md:

  1. Review the full diff (git diff main...HEAD) and verify every change is intentional and related to the task — remove any unrelated changes.
  2. Read .github/instructions/code-review.instructions.md and check your diff against every rule — architecture boundaries, database correctness, code quality, testing requirements, API correctness, and AI-generated code signals. Fix any violations before pushing.
  3. Confirm the code follows the project's coding standards and architecture boundaries described in this file.
  4. Run regular (fast) static checks (prek run --from-ref <target_branch> --stage pre-commit) and fix any failures. This includes mypy checks for non-provider projects (airflow-core, task-sdk, airflow-ctl, dev, scripts, devel-common).
  5. Run manual (slower) checks (prek run --from-ref <target_branch> --stage manual) and fix any failures.
  6. Run relevant individual tests and confirm they pass.
  7. Find which tests to run for the changes with selective-checks and run those tests in parallel to confirm they pass and check for CI-specific issues.
  8. Check for security issues — no secrets, no injection vulnerabilities, no unsafe patterns.

Before pushing, always rebase your branch onto the latest target branch (usually main) to avoid merge conflicts and ensure CI runs against up-to-date code:

git fetch <upstream-remote> <target_branch>
git rebase <upstream-remote>/<target_branch>

If there are conflicts, resolve them and continue the rebase. If the rebase is too complex, ask the user for guidance.

Then push the branch to the fork remote and open the PR creation page in the browser with the body pre-filled (including the generative AI disclosure already checked):

git push -u <fork-remote> <branch-name>
gh pr create --web --title "Short title (under 70 chars)" --body "$(cat <<'EOF'
Brief description of the changes.

closes: #ISSUE  (if applicable)

---

##### Was generative AI tooling used to co-author this PR?

- [X] Yes — <Agent Name and Version>

Generated-by: <Agent Name and Version> following [the guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)

EOF
)"

The --web flag opens the browser so the user can review and submit. The --body flag pre-fills the PR template with the generative AI disclosure already completed.

Remind the user to:

  1. Review the PR title — keep it short (under 70 chars) and focused on user impact.
  2. Add a brief description of the changes at the top of the body.
  3. Reference related issues when applicable (closes: #ISSUE or related: #ISSUE).

Boundaries

  • Ask first
    • Large cross-package refactors.
    • New dependencies with broad impact.
    • Destructive data or migration changes.
  • Never
    • Commit secrets, credentials, or tokens.
    • Edit generated files by hand when a generation workflow exists.
    • Use destructive git operations unless explicitly requested.

References