Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,17 @@ repos:
^airflow-core/src/airflow/ui/public/i18n/locales/en/.*\.json$|
^\.github/skills/airflow-translations/SKILL\.md$
pass_filenames: false
- id: generate-agent-skills
name: Sync AGENTS.md commands from contributing docs
entry: ./scripts/ci/prek/generate_agent_skills.py
language: python
files: >
(?x)
^contributing-docs/.*\.rst$|
^AGENTS\.md$|
^scripts/ci/prek/generate_agent_skills\.py$
pass_filenames: false
additional_dependencies: ['pyyaml', 'rich>=13.6.0']
- id: update-pyproject-toml
name: Update Airflow's meta-package pyproject.toml
language: python
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Don't spell out **Directed Acyclic Graph** except for historical context.
`<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.

<!-- START generated-commands, please keep comment here to allow auto update -->
- **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`
Expand All @@ -54,6 +55,7 @@ Don't spell out **Directed Acyclic Graph** except for historical context.
- **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>`
<!-- END generated-commands, please keep comment here to allow auto update -->

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`.

Expand Down
14 changes: 14 additions & 0 deletions contributing-docs/08_static_code_checks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,20 @@ You can always skip running the tests by providing ``--no-verify`` flag to the

To check other usage types of the pre-commit framework, see `Pre-commit website <https://pre-commit.com/>`__.

.. AGENT-SKILL-START
type: agents-md-commands
order: 30
lines:
- "- **Type-check (non-providers):** run the prek hook \u2014 `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`. The hook prefers `uv` from the project's main `.venv/bin/uv` (installed by `uv sync` \u2014 `uv` is part of the `dev` dependency group via the `all` extras) for a project-pinned uv version; it falls back to `uv` on `$PATH` with a warning if that binary is missing. 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>`"
.. AGENT-SKILL-END

Disabling particular checks
---------------------------

Expand Down
27 changes: 27 additions & 0 deletions contributing-docs/testing/unit_tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ rerun in Breeze as needed (``-n auto`` will parallelize tests using the ``pytest
breeze shell --backend none --python 3.10
> pytest airflow-core/tests --skip-db-tests -n auto

.. AGENT-SKILL-START
type: agents-md-commands
order: 10
lines:
- "- **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`"
.. AGENT-SKILL-END

Airflow DB tests
................

Expand Down Expand Up @@ -278,6 +289,22 @@ As explained before, you cannot run DB tests in parallel using the ``pytest-xdis

breeze testing core-tests --run-db-tests-only --backend postgres --python 3.10 --run-in-parallel

.. AGENT-SKILL-START
type: agents-md-commands
order: 20
lines:
- "- **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`"
.. AGENT-SKILL-END

Examples of marking test as DB test
...................................

Expand Down
110 changes: 110 additions & 0 deletions scripts/ci/prek/generate_agent_skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Sync the AGENTS.md Commands section from contributing docs.

Contributing docs contain ``.. AGENT-SKILL-START`` / ``.. AGENT-SKILL-END``
RST comment blocks with pre-formatted AGENTS.md bullet lines. This script
collects those blocks, sorts them by ``order``, and updates the
generated-commands section in AGENTS.md via ``insert_documentation()``.

The prek hook runs this automatically when contributing docs or AGENTS.md
change.
"""

from __future__ import annotations

import re
import sys
from dataclasses import dataclass
from pathlib import Path

import yaml
from common_prek_utils import AIRFLOW_ROOT_PATH, console, insert_documentation

CONTRIBUTING_DOCS_ROOT = AIRFLOW_ROOT_PATH / "contributing-docs"
AGENTS_MD_PATH = AIRFLOW_ROOT_PATH / "AGENTS.md"
AGENTS_MD_START = "<!-- START generated-commands, please keep comment here to allow auto update -->"
AGENTS_MD_END = "<!-- END generated-commands, please keep comment here to allow auto update -->"
BLOCK_RE = re.compile(r"\.\. AGENT-SKILL-START\n(.*?)\.\. AGENT-SKILL-END", re.DOTALL)


@dataclass(frozen=True)
class CommandBlock:
order: int
lines: tuple[str, ...]


def _dedent_rst_comment_body(raw: str) -> str:
"""Remove the 3-space RST comment indentation from each line."""
lines = raw.splitlines()
return "\n".join(line[3:] if line.startswith(" ") else line for line in lines).strip()


def _parse_blocks_from_rst(rst_path: Path) -> list[CommandBlock]:
"""Extract all AGENT-SKILL command blocks from a single RST file."""
text = rst_path.read_text()
blocks: list[CommandBlock] = []
for match in BLOCK_RE.finditer(text):
block = yaml.safe_load(_dedent_rst_comment_body(match.group(1)))
if not isinstance(block, dict) or block.get("type") != "agents-md-commands":
continue
order = int(block.get("order", 100))
raw_lines = block.get("lines", [])
lines = tuple(str(line).rstrip() for line in raw_lines)
blocks.append(CommandBlock(order=order, lines=lines))
return blocks


def collect_blocks(
contributing_docs_root: Path = CONTRIBUTING_DOCS_ROOT,
) -> list[CommandBlock]:
"""Scan all RST files and return command blocks sorted by order."""
blocks: list[CommandBlock] = []
for rst_path in sorted(contributing_docs_root.rglob("*.rst")):
blocks.extend(_parse_blocks_from_rst(rst_path))
blocks.sort(key=lambda b: b.order)
return blocks


def render_lines(blocks: list[CommandBlock]) -> list[str]:
"""Flatten all blocks into a list of lines for insert_documentation()."""
result: list[str] = []
for block in blocks:
for line in block.lines:
result.append(line + "\n")
return result


def main() -> int:
blocks = collect_blocks()
if not blocks:
console.print("[red]No AGENT-SKILL blocks found in contributing docs.[/red]")
return 1
content = render_lines(blocks)
insert_documentation(
file_path=AGENTS_MD_PATH,
content=content,
header=AGENTS_MD_START,
footer=AGENTS_MD_END,
extra_information="AGENTS.md commands from contributing docs",
)
return 0


if __name__ == "__main__":
sys.exit(main())
47 changes: 47 additions & 0 deletions scripts/tests/ci/prek/test_generate_agent_skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from ci.prek.generate_agent_skills import (
collect_blocks,
render_lines,
)


def test_collect_blocks_finds_all_markers():
blocks = collect_blocks()

assert len(blocks) >= 3


def test_collect_blocks_sorted_by_order():
blocks = collect_blocks()

orders = [b.order for b in blocks]
assert orders == sorted(orders)


def test_render_lines_produces_bullet_list():
blocks = collect_blocks()
lines = render_lines(blocks)

assert len(lines) > 0
assert all(line.startswith("- **") for line in lines)
assert any("uv run --project <PROJECT> pytest" in line for line in lines)
assert any("breeze testing helm-tests" in line for line in lines)
assert any("prek run mypy-<project>" in line for line in lines)
assert any("breeze selective-checks" in line for line in lines)