This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new c688a6f0abc Add breeze verify to list local checks for changed files
(#73241)
c688a6f0abc is described below
commit c688a6f0abc79d7a54bb5a2ef4276cd49ba27b42
Author: Andrew Chang <[email protected]>
AuthorDate: Sun Oct 4 10:26:27 2026 +0800
Add breeze verify to list local checks for changed files (#73241)
* Add breeze verify to show CI-required local verification for changed files
The self-review checklist in AGENTS.md (step 7) asks contributors to find
the tests selective-checks would run and run them locally. The only tool
for that is `breeze ci selective-check`, which looks at a single commit
and prints GitHub Actions outputs, so a contributor still has to read the
workflow files to turn those outputs into commands. This adds a
developer-facing, dry-run command that feeds the working-tree diff to the
same SelectiveChecks logic and prints the commands for the default CI
matrix cell, with the rest explicitly left to CI.
* Print the CI reproduction banner on stderr so breeze verify --json stays
parseable
In GitHub Actions breeze appends a "how to reproduce locally" banner after
every command. It went to stdout, so any command whose stdout is a machine
contract, like `breeze verify --json`, produced JSON followed by the banner.
The banner is a log aid, so stderr is the right stream for it.
* Let a breeze command opt out of the CI reproduction banner instead of
moving it to stderr
Moving the banner to stderr broke the build-info job: `breeze ci
get-workflow-info` and `breeze ci selective-check` write their GitHub
Actions outputs on stderr (`2>> $GITHUB_OUTPUT`), so the banner ended up
in the outputs file. Neither stream is safe for every command. Keep the
banner on stdout and let a command whose stdout is a machine contract,
here `breeze verify --json`, switch it off through the click context.
* List renamed files the way CI does when computing breeze verify changed
files
CI feeds selective checks the output of `git diff-tree`, which has no
rename detection and reports a rename as the old path deleted plus the
new path added. `git diff --name-only` detects renames by default and
lists only the new path, so a rule matching the old path would not fire
locally although it fires in CI.
* Only list jobs triggered by the change in breeze verify
Breeze's own unit tests, the shared distribution tests and the translation
check run on every PR regardless of what changed, so listing them on every
run adds rows nobody acts on and pushes the relevant ones down. Now every
row comes from a SelectiveChecks output. The `required` column goes with
them: with the translation check gone it was always true.
* Make breeze verify list what to run by default and keep the CI expansion
behind --full
When a change touches CI tooling or dependency files, selective checks
make CI run the whole suite. That is the right call for CI, but an agent
that executes the list literally would run Go, Java and every provider
for a breeze change. The default now applies the same selective-checks
rules without that expansion, notes that CI will run more, and --full
lists the whole default matrix cell.
* Fold long commands in the breeze verify table instead of truncating them
A truncated command cannot be copied, and the prek row with its SKIP list
is longer than most terminals.
* Print breeze verify commands one per line instead of in a table
A table cell is not something you can copy: long commands got truncated,
and folding them only spread one command over several lines. Each command
is now on its own unwrapped line under a comment saying what kind of check
it is and where it runs, so a triple-click selects the whole command.
* Keep the table output in breeze verify
Reverts the one-command-per-line output. The table is easier to read, and
anyone who wants to copy commands has --json.
* List the jobs CI runs on every PR under breeze verify --full
--full means everything CI runs for the default matrix cell, so breeze's
own unit tests and the shared distribution tests belong there even though
no change triggers them. The default list still leaves them out. The
translation check stays out of both: CI runs it with `|| true`.
* Address review: guard hook names, use --from-ref by default, point agents
at breeze verify
The manual-stage hook names in the mapping are now checked against
ci-amd.yml, so a renamed hook fails a test instead of going stale. The
default prek row uses --from-ref, the scope AGENTS.md recommends, and
--full keeps CI's --all-files form. Step 7 of the publish-changes skill,
the AGENTS.md commands block and the selective-checks doc now point at
breeze verify. All patches in the CLI tests use autospec.
* Classify breeze verify flags from the workflow gates instead of the run_
prefix
The classification test used to enumerate SelectiveChecks properties named
run_* and assume those were the job gates, so a gate with another name
would slip through. It now scans every `if:` in .github/workflows/ for
the selective-checks outputs CI actually gates on, which also drops the
hand-kept list of run_* outputs no job reads.
* Scan complete multiline workflow conditions
* Keep breeze verify classification focused on run flags
* Regenerate breeze command images and skill-eval hash after rebase
* Leave static checks to prek in breeze verify
prek already picks the hooks to run for the changed files, so breeze
verify no longer lists a prek row, in either mode. The mypy-providers
and migration-round-trip jobs are manual-stage prek hooks too; their
selective-checks flags are now classified in PREK_JOBS and not printed.
The output, the --full help, the breeze docs, AGENTS.md and the
publish-changes skill say static checks are not listed.
Also classify run_agent_framework_tests, added to SelectiveChecks on
main, with the breeze shell commands its CI job runs per framework.
* Compare breeze verify with main on the apache/airflow remote by default
The default --base-ref was the local main branch. In a fork it is often
behind the main the branch has merged (a GitHub "Update branch" or a
conflict fix), and then every merged-in commit counted as a change:
3287 files instead of the 21 the PR changes.
The default is now main on the remote whose URL points at apache/airflow,
preferring upstream, which is the base GitHub compares the PR with.
Without such a remote it falls back to the local main and says so.
The local copy of that remote can be stale too, after "Update branch"
without a fetch. breeze verify now warns when a merge on the branch
brought in commits the base does not have. With --json the warnings go
to stderr so stdout stays parseable.
* Point breeze verify at the prek hooks a default install does not run
Dropping the static-check rows also hid mypy-providers and
migration-round-trip, which CI runs but a default prek install never triggers
(pre-push and manual stage), so a migration change got no hint until CI failed.
The CLI tests also ran real git against the developer's checkout and global
config.
Generated-by: Claude Opus 5
---------
Co-authored-by: Jarek Potiuk <[email protected]>
---
.agents/skills/airflow-publish-changes/SKILL.md | 2 +-
AGENTS.md | 1 +
contributing-docs/08_static_code_checks.rst | 1 +
dev/breeze/doc/03_developer_tasks.rst | 55 ++++
dev/breeze/doc/ci/04_selective_checks.md | 5 +
dev/breeze/doc/images/output-commands.svg | 68 +++--
.../output_setup_check-all-params-in-groups.svg | 4 +-
.../output_setup_check-all-params-in-groups.txt | 2 +-
.../output_setup_regenerate-command-images.svg | 4 +-
.../output_setup_regenerate-command-images.txt | 2 +-
dev/breeze/doc/images/output_verify.svg | 128 ++++++++
dev/breeze/doc/images/output_verify.txt | 1 +
dev/breeze/src/airflow_breeze/breeze.py | 1 +
.../commands/developer_commands_config.py | 11 +
.../src/airflow_breeze/commands/verify_commands.py | 180 ++++++++++++
.../src/airflow_breeze/utils/reproduce_ci.py | 10 +-
.../src/airflow_breeze/utils/verification_plan.py | 170 +++++++++++
dev/breeze/tests/test_reproduce_ci.py | 12 +
dev/breeze/tests/test_verification_plan.py | 244 +++++++++++++++
dev/breeze/tests/test_verify_commands.py | 327 +++++++++++++++++++++
dev/skill-evals/last-eval-hash.txt | 2 +-
21 files changed, 1188 insertions(+), 42 deletions(-)
diff --git a/.agents/skills/airflow-publish-changes/SKILL.md
b/.agents/skills/airflow-publish-changes/SKILL.md
index cc4fe94beb7..8f115bea26f 100644
--- a/.agents/skills/airflow-publish-changes/SKILL.md
+++ b/.agents/skills/airflow-publish-changes/SKILL.md
@@ -115,7 +115,7 @@ code review checklist in
[`.github/instructions/code-review.instructions.md`](..
and fix any failures. The skipped hooks start long-running local servers or
provision the
opt-in Codex environment rather than run checks that complete.
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.
+7. Run `breeze verify` and execute every command it lists. The list comes from
the same selective-checks logic CI uses. `breeze verify --full` shows
everything CI runs for the default matrix cell, including the full suite CI
adds when a change touches CI tooling or dependency files. Static checks are
not listed: steps 4 and 5 cover them. Confirm the commands 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`)
diff --git a/AGENTS.md b/AGENTS.md
index f7a6a9b5fb7..7685fb036af 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -54,6 +54,7 @@ Don't spell out **Directed Acyclic Graph** except for
historical context.
- **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 --skip compile-ui-assets-dev --skip view-skill-eval --skip
run-skill-eval-codex` (the skipped hooks start long-running local servers or
provision the opt-in Codex environment rather than run checks that complete)
- **Build docs:** `breeze build-docs`
+- **List the local verification for your changes:** `breeze verify` (add
`--full` for everything CI runs for the default matrix cell except static
checks, `--json` for machine-readable output). It uses the same
selective-checks logic as CI.
- **Determine which tests to run based on changed files:** `breeze ci
selective-check --commit-ref <commit_with_squashed_changes>`
<!-- END generated-commands, please keep comment here to allow auto update -->
diff --git a/contributing-docs/08_static_code_checks.rst
b/contributing-docs/08_static_code_checks.rst
index 9bae6b9248f..9c34dfd16b1 100644
--- a/contributing-docs/08_static_code_checks.rst
+++ b/contributing-docs/08_static_code_checks.rst
@@ -236,6 +236,7 @@ To check other usage types of the pre-commit framework, see
`Pre-commit website
- "- **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 --skip compile-ui-assets-dev --skip view-skill-eval --skip
run-skill-eval-codex` (the skipped hooks start long-running local servers or
provision the opt-in Codex environment rather than run checks that complete)"
- "- **Build docs:** `breeze build-docs`"
+ - "- **List the local verification for your changes:** `breeze verify`
(add `--full` for everything CI runs for the default matrix cell except static
checks, `--json` for machine-readable output). It uses the same
selective-checks logic as CI."
- "- **Determine which tests to run based on changed files:** `breeze ci
selective-check --commit-ref <commit_with_squashed_changes>`"
.. AGENT-SKILL-END
diff --git a/dev/breeze/doc/03_developer_tasks.rst
b/dev/breeze/doc/03_developer_tasks.rst
index 56b8bbbb8ba..b94b4b01727 100644
--- a/dev/breeze/doc/03_developer_tasks.rst
+++ b/dev/breeze/doc/03_developer_tasks.rst
@@ -316,6 +316,61 @@ If the provider name is
``apache-airflow-providers-cncf-kubernetes``, it will be
Note: For building docs for apache-airflow-providers index, use
``apache-airflow-providers``
as the short hand operator.
+Finding out what CI will run for your change
+--------------------------------------------
+
+``breeze verify`` reads the files changed against the target branch (the
merge-base of
+``--base-ref`` and ``HEAD``, plus untracked files), runs the same
selective-checks logic CI uses
+and lists the commands to run locally for the change. Nothing is executed. The
only non-zero exit
+is a usage error such as a base ref git cannot resolve.
+
+By default ``--base-ref`` is ``main`` on the git remote that points at
apache/airflow (``upstream``
+if several do), the same base GitHub compares a PR with. A local ``main`` that
is behind the
+``main`` your branch has merged would count every merged-in commit as your
change. Without such a
+remote, ``breeze verify`` falls back to the local ``main`` branch and says so.
It also warns when
+the branch has merged commits the base does not have, for example after
GitHub's "Update branch"
+when the remote was not fetched since. With ``--json`` the warnings go to
stderr.
+
+.. code-block:: bash
+
+ breeze verify
+ breeze verify --json
+ breeze verify --full
+ breeze verify --base-ref main
+
+Each row says what kind of check it is, whether it runs on the host or needs
Docker and the
+CI image (``breeze``), and the exact command. Jobs CI runs on every PR
regardless of the change
+(breeze's own unit tests, the shared distributions) only show up with
``--full``. The translation
+check is never listed: CI runs it with ``|| true``, so it cannot fail a PR.
+Static checks are not listed either: ``prek`` already picks the hooks to run
for the changed files,
+so run it as usual. The exception is the ``mypy-providers`` and
``migration-round-trip`` hooks,
+which CI runs but a default ``prek install`` does not (they are ``pre-push``
or ``manual`` stage
+hooks). When the change triggers them, ``breeze verify`` prints the
+``prek run --stage manual <hook> --from-ref <base>`` command to run each one,
and ``--json``
+lists them under ``manual_prek_hooks``.
+Use ``--json`` for machine-readable output with the same fields.
+
+When a change touches CI tooling or dependency files, selective checks make CI
run the full suite.
+The default list leaves that expansion out and only shows what the changed
files match themselves,
+with a note that CI will run more. ``--full`` lists everything CI runs for the
default matrix cell
+(default Python, sqlite), apart from static checks.
+
+Even ``--full`` is not the full CI matrix. Other Python versions, Postgres and
MySQL,
+lowest-dependency runs, Kubernetes, Helm, e2e suites, the provider
compatibility matrix and ARM
+runners only run in CI. Two more caveats. Selective checks compare
``pyproject.toml`` contents
+between ``HEAD`` and ``HEAD^`` only, so dependency changes that are
uncommitted or in earlier
+commits of your branch are not detected as such (test selection is unaffected,
only the
+dependency-bump checks are). Packaging steps CI runs around some tests
(building and
+twine-checking the Task SDK and airflow-ctl wheels, regenerating the Python
API client from its
+own repository) are not listed.
+
+These are all available flags of ``verify`` command:
+
+.. image:: ./images/output_verify.svg
+ :target:
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_verify.svg
+ :width: 100%
+ :alt: Breeze verify
+
Running static checks
---------------------
diff --git a/dev/breeze/doc/ci/04_selective_checks.md
b/dev/breeze/doc/ci/04_selective_checks.md
index f4f0ce8badf..5d96ed6d910 100644
--- a/dev/breeze/doc/ci/04_selective_checks.md
+++ b/dev/breeze/doc/ci/04_selective_checks.md
@@ -50,6 +50,11 @@ contributors in case of simpler changes.
> [`dev/breeze/src/airflow_breeze/utils/selective_checks.py`](../../src/airflow_breeze/utils/selective_checks.py).
> When you change that file, **update this document in the same PR** so the
> behaviour and the
> documentation stay in sync.
+>
+> `breeze verify` maps the `run_*` flags to local commands in
+>
[`dev/breeze/src/airflow_breeze/utils/verification_plan.py`](../../src/airflow_breeze/utils/verification_plan.py).
+> Adding a `run_*` flag to `SelectiveChecks` also means classifying it there;
+> `dev/breeze/tests/test_verification_plan.py` fails until you do.
## Why selective checks exist (the optimisation goal)
diff --git a/dev/breeze/doc/images/output-commands.svg
b/dev/breeze/doc/images/output-commands.svg
index 13418f6e0a6..e6405bcad55 100644
--- a/dev/breeze/doc/images/output-commands.svg
+++ b/dev/breeze/doc/images/output-commands.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 2538.7999999999997"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 2563.2"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -44,7 +44,7 @@
<defs>
<clipPath id="breeze-help-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="2487.7999999999997" />
+ <rect x="0" y="0" width="1463.0" height="2512.2" />
</clipPath>
<clipPath id="breeze-help-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -349,9 +349,12 @@
<clipPath id="breeze-help-line-100">
<rect x="0" y="2441.5" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-help-line-101">
+ <rect x="0" y="2465.9" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="2536.8" rx="8"/><text
class="breeze-help-title" fill="#c5c8c6" text-anchor="middle" x="740"
y="27">Breeze commands</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="2561.2" rx="8"/><text
class="breeze-help-title" fill="#c5c8c6" text-anchor="middle" x="740"
y="27">Breeze commands</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -434,35 +437,36 @@
</text><text class="breeze-help-r5" x="0" y="1728" textLength="12.2"
clip-path="url(#breeze-help-line-70)">│</text><text class="breeze-help-r1"
x="329.4" y="1728" textLength="1110.2"
clip-path="url(#breeze-help-line-70)">images.                                            
[...]
</text><text class="breeze-help-r5" x="0" y="1752.4" textLength="12.2"
clip-path="url(#breeze-help-line-71)">│</text><text class="breeze-help-r4"
x="24.4" y="1752.4" textLength="280.6"
clip-path="url(#breeze-help-line-71)">generate-migration-file</text><text
class="breeze-help-r1" x="329.4" y="1752.4" textLength="1110.2"
clip-path="url(#breeze-help-line-71)">Autogenerate the alembic migration file for the ORM changes.     &
[...]
</text><text class="breeze-help-r5" x="0" y="1776.8" textLength="12.2"
clip-path="url(#breeze-help-line-72)">│</text><text class="breeze-help-r4"
x="24.4" y="1776.8" textLength="280.6"
clip-path="url(#breeze-help-line-72)">doctor                 </text><text
class="breeze-help-r1" x="329.4" y="1776.8" textLength="1110.2"
clip-path="url(#breeze-help-line-72)">Auto-healing of breeze  &#
[...]
-</text><text class="breeze-help-r5" x="0" y="1801.2" textLength="1464"
clip-path="url(#breeze-help-line-73)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="1801.2" textLength="12.2"
clip-path="url(#breeze-help-line-73)">
-</text><text class="breeze-help-r5" x="0" y="1825.6" textLength="24.4"
clip-path="url(#breeze-help-line-74)">╭─</text><text class="breeze-help-r5"
x="24.4" y="1825.6" textLength="219.6"
clip-path="url(#breeze-help-line-74)"> Testing commands </text><text
class="breeze-help-r5" x="244" y="1825.6" textLength="1195.6"
clip-path="url(#breeze-help-line-74)">──────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="1850" textLength="12.2"
clip-path="url(#breeze-help-line-75)">│</text><text class="breeze-help-r4"
x="24.4" y="1850" textLength="183"
clip-path="url(#breeze-help-line-75)">testing        </text><text
class="breeze-help-r1" x="231.8" y="1850" textLength="1207.8"
clip-path="url(#breeze-help-line-75)">Tools that developers can use to run tests     
[...]
-</text><text class="breeze-help-r5" x="0" y="1874.4" textLength="12.2"
clip-path="url(#breeze-help-line-76)">│</text><text class="breeze-help-r4"
x="24.4" y="1874.4" textLength="183"
clip-path="url(#breeze-help-line-76)">k8s            </text><text
class="breeze-help-r1" x="231.8" y="1874.4" textLength="1207.8"
clip-path="url(#breeze-help-line-76)">Tools that developers use to run Kubernetes te
[...]
-</text><text class="breeze-help-r5" x="0" y="1898.8" textLength="1464"
clip-path="url(#breeze-help-line-77)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="1898.8" textLength="12.2"
clip-path="url(#breeze-help-line-77)">
-</text><text class="breeze-help-r5" x="0" y="1923.2" textLength="24.4"
clip-path="url(#breeze-help-line-78)">╭─</text><text class="breeze-help-r5"
x="24.4" y="1923.2" textLength="195.2"
clip-path="url(#breeze-help-line-78)"> Image commands </text><text
class="breeze-help-r5" x="219.6" y="1923.2" textLength="1220"
clip-path="url(#breeze-help-line-78)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="1947.6" textLength="12.2"
clip-path="url(#breeze-help-line-79)">│</text><text class="breeze-help-r4"
x="24.4" y="1947.6" textLength="207.4"
clip-path="url(#breeze-help-line-79)">ci-image         </text><text
class="breeze-help-r1" x="256.2" y="1947.6" textLength="1183.4"
clip-path="url(#breeze-help-line-79)">Tools that developers can use to manually manage CI&
[...]
-</text><text class="breeze-help-r5" x="0" y="1972" textLength="12.2"
clip-path="url(#breeze-help-line-80)">│</text><text class="breeze-help-r4"
x="24.4" y="1972" textLength="207.4"
clip-path="url(#breeze-help-line-80)">prod-image       </text><text
class="breeze-help-r1" x="256.2" y="1972" textLength="1183.4"
clip-path="url(#breeze-help-line-80)">Tools that developers can use to manually manage PROD images
[...]
-</text><text class="breeze-help-r5" x="0" y="1996.4" textLength="1464"
clip-path="url(#breeze-help-line-81)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="1996.4" textLength="12.2"
clip-path="url(#breeze-help-line-81)">
-</text><text class="breeze-help-r5" x="0" y="2020.8" textLength="24.4"
clip-path="url(#breeze-help-line-82)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2020.8" textLength="353.8"
clip-path="url(#breeze-help-line-82)"> Release management commands </text><text
class="breeze-help-r5" x="378.2" y="2020.8" textLength="1061.4"
clip-path="url(#breeze-help-line-82)">───────────────────────────────────────────────────────────────────────────────────────</text><text
clas [...]
-</text><text class="breeze-help-r5" x="0" y="2045.2" textLength="12.2"
clip-path="url(#breeze-help-line-83)">│</text><text class="breeze-help-r4"
x="24.4" y="2045.2" textLength="280.6"
clip-path="url(#breeze-help-line-83)">release-management     </text><text
class="breeze-help-r1" x="329.4" y="2045.2" textLength="1110.2"
clip-path="url(#breeze-help-line-83)">Tools that release managers can use to prepare and manage
[...]
-</text><text class="breeze-help-r5" x="0" y="2069.6" textLength="12.2"
clip-path="url(#breeze-help-line-84)">│</text><text class="breeze-help-r4"
x="24.4" y="2069.6" textLength="280.6"
clip-path="url(#breeze-help-line-84)">sbom                   </text><text
class="breeze-help-r1" x="329.4" y="2069.6" textLength="1110.2"
clip-path="url(#breeze-help-line-84)">Tools that release ma [...]
-</text><text class="breeze-help-r5" x="0" y="2094" textLength="12.2"
clip-path="url(#breeze-help-line-85)">│</text><text class="breeze-help-r4"
x="24.4" y="2094" textLength="280.6"
clip-path="url(#breeze-help-line-85)">workflow-run           </text><text
class="breeze-help-r1" x="329.4" y="2094" textLength="1110.2"
clip-path="url(#breeze-help-line-85)">Tools to manage Airflow repository workflows  &
[...]
-</text><text class="breeze-help-r5" x="0" y="2118.4" textLength="1464"
clip-path="url(#breeze-help-line-86)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2118.4" textLength="12.2"
clip-path="url(#breeze-help-line-86)">
-</text><text class="breeze-help-r5" x="0" y="2142.8" textLength="24.4"
clip-path="url(#breeze-help-line-87)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2142.8" textLength="158.6"
clip-path="url(#breeze-help-line-87)"> CI commands </text><text
class="breeze-help-r5" x="183" y="2142.8" textLength="1256.6"
clip-path="url(#breeze-help-line-87)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2167.2" textLength="12.2"
clip-path="url(#breeze-help-line-88)">│</text><text class="breeze-help-r4"
x="24.4" y="2167.2" textLength="61"
clip-path="url(#breeze-help-line-88)">ci   </text><text
class="breeze-help-r1" x="109.8" y="2167.2" textLength="1329.8"
clip-path="url(#breeze-help-line-88)">Tools that CI workflows use to cleanup/manage CI environment      &
[...]
-</text><text class="breeze-help-r5" x="0" y="2191.6" textLength="1464"
clip-path="url(#breeze-help-line-89)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2191.6" textLength="12.2"
clip-path="url(#breeze-help-line-89)">
-</text><text class="breeze-help-r5" x="0" y="2216" textLength="24.4"
clip-path="url(#breeze-help-line-90)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2216" textLength="231.8"
clip-path="url(#breeze-help-line-90)"> Registry commands </text><text
class="breeze-help-r5" x="256.2" y="2216" textLength="1183.4"
clip-path="url(#breeze-help-line-90)">─────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="breeze-h [...]
-</text><text class="breeze-help-r5" x="0" y="2240.4" textLength="12.2"
clip-path="url(#breeze-help-line-91)">│</text><text class="breeze-help-r4"
x="24.4" y="2240.4" textLength="256.2"
clip-path="url(#breeze-help-line-91)">registry             </text><text
class="breeze-help-r1" x="305" y="2240.4" textLength="1134.6"
clip-path="url(#breeze-help-line-91)">Tools for the Airflow Provider Registry 
[...]
-</text><text class="breeze-help-r5" x="0" y="2264.8" textLength="1464"
clip-path="url(#breeze-help-line-92)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2264.8" textLength="12.2"
clip-path="url(#breeze-help-line-92)">
-</text><text class="breeze-help-r5" x="0" y="2289.2" textLength="24.4"
clip-path="url(#breeze-help-line-93)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2289.2" textLength="158.6"
clip-path="url(#breeze-help-line-93)"> UI commands </text><text
class="breeze-help-r5" x="183" y="2289.2" textLength="1256.6"
clip-path="url(#breeze-help-line-93)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2313.6" textLength="12.2"
clip-path="url(#breeze-help-line-94)">│</text><text class="breeze-help-r4"
x="24.4" y="2313.6" textLength="85.4"
clip-path="url(#breeze-help-line-94)">ui     </text><text
class="breeze-help-r1" x="134.2" y="2313.6" textLength="1305.4"
clip-path="url(#breeze-help-line-94)">Tools for UI development and maintenance         
[...]
-</text><text class="breeze-help-r5" x="0" y="2338" textLength="1464"
clip-path="url(#breeze-help-line-95)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2338" textLength="12.2"
clip-path="url(#breeze-help-line-95)">
-</text><text class="breeze-help-r5" x="0" y="2362.4" textLength="24.4"
clip-path="url(#breeze-help-line-96)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2362.4" textLength="207.4"
clip-path="url(#breeze-help-line-96)"> Issues commands </text><text
class="breeze-help-r5" x="231.8" y="2362.4" textLength="1207.8"
clip-path="url(#breeze-help-line-96)">───────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="br [...]
-</text><text class="breeze-help-r5" x="0" y="2386.8" textLength="12.2"
clip-path="url(#breeze-help-line-97)">│</text><text class="breeze-help-r4"
x="24.4" y="2386.8" textLength="231.8"
clip-path="url(#breeze-help-line-97)">issues             </text><text
class="breeze-help-r1" x="280.6" y="2386.8" textLength="1159"
clip-path="url(#breeze-help-line-97)">Tools for managing GitHub issues.   &
[...]
-</text><text class="breeze-help-r5" x="0" y="2411.2" textLength="1464"
clip-path="url(#breeze-help-line-98)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2411.2" textLength="12.2"
clip-path="url(#breeze-help-line-98)">
-</text><text class="breeze-help-r5" x="0" y="2435.6" textLength="24.4"
clip-path="url(#breeze-help-line-99)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2435.6" textLength="195.2"
clip-path="url(#breeze-help-line-99)"> Setup commands </text><text
class="breeze-help-r5" x="219.6" y="2435.6" textLength="1220"
clip-path="url(#breeze-help-line-99)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2460" textLength="12.2"
clip-path="url(#breeze-help-line-100)">│</text><text class="breeze-help-r4"
x="24.4" y="2460" textLength="146.4"
clip-path="url(#breeze-help-line-100)">setup       </text><text
class="breeze-help-r1" x="195.2" y="2460" textLength="1244.4"
clip-path="url(#breeze-help-line-100)">Tools that developers can use to configure Breeze    &#
[...]
-</text><text class="breeze-help-r5" x="0" y="2484.4" textLength="1464"
clip-path="url(#breeze-help-line-101)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2484.4" textLength="12.2"
clip-path="url(#breeze-help-line-101)">
+</text><text class="breeze-help-r5" x="0" y="1801.2" textLength="12.2"
clip-path="url(#breeze-help-line-73)">│</text><text class="breeze-help-r4"
x="24.4" y="1801.2" textLength="280.6"
clip-path="url(#breeze-help-line-73)">verify                 </text><text
class="breeze-help-r1" x="329.4" y="1801.2" textLength="1110.2"
clip-path="url(#breeze-help-line-73)">List the local verification
[...]
+</text><text class="breeze-help-r5" x="0" y="1825.6" textLength="1464"
clip-path="url(#breeze-help-line-74)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="1825.6" textLength="12.2"
clip-path="url(#breeze-help-line-74)">
+</text><text class="breeze-help-r5" x="0" y="1850" textLength="24.4"
clip-path="url(#breeze-help-line-75)">╭─</text><text class="breeze-help-r5"
x="24.4" y="1850" textLength="219.6"
clip-path="url(#breeze-help-line-75)"> Testing commands </text><text
class="breeze-help-r5" x="244" y="1850" textLength="1195.6"
clip-path="url(#breeze-help-line-75)">──────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="breeze-hel [...]
+</text><text class="breeze-help-r5" x="0" y="1874.4" textLength="12.2"
clip-path="url(#breeze-help-line-76)">│</text><text class="breeze-help-r4"
x="24.4" y="1874.4" textLength="183"
clip-path="url(#breeze-help-line-76)">testing        </text><text
class="breeze-help-r1" x="231.8" y="1874.4" textLength="1207.8"
clip-path="url(#breeze-help-line-76)">Tools that developers can use to run tests    
[...]
+</text><text class="breeze-help-r5" x="0" y="1898.8" textLength="12.2"
clip-path="url(#breeze-help-line-77)">│</text><text class="breeze-help-r4"
x="24.4" y="1898.8" textLength="183"
clip-path="url(#breeze-help-line-77)">k8s            </text><text
class="breeze-help-r1" x="231.8" y="1898.8" textLength="1207.8"
clip-path="url(#breeze-help-line-77)">Tools that developers use to run Kubernetes te
[...]
+</text><text class="breeze-help-r5" x="0" y="1923.2" textLength="1464"
clip-path="url(#breeze-help-line-78)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="1923.2" textLength="12.2"
clip-path="url(#breeze-help-line-78)">
+</text><text class="breeze-help-r5" x="0" y="1947.6" textLength="24.4"
clip-path="url(#breeze-help-line-79)">╭─</text><text class="breeze-help-r5"
x="24.4" y="1947.6" textLength="195.2"
clip-path="url(#breeze-help-line-79)"> Image commands </text><text
class="breeze-help-r5" x="219.6" y="1947.6" textLength="1220"
clip-path="url(#breeze-help-line-79)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="1972" textLength="12.2"
clip-path="url(#breeze-help-line-80)">│</text><text class="breeze-help-r4"
x="24.4" y="1972" textLength="207.4"
clip-path="url(#breeze-help-line-80)">ci-image         </text><text
class="breeze-help-r1" x="256.2" y="1972" textLength="1183.4"
clip-path="url(#breeze-help-line-80)">Tools that developers can use to manually manage CI i
[...]
+</text><text class="breeze-help-r5" x="0" y="1996.4" textLength="12.2"
clip-path="url(#breeze-help-line-81)">│</text><text class="breeze-help-r4"
x="24.4" y="1996.4" textLength="207.4"
clip-path="url(#breeze-help-line-81)">prod-image       </text><text
class="breeze-help-r1" x="256.2" y="1996.4" textLength="1183.4"
clip-path="url(#breeze-help-line-81)">Tools that developers can use to manually manage PROD ima
[...]
+</text><text class="breeze-help-r5" x="0" y="2020.8" textLength="1464"
clip-path="url(#breeze-help-line-82)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2020.8" textLength="12.2"
clip-path="url(#breeze-help-line-82)">
+</text><text class="breeze-help-r5" x="0" y="2045.2" textLength="24.4"
clip-path="url(#breeze-help-line-83)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2045.2" textLength="353.8"
clip-path="url(#breeze-help-line-83)"> Release management commands </text><text
class="breeze-help-r5" x="378.2" y="2045.2" textLength="1061.4"
clip-path="url(#breeze-help-line-83)">───────────────────────────────────────────────────────────────────────────────────────</text><text
clas [...]
+</text><text class="breeze-help-r5" x="0" y="2069.6" textLength="12.2"
clip-path="url(#breeze-help-line-84)">│</text><text class="breeze-help-r4"
x="24.4" y="2069.6" textLength="280.6"
clip-path="url(#breeze-help-line-84)">release-management     </text><text
class="breeze-help-r1" x="329.4" y="2069.6" textLength="1110.2"
clip-path="url(#breeze-help-line-84)">Tools that release managers can use to prepare and manage
[...]
+</text><text class="breeze-help-r5" x="0" y="2094" textLength="12.2"
clip-path="url(#breeze-help-line-85)">│</text><text class="breeze-help-r4"
x="24.4" y="2094" textLength="280.6"
clip-path="url(#breeze-help-line-85)">sbom                   </text><text
class="breeze-help-r1" x="329.4" y="2094" textLength="1110.2"
clip-path="url(#breeze-help-line-85)">Tools that release managers
[...]
+</text><text class="breeze-help-r5" x="0" y="2118.4" textLength="12.2"
clip-path="url(#breeze-help-line-86)">│</text><text class="breeze-help-r4"
x="24.4" y="2118.4" textLength="280.6"
clip-path="url(#breeze-help-line-86)">workflow-run           </text><text
class="breeze-help-r1" x="329.4" y="2118.4" textLength="1110.2"
clip-path="url(#breeze-help-line-86)">Tools to manage Airflow repository workflows &
[...]
+</text><text class="breeze-help-r5" x="0" y="2142.8" textLength="1464"
clip-path="url(#breeze-help-line-87)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2142.8" textLength="12.2"
clip-path="url(#breeze-help-line-87)">
+</text><text class="breeze-help-r5" x="0" y="2167.2" textLength="24.4"
clip-path="url(#breeze-help-line-88)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2167.2" textLength="158.6"
clip-path="url(#breeze-help-line-88)"> CI commands </text><text
class="breeze-help-r5" x="183" y="2167.2" textLength="1256.6"
clip-path="url(#breeze-help-line-88)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="2191.6" textLength="12.2"
clip-path="url(#breeze-help-line-89)">│</text><text class="breeze-help-r4"
x="24.4" y="2191.6" textLength="61"
clip-path="url(#breeze-help-line-89)">ci   </text><text
class="breeze-help-r1" x="109.8" y="2191.6" textLength="1329.8"
clip-path="url(#breeze-help-line-89)">Tools that CI workflows use to cleanup/manage CI environment      &
[...]
+</text><text class="breeze-help-r5" x="0" y="2216" textLength="1464"
clip-path="url(#breeze-help-line-90)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2216" textLength="12.2"
clip-path="url(#breeze-help-line-90)">
+</text><text class="breeze-help-r5" x="0" y="2240.4" textLength="24.4"
clip-path="url(#breeze-help-line-91)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2240.4" textLength="231.8"
clip-path="url(#breeze-help-line-91)"> Registry commands </text><text
class="breeze-help-r5" x="256.2" y="2240.4" textLength="1183.4"
clip-path="url(#breeze-help-line-91)">─────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="br [...]
+</text><text class="breeze-help-r5" x="0" y="2264.8" textLength="12.2"
clip-path="url(#breeze-help-line-92)">│</text><text class="breeze-help-r4"
x="24.4" y="2264.8" textLength="256.2"
clip-path="url(#breeze-help-line-92)">registry             </text><text
class="breeze-help-r1" x="305" y="2264.8" textLength="1134.6"
clip-path="url(#breeze-help-line-92)">Tools for the Airflow Provider Registry 
[...]
+</text><text class="breeze-help-r5" x="0" y="2289.2" textLength="1464"
clip-path="url(#breeze-help-line-93)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2289.2" textLength="12.2"
clip-path="url(#breeze-help-line-93)">
+</text><text class="breeze-help-r5" x="0" y="2313.6" textLength="24.4"
clip-path="url(#breeze-help-line-94)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2313.6" textLength="158.6"
clip-path="url(#breeze-help-line-94)"> UI commands </text><text
class="breeze-help-r5" x="183" y="2313.6" textLength="1256.6"
clip-path="url(#breeze-help-line-94)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="2338" textLength="12.2"
clip-path="url(#breeze-help-line-95)">│</text><text class="breeze-help-r4"
x="24.4" y="2338" textLength="85.4"
clip-path="url(#breeze-help-line-95)">ui     </text><text
class="breeze-help-r1" x="134.2" y="2338" textLength="1305.4"
clip-path="url(#breeze-help-line-95)">Tools for UI development and maintenance          
[...]
+</text><text class="breeze-help-r5" x="0" y="2362.4" textLength="1464"
clip-path="url(#breeze-help-line-96)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2362.4" textLength="12.2"
clip-path="url(#breeze-help-line-96)">
+</text><text class="breeze-help-r5" x="0" y="2386.8" textLength="24.4"
clip-path="url(#breeze-help-line-97)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2386.8" textLength="207.4"
clip-path="url(#breeze-help-line-97)"> Issues commands </text><text
class="breeze-help-r5" x="231.8" y="2386.8" textLength="1207.8"
clip-path="url(#breeze-help-line-97)">───────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="br [...]
+</text><text class="breeze-help-r5" x="0" y="2411.2" textLength="12.2"
clip-path="url(#breeze-help-line-98)">│</text><text class="breeze-help-r4"
x="24.4" y="2411.2" textLength="231.8"
clip-path="url(#breeze-help-line-98)">issues             </text><text
class="breeze-help-r1" x="280.6" y="2411.2" textLength="1159"
clip-path="url(#breeze-help-line-98)">Tools for managing GitHub issues.   &
[...]
+</text><text class="breeze-help-r5" x="0" y="2435.6" textLength="1464"
clip-path="url(#breeze-help-line-99)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2435.6" textLength="12.2"
clip-path="url(#breeze-help-line-99)">
+</text><text class="breeze-help-r5" x="0" y="2460" textLength="24.4"
clip-path="url(#breeze-help-line-100)">╭─</text><text class="breeze-help-r5"
x="24.4" y="2460" textLength="195.2"
clip-path="url(#breeze-help-line-100)"> Setup commands </text><text
class="breeze-help-r5" x="219.6" y="2460" textLength="1220"
clip-path="url(#breeze-help-line-100)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="breeze- [...]
+</text><text class="breeze-help-r5" x="0" y="2484.4" textLength="12.2"
clip-path="url(#breeze-help-line-101)">│</text><text class="breeze-help-r4"
x="24.4" y="2484.4" textLength="146.4"
clip-path="url(#breeze-help-line-101)">setup       </text><text
class="breeze-help-r1" x="195.2" y="2484.4" textLength="1244.4"
clip-path="url(#breeze-help-line-101)">Tools that developers can use to configure Breeze   &#
[...]
+</text><text class="breeze-help-r5" x="0" y="2508.8" textLength="1464"
clip-path="url(#breeze-help-line-102)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-help-r1" x="1464" y="2508.8" textLength="12.2"
clip-path="url(#breeze-help-line-102)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index 102ac89521a..a3da27c3903 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -246,8 +246,8 @@
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="971.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="971.6"
textLength="890.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">testing:core-tests | testing:docker-compose-tests | testing:helm-tests | </text><text
class="breeze-setup-check-all-params-in-gr [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="996"
textLength="1195.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">testing:providers-integration-tests | testing:providers-tests | testing:python-api-client-tests | </text><text
class="breeze-setup- [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1020.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1020.4"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">testing:system-tests | testing:task-sdk-integration-tests | testing:task-sdk-tests | testing:ui-e2e-tests</text><text
class="br [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1044.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1044.8"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">| ui | ui:check-translation-completeness | ui:compile-assets | workflow-run | workflow-run:publish-docs |</
[...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1069.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1069.2"
textLength="841.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">workflow-run:release-constraints | workflow-run:sync-staging-to-main)</text><text
class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y= [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1044.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1044.8"
textLength="1061.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">| ui | ui:check-translation-completeness | ui:compile-assets | verify | workflow-run | </text><text
[...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1069.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1069.2"
textLength="1183.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">workflow-run:publish-docs | workflow-run:release-constraints | workflow-run:sync-staging-to-main)</text><text
class="breeze-setup-check [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1093.6" textLength="1464"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1093.6"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="1118"
textLength="24.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">╭─</text><text
class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="1118"
textLength="195.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)"> Common options </text><text
class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="1118"
textLength="1220" clip-path="url(#breeze-setup [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="1142.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-46)">│</text><text
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1142.4"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-46)">--verbose</text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1142.4"
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
index 79fb51c84ed..e906db0e840 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
@@ -1 +1 @@
-466e12cd92f255b4a2ea75d3fe6d2c6c
+071569892714c5561156e19cdaa04041
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
index 9d2c56dd70a..8d98810ae3a 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -264,8 +264,8 @@
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1044.8" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1044.8"
textLength="1037"
clip-path="url(#breeze-setup-regenerate-command-images-line-42)">testing:helm-tests | testing:providers-integration-tests | testing:providers-tests | </text><text
class="breeze-setup-regenerate-co [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1069.2" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1069.2"
textLength="1146.8"
clip-path="url(#breeze-setup-regenerate-command-images-line-43)">testing:python-api-client-tests | testing:system-tests | testing:task-sdk-integration-tests | </text><text
class="breeze-setup-re [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1093.6" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1093.6"
textLength="1085.8"
clip-path="url(#breeze-setup-regenerate-command-images-line-44)">testing:task-sdk-tests | testing:ui-e2e-tests | ui | ui:check-translation-completeness | </text><text
class="breeze-set [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118"
textLength="1195.6"
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:compile-assets | workflow-run | workflow-run:publish-docs | workflow-run:release-constraints | </text><text
class="breez [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1142.4" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1142.4"
textLength="414.8"
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">workflow-run:sync-staging-to-main)</text><text
class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1142.4"
textLength="12.2" clip-path="url(#breeze- [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118"
textLength="878.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:compile-assets | verify | workflow-run | workflow-run:publish-docs | </text><text
class="breeze-setup-regenerate-command- [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1142.4" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1142.4"
textLength="841.8"
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">workflow-run:release-constraints | workflow-run:sync-staging-to-main)</text><text
class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1142 [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1166.8" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-47)">│</text><text
class="breeze-setup-regenerate-command-images-r4" x="24.4" y="1166.8"
textLength="146.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-47)">--check-only</text><text
class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1166.8"
textLength="1244.4" clip-path="url(#breeze-setup-regenerate-comma [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1191.2" textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-48)">│</text><text
class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1191.2"
textLength="170.8"
clip-path="url(#breeze-setup-regenerate-command-images-line-48)">together with </text><text
class="breeze-setup-regenerate-command-images-r4" x="366" y="1191.2"
textLength="109.8" clip-path="url(#breeze-setup-regene [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0"
y="1215.6" textLength="1464"
clip-path="url(#breeze-setup-regenerate-command-images-line-49)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-setup-regenerate-command-images-r1" x="1464" y="1215.6"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-49)">
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
index 8c6c5112178..d5eb31a16b7 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
@@ -1 +1 @@
-4463e56c4d0363f78d210bc76e8613bb
+57d31722815e46eeb1cdf1b92d5a87b1
diff --git a/dev/breeze/doc/images/output_verify.svg
b/dev/breeze/doc/images/output_verify.svg
new file mode 100644
index 00000000000..4cecb2cbe93
--- /dev/null
+++ b/dev/breeze/doc/images/output_verify.svg
@@ -0,0 +1,128 @@
+<svg class="rich-terminal" viewBox="0 0 1482 464.79999999999995"
xmlns="http://www.w3.org/2000/svg">
+ <!-- Generated with Rich https://www.textualize.io -->
+ <style>
+
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Regular"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff")
format("woff");
+ font-style: normal;
+ font-weight: 400;
+ }
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Bold"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff")
format("woff");
+ font-style: bold;
+ font-weight: 700;
+ }
+
+ .breeze-verify-matrix {
+ font-family: Fira Code, monospace;
+ font-size: 20px;
+ line-height: 24.4px;
+ font-variant-east-asian: full-width;
+ }
+
+ .breeze-verify-title {
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }
+
+ .breeze-verify-r1 { fill: #c5c8c6 }
+.breeze-verify-r2 { fill: #d0b344 }
+.breeze-verify-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-verify-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-verify-r5 { fill: #868887 }
+.breeze-verify-r6 { fill: #8d7b39 }
+.breeze-verify-r7 { fill: #98a84b;font-weight: bold }
+ </style>
+
+ <defs>
+ <clipPath id="breeze-verify-clip-terminal">
+ <rect x="0" y="0" width="1463.0" height="413.79999999999995" />
+ </clipPath>
+ <clipPath id="breeze-verify-line-0">
+ <rect x="0" y="1.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-1">
+ <rect x="0" y="25.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-2">
+ <rect x="0" y="50.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-3">
+ <rect x="0" y="74.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-4">
+ <rect x="0" y="99.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-5">
+ <rect x="0" y="123.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-6">
+ <rect x="0" y="147.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-7">
+ <rect x="0" y="172.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-8">
+ <rect x="0" y="196.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-9">
+ <rect x="0" y="221.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-10">
+ <rect x="0" y="245.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-11">
+ <rect x="0" y="269.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-12">
+ <rect x="0" y="294.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-13">
+ <rect x="0" y="318.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-14">
+ <rect x="0" y="343.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-verify-line-15">
+ <rect x="0" y="367.5" width="1464" height="24.65"/>
+ </clipPath>
+ </defs>
+
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="462.8" rx="8"/><text
class="breeze-verify-title" fill="#c5c8c6" text-anchor="middle" x="740"
y="27">Command: verify</text>
+ <g transform="translate(26,22)">
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
+ </g>
+
+ <g transform="translate(9, 41)"
clip-path="url(#breeze-verify-clip-terminal)">
+
+ <g class="breeze-verify-matrix">
+ <text class="breeze-verify-r1" x="1464" y="20" textLength="12.2"
clip-path="url(#breeze-verify-line-0)">
+</text><text class="breeze-verify-r2" x="12.2" y="44.4" textLength="73.2"
clip-path="url(#breeze-verify-line-1)">Usage:</text><text
class="breeze-verify-r3" x="97.6" y="44.4" textLength="158.6"
clip-path="url(#breeze-verify-line-1)">breeze verify</text><text
class="breeze-verify-r1" x="268.4" y="44.4" textLength="12.2"
clip-path="url(#breeze-verify-line-1)">[</text><text class="breeze-verify-r4"
x="280.6" y="44.4" textLength="85.4"
clip-path="url(#breeze-verify-line-1)">OPTIONS</tex [...]
+</text><text class="breeze-verify-r1" x="1464" y="68.8" textLength="12.2"
clip-path="url(#breeze-verify-line-2)">
+</text><text class="breeze-verify-r1" x="12.2" y="93.2" textLength="1098"
clip-path="url(#breeze-verify-line-3)">List the local verification CI would require for the current changes. Nothing is executed.</text><text
class="breeze-verify-r1" x="1464" y="93.2" textLength="12.2"
clip-path="url(#breeze-verify-line-3)">
+</text><text class="breeze-verify-r1" x="1464" y="117.6" textLength="12.2"
clip-path="url(#breeze-verify-line-4)">
+</text><text class="breeze-verify-r5" x="0" y="142" textLength="24.4"
clip-path="url(#breeze-verify-line-5)">╭─</text><text class="breeze-verify-r5"
x="24.4" y="142" textLength="170.8"
clip-path="url(#breeze-verify-line-5)"> Verify flags </text><text
class="breeze-verify-r5" x="195.2" y="142" textLength="1244.4"
clip-path="url(#breeze-verify-line-5)">──────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="br [...]
+</text><text class="breeze-verify-r5" x="0" y="166.4" textLength="12.2"
clip-path="url(#breeze-verify-line-6)">│</text><text class="breeze-verify-r4"
x="24.4" y="166.4" textLength="122"
clip-path="url(#breeze-verify-line-6)">--base-ref</text><text
class="breeze-verify-r1" x="170.8" y="166.4" textLength="1268.8"
clip-path="url(#breeze-verify-line-6)">Git ref the change will be merged into. Changed files are computed from its
[...]
+</text><text class="breeze-verify-r5" x="0" y="190.8" textLength="12.2"
clip-path="url(#breeze-verify-line-7)">│</text><text class="breeze-verify-r1"
x="170.8" y="190.8" textLength="1268.8"
clip-path="url(#breeze-verify-line-7)">Defaults to main on the remote that points at apache/airflow, or the local main branch if there is no   </text><text
class="breeze-verify-r5" x="1451.8" y="19 [...]
+</text><text class="breeze-verify-r5" x="0" y="215.2" textLength="12.2"
clip-path="url(#breeze-verify-line-8)">│</text><text class="breeze-verify-r1"
x="170.8" y="215.2" textLength="158.6"
clip-path="url(#breeze-verify-line-8)">such remote. </text><text
class="breeze-verify-r6" x="329.4" y="215.2" textLength="73.2"
clip-path="url(#breeze-verify-line-8)">(TEXT)</text><text
class="breeze-verify-r5" x="1451.8" y="215.2" textLength="12.2"
clip-path="url(#breeze-verify-line-8)">│</t [...]
+</text><text class="breeze-verify-r5" x="0" y="239.6" textLength="12.2"
clip-path="url(#breeze-verify-line-9)">│</text><text class="breeze-verify-r4"
x="24.4" y="239.6" textLength="122"
clip-path="url(#breeze-verify-line-9)">--full    </text><text
class="breeze-verify-r1" x="170.8" y="239.6" textLength="1268.8"
clip-path="url(#breeze-verify-line-9)">List everything CI runs for the default matrix cell except static 
[...]
+</text><text class="breeze-verify-r5" x="0" y="264" textLength="12.2"
clip-path="url(#breeze-verify-line-10)">│</text><text class="breeze-verify-r1"
x="170.8" y="264" textLength="1268.8"
clip-path="url(#breeze-verify-line-10)">adds when a change touches CI tooling or dependency files.                           
[...]
+</text><text class="breeze-verify-r5" x="0" y="288.4" textLength="12.2"
clip-path="url(#breeze-verify-line-11)">│</text><text class="breeze-verify-r4"
x="24.4" y="288.4" textLength="122"
clip-path="url(#breeze-verify-line-11)">--json    </text><text
class="breeze-verify-r1" x="170.8" y="288.4" textLength="573.4"
clip-path="url(#breeze-verify-line-11)">Print machine-readable JSON instead of a table.</text><text
class="breeze-verify-r5" x=" [...]
+</text><text class="breeze-verify-r5" x="0" y="312.8" textLength="1464"
clip-path="url(#breeze-verify-line-12)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-verify-r1" x="1464" y="312.8" textLength="12.2"
clip-path="url(#breeze-verify-line-12)">
+</text><text class="breeze-verify-r5" x="0" y="337.2" textLength="24.4"
clip-path="url(#breeze-verify-line-13)">╭─</text><text class="breeze-verify-r5"
x="24.4" y="337.2" textLength="195.2"
clip-path="url(#breeze-verify-line-13)"> Common options </text><text
class="breeze-verify-r5" x="219.6" y="337.2" textLength="1220"
clip-path="url(#breeze-verify-line-13)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
cl [...]
+</text><text class="breeze-verify-r5" x="0" y="361.6" textLength="12.2"
clip-path="url(#breeze-verify-line-14)">│</text><text class="breeze-verify-r4"
x="24.4" y="361.6" textLength="109.8"
clip-path="url(#breeze-verify-line-14)">--verbose</text><text
class="breeze-verify-r7" x="158.6" y="361.6" textLength="24.4"
clip-path="url(#breeze-verify-line-14)">-v</text><text class="breeze-verify-r1"
x="207.4" y="361.6" textLength="585.6"
clip-path="url(#breeze-verify-line-14)">Print verbose& [...]
+</text><text class="breeze-verify-r5" x="0" y="386" textLength="12.2"
clip-path="url(#breeze-verify-line-15)">│</text><text class="breeze-verify-r4"
x="24.4" y="386" textLength="109.8"
clip-path="url(#breeze-verify-line-15)">--help   </text><text
class="breeze-verify-r7" x="158.6" y="386" textLength="24.4"
clip-path="url(#breeze-verify-line-15)">-h</text><text class="breeze-verify-r1"
x="207.4" y="386" textLength="329.4"
clip-path="url(#breeze-verify-line-15)">Show th [...]
+</text><text class="breeze-verify-r5" x="0" y="410.4" textLength="1464"
clip-path="url(#breeze-verify-line-16)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-verify-r1" x="1464" y="410.4" textLength="12.2"
clip-path="url(#breeze-verify-line-16)">
+</text>
+ </g>
+ </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_verify.txt
b/dev/breeze/doc/images/output_verify.txt
new file mode 100644
index 00000000000..24e0226a248
--- /dev/null
+++ b/dev/breeze/doc/images/output_verify.txt
@@ -0,0 +1 @@
+af28dee252e39588e825b2fef52146f0
diff --git a/dev/breeze/src/airflow_breeze/breeze.py
b/dev/breeze/src/airflow_breeze/breeze.py
index be8b8bae1ae..34b4a8d5d65 100755
--- a/dev/breeze/src/airflow_breeze/breeze.py
+++ b/dev/breeze/src/airflow_breeze/breeze.py
@@ -46,6 +46,7 @@ from airflow_breeze.commands.sbom_commands import sbom_group
# noqa: E402
from airflow_breeze.commands.setup_commands import setup_group # noqa: E402
from airflow_breeze.commands.testing_commands import testing_group # noqa:
E402
from airflow_breeze.commands.ui_commands import ui_group # noqa: E402
+from airflow_breeze.commands import verify_commands # noqa: E402, F401
main.add_command(testing_group)
main.add_command(kubernetes_group)
diff --git
a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
index 0c38fb7efa9..8d53feb5a5d 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
@@ -28,6 +28,7 @@ DEVELOPER_COMMANDS: dict[str, str | list[str]] = {
"cleanup",
"generate-migration-file",
"doctor",
+ "verify",
],
}
DEVELOPER_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
@@ -400,6 +401,16 @@ DEVELOPER_PARAMETERS: dict[str, list[dict[str, str |
list[str]]]] = {
],
},
],
+ "breeze verify": [
+ {
+ "name": "Verify flags",
+ "options": [
+ "--base-ref",
+ "--full",
+ "--json",
+ ],
+ },
+ ],
"breeze doctor": [
{
"name": "Auto-healing of breeze",
diff --git a/dev/breeze/src/airflow_breeze/commands/verify_commands.py
b/dev/breeze/src/airflow_breeze/commands/verify_commands.py
new file mode 100644
index 00000000000..0392a600c6a
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/commands/verify_commands.py
@@ -0,0 +1,180 @@
+# 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
+
+import contextlib
+import io
+import json
+import re
+import sys
+from subprocess import CompletedProcess
+
+import click
+from rich.markup import escape
+from rich.table import Table
+
+from airflow_breeze.branch_defaults import AIRFLOW_BRANCH,
DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH
+from airflow_breeze.commands.common_options import option_verbose
+from airflow_breeze.commands.main_command import main
+from airflow_breeze.global_constants import GithubEvents
+from airflow_breeze.utils.console import get_console, get_stderr_console
+from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
+from airflow_breeze.utils.reproduce_ci import SKIP_LOCAL_REPRODUCTION
+from airflow_breeze.utils.run_utils import run_command
+from airflow_breeze.utils.shared_options import get_verbose
+from airflow_breeze.utils.verification_plan import LeanSelectiveChecks,
build_local_verification_plan
+
+APACHE_AIRFLOW_URL = re.compile(r"github\.com[:/]apache/airflow(\.git)?/?$")
+
+
+def _run_git(*args: str) -> CompletedProcess:
+ return run_command(["git", *args], capture_output=True, text=True,
check=False, cwd=AIRFLOW_ROOT_PATH)
+
+
+def find_default_base_ref() -> str | None:
+ """The target branch on the remote that points at apache/airflow,
preferring ``upstream``."""
+ remotes = []
+ for line in _run_git("config", "--get-regexp",
r"^remote\..*\.url$").stdout.splitlines():
+ key, _, url = line.partition(" ")
+ if APACHE_AIRFLOW_URL.search(url.strip()):
+ remotes.append(key.removeprefix("remote.").removesuffix(".url"))
+ for remote in sorted(remotes, key=lambda name: (name != "upstream", name)):
+ ref = f"{remote}/{AIRFLOW_BRANCH}"
+ if _run_git("rev-parse", "--verify", "--quiet",
f"refs/remotes/{ref}").returncode == 0:
+ return ref
+ return None
+
+
+def has_merged_commits_missing_from(base_ref: str) -> bool:
+ """Whether a merge on the branch brought in commits that ``base_ref`` does
not have, as after
+ GitHub's "Update branch" when the local copy of the target branch was not
fetched since."""
+ for line in _run_git("rev-list", "--merges", "--parents",
f"{base_ref}..HEAD").stdout.splitlines():
+ for merged_parent in line.split()[2:]:
+ if _run_git("merge-base", "--is-ancestor", merged_parent,
base_ref).returncode != 0:
+ return True
+ return False
+
+
+def get_changed_files_against(base_ref: str) -> tuple[str, ...]:
+ def _git(*args: str) -> list[str]:
+ result = _run_git(*args)
+ if result.returncode != 0:
+ raise click.ClickException(
+ f"git {args[0]} failed for base ref {base_ref!r}:
{result.stderr.strip()}"
+ )
+ return result.stdout.splitlines()
+
+ merge_base = _git("merge-base", base_ref, "HEAD")[0]
+ # CI lists changed files with diff-tree, which reports a rename as a
delete plus an add.
+ changed = _git("diff", "--name-only", "--no-renames", merge_base) + _git(
+ "ls-files", "--others", "--exclude-standard"
+ )
+ return tuple(sorted(set(changed)))
+
+
[email protected](
+ name="verify",
+ help="List the local verification CI would require for the current
changes. Nothing is executed.",
+)
[email protected](
+ "--base-ref",
+ help=f"Git ref the change will be merged into. Changed files are computed
from its merge-base with "
+ f"HEAD. Defaults to {AIRFLOW_BRANCH} on the remote that points at
apache/airflow, or the local "
+ f"{AIRFLOW_BRANCH} branch if there is no such remote.",
+)
[email protected](
+ "--full",
+ is_flag=True,
+ help="List everything CI runs for the default matrix cell except static
checks, including the full "
+ "suite CI adds when a change touches CI tooling or dependency files.",
+)
[email protected]("--json", "as_json", is_flag=True, help="Print machine-readable
JSON instead of a table.")
+@option_verbose
[email protected]_context
+def verify(ctx: click.Context, base_ref: str | None, full: bool, as_json:
bool):
+ from airflow_breeze.utils.selective_checks import SelectiveChecks
+
+ if as_json:
+ ctx.meta[SKIP_LOCAL_REPRODUCTION] = True
+ no_apache_remote = False
+ if base_ref is None:
+ base_ref = find_default_base_ref()
+ if base_ref is None:
+ base_ref, no_apache_remote = AIRFLOW_BRANCH, True
+ # SelectiveChecks narrates its decisions on stdout; keep stdout for the
result only.
+ with contextlib.redirect_stdout(sys.stderr if get_verbose() else
io.StringIO()):
+ changed_files = get_changed_files_against(base_ref)
+ kwargs = dict(
+ files=changed_files,
+ commit_ref="HEAD",
+ default_branch=AIRFLOW_BRANCH,
+ default_constraints_branch=DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH,
+ github_event=GithubEvents.PULL_REQUEST,
+ )
+ ci = SelectiveChecks(**kwargs)
+ sc = ci if full else LeanSelectiveChecks(**kwargs)
+ result = build_local_verification_plan(
+ sc, changed_files, base_ref,
full_tests_needed=ci.full_tests_needed, full=full
+ )
+ warnings = []
+ if no_apache_remote:
+ warnings.append(
+ f"No git remote points at apache/airflow, so changes are compared
with the local "
+ f"{AIRFLOW_BRANCH} branch."
+ )
+ if has_merged_commits_missing_from(base_ref):
+ warnings.append(
+ f"Your branch has merged commits that are not in {base_ref}, so
they are listed as your "
+ f"changes. Update {base_ref} (for example with git fetch) and run
again."
+ )
+ if as_json:
+ print(json.dumps(result, indent=2))
+ for warning in warnings:
+ get_stderr_console().print(f"[warning]{warning}[/]")
+ return
+ console = get_console()
+ for warning in warnings:
+ console.print(f"[warning]{warning}[/]\n")
+ console.print(
+ f"[info]{len(changed_files)} changed file(s) against {base_ref}, "
+ f"CI default Python {result['default_python_version']}[/]\n"
+ )
+ table = Table(title="What CI runs for this change" if full else "What to
run for this change")
+ table.add_column("kind")
+ table.add_column("runs_in")
+ table.add_column("command", overflow="fold")
+ for item in result["items"]:
+ table.add_row(item["kind"], item["runs_in"], escape(item["command"]))
+ console.print(table)
+ console.print(
+ "\n[info]Static checks are not listed. Run prek as usual. It picks the
hooks for the changed files.[/]"
+ )
+ if result["manual_prek_hooks"]:
+ console.print(
+ "\n[warning]CI also runs these prek hooks, which a default prek
install does not run:[/]"
+ )
+ for hook in result["manual_prek_hooks"]:
+ console.print(escape(f"prek run --stage manual {hook} --from-ref
{base_ref}"), soft_wrap=True)
+ if result["full_tests_needed"] and not full:
+ console.print(
+ "\n[warning]CI also runs the full suite for this change because it
touches CI tooling or "
+ "dependency files. Pass --full to list it.[/]"
+ )
+ console.print(
+ f"\n[warning]Covers Python {result['default_python_version']} on
sqlite only; other Python versions, "
+ "Postgres, MySQL, the provider compatibility matrix and CI-only jobs
run in CI.[/]"
+ )
diff --git a/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
index 4a429f556ab..87dd17f484b 100644
--- a/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
+++ b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
@@ -218,9 +218,15 @@ def print_local_reproduction(commands:
list[ReproductionCommand]) -> None:
console.print(f"[warning]{ruler}[/]\n")
+SKIP_LOCAL_REPRODUCTION = "skip_local_reproduction"
+
+
def maybe_print_reproduction(ctx: click.Context) -> None:
- """Called by BreezeCommand.invoke() — prints reproduction instructions in
CI."""
- if not should_print_local_reproduction():
+ """Called by BreezeCommand.invoke() — prints reproduction instructions in
CI.
+
+ Commands whose stdout is a machine-readable contract set
``ctx.meta[SKIP_LOCAL_REPRODUCTION]``.
+ """
+ if not should_print_local_reproduction() or
ctx.meta.get(SKIP_LOCAL_REPRODUCTION):
return
github_repository = ctx.params.get("github_repository",
APACHE_AIRFLOW_GITHUB_REPOSITORY)
diff --git a/dev/breeze/src/airflow_breeze/utils/verification_plan.py
b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
new file mode 100644
index 00000000000..6e2e73fe69d
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
@@ -0,0 +1,170 @@
+# 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
+
+import json
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from airflow_breeze.utils.selective_checks import SelectiveChecks
+
+
+@dataclass(frozen=True)
+class VerificationItem:
+ kind: str
+ command: str
+ runs_in: str
+
+
+# SelectiveChecks flag -> (kind, command, runs_in) of the CI job(s) that flag
gates.
+# Mirrors .github/workflows/ci-amd.yml and basic-tests.yml; "breeze" means the
command needs
+# Docker and the CI image, "host" means only host tooling.
+FLAG_COMMANDS: dict[str, tuple[tuple[str, str, str], ...]] = {
+ "run_task_sdk_tests": (("unit", "breeze testing task-sdk-tests",
"breeze"),),
+ "run_airflow_ctl_tests": (("unit", "breeze testing airflow-ctl-tests",
"breeze"),),
+ "run_scripts_tests": (("unit", "uv run --project scripts pytest
scripts/tests/", "host"),),
+ "run_ui_tests": (
+ ("ui", "cd airflow-core/src/airflow/ui && pnpm install
--frozen-lockfile && pnpm test", "host"),
+ (
+ "ui",
+ "cd airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui
&& pnpm install --frozen-lockfile && pnpm test",
+ "host",
+ ),
+ ),
+ "run_api_codegen": (("schema", "breeze testing python-api-client-tests",
"breeze"),),
+ "run_go_sdk_tests": (("unit", "cd go-sdk && go test ./...", "host"),),
+ "run_java_sdk_tests": (
+ ("unit", "cd java-sdk && ./gradlew test", "host"),
+ ("docs", "breeze build-docs --sdk-docs-only --sdk=java", "breeze"),
+ ),
+ "run_ts_sdk_docs": (("docs", "breeze build-docs --sdk-docs-only
--sdk=typescript", "breeze"),),
+ "run_agent_framework_tests": (
+ (
+ "unit",
+ 'breeze shell --backend none --skip-db-tests "bash
/opt/airflow/scripts/in_container/run_agent_framework_tests.sh strands-agents"',
+ "breeze",
+ ),
+ (
+ "unit",
+ 'breeze shell --backend none --skip-db-tests "bash
/opt/airflow/scripts/in_container/run_agent_framework_tests.sh google-adk"',
+ "breeze",
+ ),
+ ),
+ "run_breeze_integration_tests": (
+ ("unit", "cd dev/breeze && uv run --locked pytest", "host"),
+ ("unit", "cd dev/breeze && uv run --locked pytest -m
integration_tests", "host"),
+ ),
+}
+
+# Gated CI jobs that need a cluster, a prod image or an e2e stack; classified
only, never printed.
+NOT_RUNNABLE_LOCALLY: frozenset[str] = frozenset(
+ {
+ "run_kubernetes_tests",
+ "run_helm_tests",
+ "run_kustomize_overlays_tests",
+ "run_ui_e2e_tests",
+ "run_task_sdk_integration_tests",
+ "run_airflow_ctl_integration_tests",
+ "run_system_tests",
+ "run_remote_logging_s3_e2e_tests",
+ "run_remote_logging_elasticsearch_e2e_tests",
+ "run_remote_logging_opensearch_e2e_tests",
+ "run_event_driven_e2e_tests",
+ "run_java_sdk_e2e_tests",
+ "run_go_sdk_e2e_tests",
+ "run_openlineage_e2e_tests",
+ "run_openlineage_e2e_compat_tests",
+ "run_ts_sdk_e2e_tests",
+ }
+)
+
+# Gated CI jobs that run a prek hook a default `prek install` never triggers
(the hooks are
+# manual or pre-push stage), mapped to that hook. They are not plan items; the
hooks are
+# reported separately so the user can run them with `--stage manual`.
+PREK_JOBS: dict[str, str] = {
+ "run_mypy_providers": "mypy-providers",
+ "has_migrations": "migration-round-trip",
+}
+
+
+def build_unit_test_items(group: str, test_types_json: str | None) ->
list[VerificationItem]:
+ test_types = " ".join(chunk["test_types"] for chunk in
json.loads(test_types_json or "null") or [])
+ if not test_types:
+ return []
+ prefix = f"breeze testing {group}-tests"
+ return [
+ VerificationItem(
+ "unit",
+ f'{prefix} --run-in-parallel --run-db-tests-only
--parallel-test-types "{test_types}"',
+ "breeze",
+ ),
+ VerificationItem(
+ "unit",
+ f'{prefix} --use-xdist --skip-db-tests --no-db-cleanup --backend
none --parallel-test-types "{test_types}"',
+ "breeze",
+ ),
+ ]
+
+
+class LeanSelectiveChecks(SelectiveChecks):
+ """The same selective-checks rules without the run-everything safety net
CI applies to changes
+ that touch CI tooling or dependency files; only what the changed files
match themselves."""
+
+ full_tests_needed = False
+
+
+def build_local_verification_plan(
+ sc: SelectiveChecks,
+ changed_files: tuple[str, ...],
+ base_ref: str,
+ *,
+ full_tests_needed: bool,
+ full: bool = False,
+) -> dict[str, Any]:
+ items: list[VerificationItem] = []
+ if sc.run_unit_tests:
+ items += build_unit_test_items("core",
sc.core_test_types_list_as_strings_in_json)
+ if not sc.skip_providers_tests:
+ items += build_unit_test_items("providers",
sc.providers_test_types_list_as_strings_in_json)
+ for flag, commands in FLAG_COMMANDS.items():
+ if getattr(sc, flag):
+ items += [VerificationItem(*command) for command in commands]
+ if sc.docs_build:
+ items.append(
+ VerificationItem("docs", f"breeze build-docs
{sc.docs_list_as_string}".rstrip(), "breeze")
+ )
+ if full:
+ # Jobs CI runs on every PR regardless of the change.
+ breeze_tests = VerificationItem("unit", "cd dev/breeze && uv run
--locked pytest", "host")
+ if breeze_tests not in items:
+ items.append(breeze_tests)
+ if dists := "
".join(sorted(json.loads(sc.shared_distributions_as_json))):
+ items.append(
+ VerificationItem(
+ "unit",
+ f"for d in {dists}; do (cd shared/$d && uv run --group dev
pytest) || exit 1; done",
+ "host",
+ )
+ )
+ return {
+ "base_ref": base_ref,
+ "default_python_version": sc.default_python_version,
+ "full_tests_needed": full_tests_needed,
+ "changed_files": list(changed_files),
+ "items": [asdict(item) for item in items],
+ "manual_prek_hooks": [hook for flag, hook in PREK_JOBS.items() if
getattr(sc, flag)],
+ }
diff --git a/dev/breeze/tests/test_reproduce_ci.py
b/dev/breeze/tests/test_reproduce_ci.py
index 13d9c1b58e6..5b85b41d360 100644
--- a/dev/breeze/tests/test_reproduce_ci.py
+++ b/dev/breeze/tests/test_reproduce_ci.py
@@ -24,10 +24,12 @@ import pytest
from airflow_breeze.utils.click_utils import BreezeGroup
from airflow_breeze.utils.reproduce_ci import (
+ SKIP_LOCAL_REPRODUCTION,
ReproductionCommand,
build_checkout_reproduction_commands,
build_ci_image_reproduction_command,
build_reproduction_command_from_context,
+ maybe_print_reproduction,
print_local_reproduction,
should_print_local_reproduction,
)
@@ -430,3 +432,13 @@ class TestBuildReproductionCommandFromContext:
ctx = _invoke_and_get_context(cmd, ["-b", "postgres"])
result = build_reproduction_command_from_context(ctx)
assert result.argv == ["my-cmd", "--backend", "postgres"]
+
+
[email protected]("airflow_breeze.utils.reproduce_ci.print_local_reproduction",
autospec=True)
+def test_commands_can_opt_out_of_reproduction_banner(mock_print, monkeypatch):
+ monkeypatch.setenv("CI", "true")
+ monkeypatch.setenv("GITHUB_ACTIONS", "true")
+ ctx = click.Context(click.Command("verify"))
+ ctx.meta[SKIP_LOCAL_REPRODUCTION] = True
+ maybe_print_reproduction(ctx)
+ mock_print.assert_not_called()
diff --git a/dev/breeze/tests/test_verification_plan.py
b/dev/breeze/tests/test_verification_plan.py
new file mode 100644
index 00000000000..6a5d10250d3
--- /dev/null
+++ b/dev/breeze/tests/test_verification_plan.py
@@ -0,0 +1,244 @@
+# 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
+
+import inspect
+import json
+import shlex
+from functools import cached_property
+from unittest.mock import Mock
+
+import pytest
+import yaml
+
+from airflow_breeze.global_constants import
DEFAULT_PYTHON_MAJOR_MINOR_VERSION, GithubEvents
+from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
+from airflow_breeze.utils.selective_checks import SelectiveChecks
+from airflow_breeze.utils.verification_plan import (
+ FLAG_COMMANDS,
+ NOT_RUNNABLE_LOCALLY,
+ PREK_JOBS,
+ LeanSelectiveChecks,
+ build_local_verification_plan,
+ build_unit_test_items,
+)
+
+NEUTRAL_COMMIT = "938f0c1f3cc4cbe867123ee8aa9f290f9f18100a"
+# Exported by SelectiveChecks but no workflow job is gated on them (build-info
pass-through only).
+NOT_A_CI_JOB = {
+ "run_amazon_tests",
+ "run_api_tests",
+ "run_ol_tests",
+ "run_python_scans",
+ "run_javascript_scans",
+}
+
+
+def _selective_checks(files: tuple[str, ...], default_branch: str = "main") ->
SelectiveChecks:
+ return SelectiveChecks(
+ files=files,
+ commit_ref=NEUTRAL_COMMIT,
+ github_event=GithubEvents.PULL_REQUEST,
+ pr_labels=tuple(),
+ default_branch=default_branch,
+ )
+
+
+def _mock_selective_checks(**flags: object) -> Mock:
+ sc = Mock(spec=SelectiveChecks)
+ for flag in (*FLAG_COMMANDS, *PREK_JOBS, "run_unit_tests", "docs_build"):
+ setattr(sc, flag, False)
+ sc.skip_providers_tests = True
+ sc.shared_distributions_as_json = "[]"
+ sc.default_python_version = DEFAULT_PYTHON_MAJOR_MINOR_VERSION
+ for flag, value in flags.items():
+ setattr(sc, flag, value)
+ return sc
+
+
[email protected]("flag", sorted(FLAG_COMMANDS))
+def test_each_flag_maps_to_its_commands(flag: str):
+ result = build_local_verification_plan(
+ _mock_selective_checks(**{flag: True}), (), "main",
full_tests_needed=False
+ )
+ commands = [item["command"] for item in result["items"]]
+ assert commands == [command for _, command, _ in FLAG_COMMANDS[flag]]
+
+
+def test_docs_only_change_mirrors_the_ci_cell():
+ files = ("airflow-core/docs/index.rst",)
+ sc = _selective_checks(files)
+ result = build_local_verification_plan(sc, files, "main",
full_tests_needed=False)
+ assert [item["command"] for item in result["items"]] == ["breeze
build-docs apache-airflow"]
+
+
+def test_core_change_splits_db_and_non_db_cells():
+ files = ("airflow-core/src/airflow/models/dag.py",)
+ sc = _selective_checks(files)
+ core_types = " ".join(c["test_types"] for c in
json.loads(sc.core_test_types_list_as_strings_in_json))
+ result = build_local_verification_plan(sc, files, "main",
full_tests_needed=False)
+ unit = [
+ item["command"] for item in result["items"] if
item["command"].startswith("breeze testing core-tests")
+ ]
+ assert unit == [
+ f'breeze testing core-tests --run-in-parallel --run-db-tests-only
--parallel-test-types "{core_types}"',
+ "breeze testing core-tests --use-xdist --skip-db-tests --no-db-cleanup
--backend none "
+ f'--parallel-test-types "{core_types}"',
+ ]
+
+
+def test_every_selective_checks_run_flag_is_classified():
+ run_flags = {
+ name
+ for name, value in inspect.getmembers(SelectiveChecks)
+ if isinstance(value, cached_property)
+ and (name.startswith("run_") or name in ("docs_build",
"has_migrations"))
+ }
+ classified = (
+ set(FLAG_COMMANDS)
+ | NOT_RUNNABLE_LOCALLY
+ | set(PREK_JOBS)
+ | {"run_unit_tests", "docs_build"}
+ | NOT_A_CI_JOB
+ )
+ assert run_flags - classified == set(), (
+ "new SelectiveChecks flag: add it to FLAG_COMMANDS,
NOT_RUNNABLE_LOCALLY, PREK_JOBS or NOT_A_CI_JOB"
+ )
+ assert classified - run_flags == set(), "classified flag no longer exists
on SelectiveChecks"
+
+
[email protected]("group", ["core", "providers"])
+def test_unit_test_commands_use_the_same_flags_as_the_ci_script(group: str):
+ ci_script = AIRFLOW_ROOT_PATH / "scripts" / "ci" / "testing" /
"run_unit_tests.sh"
+ ci_flag_sets = {
+ frozenset(shlex.split(line)[3:])
+ for line in ci_script.read_text().splitlines()
+ if line.strip().startswith(f"breeze testing {group}-tests")
+ }
+ for item in build_unit_test_items(group, json.dumps([{"description": "x",
"test_types": "Always"}])):
+ tokens = shlex.split(item.command)[3:]
+ cut = tokens.index("--parallel-test-types")
+ assert frozenset(tokens[:cut] + tokens[cut + 2 :]) in ci_flag_sets,
item.command
+
+
+def test_empty_diff_lists_nothing():
+ plan = build_local_verification_plan(_selective_checks(()), (), "main",
full_tests_needed=False)
+ assert plan["items"] == []
+ assert plan["manual_prek_hooks"] == []
+
+
[email protected]("full", [False, True])
+def test_prek_jobs_are_not_listed(full: bool):
+ files = (
+ "providers/amazon/src/airflow/providers/amazon/hooks/s3.py",
+ "airflow-core/src/airflow/migrations/versions/0001_example.py",
+ )
+ sc = _selective_checks(files)
+ assert all(getattr(sc, flag) for flag in PREK_JOBS)
+ plan = build_local_verification_plan(sc, files, "main",
full_tests_needed=False, full=full)
+ assert not any("prek" in item["command"] for item in plan["items"])
+ assert plan["manual_prek_hooks"] == ["mypy-providers",
"migration-round-trip"]
+
+
[email protected]("hook", sorted(PREK_JOBS.values()))
+def test_prek_job_hooks_are_not_installed_by_default(hook: str):
+ stages = [
+ entry.get("stages", ["pre-commit"])
+ for config in [
+ AIRFLOW_ROOT_PATH / ".pre-commit-config.yaml",
+ *AIRFLOW_ROOT_PATH.glob("*/.pre-commit-config.yaml"),
+ ]
+ for repo in yaml.safe_load(config.read_text()).get("repos", [])
+ for entry in repo.get("hooks", [])
+ if entry.get("id") == hook
+ ]
+ assert stages, f"{hook} is not defined in any .pre-commit-config.yaml"
+ assert all("pre-commit" not in hook_stages for hook_stages in stages)
+
+
+def test_full_plan_skips_shared_distributions_when_there_are_none():
+ plan = build_local_verification_plan(
+ _mock_selective_checks(), (), "main", full_tests_needed=False,
full=True
+ )
+ assert [item["command"] for item in plan["items"]] == ["cd dev/breeze &&
uv run --locked pytest"]
+
+
+def test_release_branch_drops_providers_tests():
+ files = ("airflow-core/src/airflow/models/dag.py",)
+ result = build_local_verification_plan(
+ _selective_checks(files, default_branch="v3-1-test"), files,
"v3-1-test", full_tests_needed=False
+ )
+ commands = [item["command"] for item in result["items"]]
+ assert any(command.startswith("breeze testing core-tests") for command in
commands)
+ assert not any(command.startswith("breeze testing providers-tests") for
command in commands)
+
+
+def test_lean_plan_skips_the_full_suite_expansion_for_ci_tooling_changes():
+ files = ("dev/breeze/src/airflow_breeze/breeze.py",)
+ ci = _selective_checks(files)
+ lean = LeanSelectiveChecks(
+ files=files, commit_ref=NEUTRAL_COMMIT,
github_event=GithubEvents.PULL_REQUEST, default_branch="main"
+ )
+ assert ci.full_tests_needed is True
+ full_commands = [
+ i["command"]
+ for i in build_local_verification_plan(ci, files, "main",
full_tests_needed=True)["items"]
+ ]
+ lean_commands = [
+ i["command"]
+ for i in build_local_verification_plan(lean, files, "main",
full_tests_needed=True)["items"]
+ ]
+ assert any(c.startswith("breeze testing core-tests") for c in
full_commands)
+ assert not any(c.startswith("breeze testing") for c in lean_commands)
+ assert "cd dev/breeze && uv run --locked pytest" in lean_commands
+
+
+def test_lean_and_full_plans_agree_when_the_change_does_not_expand():
+ files = ("airflow-core/src/airflow/models/dag.py",)
+ lean = LeanSelectiveChecks(
+ files=files, commit_ref=NEUTRAL_COMMIT,
github_event=GithubEvents.PULL_REQUEST, default_branch="main"
+ )
+ assert (
+ build_local_verification_plan(lean, files, "main",
full_tests_needed=False)["items"]
+ == (
+ build_local_verification_plan(_selective_checks(files), files,
"main", full_tests_needed=False)[
+ "items"
+ ]
+ )
+ )
+
+
+def test_full_plan_adds_the_jobs_ci_runs_on_every_pr():
+ files = ("airflow-core/docs/index.rst",)
+ sc = _selective_checks(files)
+ lean = build_local_verification_plan(sc, files, "main",
full_tests_needed=False)["items"]
+ full = build_local_verification_plan(sc, files, "main",
full_tests_needed=False, full=True)["items"]
+ assert [i["command"] for i in full] == [
+ *(i["command"] for i in lean),
+ "cd dev/breeze && uv run --locked pytest",
+ "for d in "
+ + " ".join(sorted(json.loads(sc.shared_distributions_as_json)))
+ + "; do (cd shared/$d && uv run --group dev pytest) || exit 1; done",
+ ]
+
+
+def test_full_plan_does_not_duplicate_breeze_tests_for_a_breeze_change():
+ files = ("dev/breeze/src/airflow_breeze/breeze.py",)
+ full = build_local_verification_plan(
+ _selective_checks(files), files, "main", full_tests_needed=True,
full=True
+ )
+ assert [i["command"] for i in full["items"]].count("cd dev/breeze && uv
run --locked pytest") == 1
diff --git a/dev/breeze/tests/test_verify_commands.py
b/dev/breeze/tests/test_verify_commands.py
new file mode 100644
index 00000000000..58b9a163631
--- /dev/null
+++ b/dev/breeze/tests/test_verify_commands.py
@@ -0,0 +1,327 @@
+# 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
+
+import json
+import os
+import re
+import subprocess
+from subprocess import CompletedProcess
+from unittest.mock import patch
+
+import pytest
+from click.testing import CliRunner
+
+from airflow_breeze.commands.verify_commands import (
+ find_default_base_ref,
+ get_changed_files_against,
+ has_merged_commits_missing_from,
+ verify,
+)
+
+ANSI = re.compile(r"\x1b\[[0-9;]*m")
+
+
+def _default_base_ref() -> str:
+ return "main"
+
+
+def _no_merged_commits_missing(base_ref: str) -> bool:
+ return False
+
+
[email protected](autouse=True)
+def _no_git_lookups_against_the_checkout():
+ """Keep CLI tests independent of the developer's remotes; tests that need
these patch them again."""
+ # Plain functions rather than mocks, so tests can still patch these with
autospec=True.
+ with (
+ patch("airflow_breeze.commands.verify_commands.find_default_base_ref",
new=_default_base_ref),
+ patch(
+
"airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+ new=_no_merged_commits_missing,
+ ),
+ ):
+ yield
+
+
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/docs/index.rst",),
+)
+def test_json_output_keeps_stdout_parseable(mock_files, monkeypatch):
+ # GitHub Actions makes breeze print a "how to reproduce" banner after
every command.
+ monkeypatch.setenv("CI", "true")
+ monkeypatch.setenv("GITHUB_ACTIONS", "true")
+ monkeypatch.setenv("GITHUB_SHA", "0" * 40)
+ result = CliRunner().invoke(verify, ["--json"], catch_exceptions=False)
+ assert result.exit_code == 0
+ payload = json.loads(result.stdout)
+ assert set(payload) == {
+ "base_ref",
+ "default_python_version",
+ "full_tests_needed",
+ "changed_files",
+ "items",
+ "manual_prek_hooks",
+ }
+ assert payload["changed_files"] == ["airflow-core/docs/index.rst"]
+ assert payload["items"] == [
+ {"kind": "docs", "command": "breeze build-docs apache-airflow",
"runs_in": "breeze"}
+ ]
+
+
+@patch(
+ "airflow_breeze.commands.verify_commands.run_command",
+ autospec=True,
+ return_value=CompletedProcess(
+ args=[], returncode=128, stdout="", stderr="fatal: Not a valid object
name"
+ ),
+)
+def test_unknown_base_ref_is_a_clean_usage_error(mock_run):
+ result = CliRunner().invoke(verify, ["--base-ref", "no-such-branch"])
+ assert result.exit_code == 1
+ rendered = " ".join(ANSI.sub("", result.output).replace("│", " ").split())
+ assert "git merge-base failed for base ref 'no-such-branch': fatal: Not a
valid object name" in rendered
+
+
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/docs/index.rst",),
+)
+def test_selective_checks_narration_is_hidden_unless_verbose(mock_files):
+ result = CliRunner().invoke(verify, [], catch_exceptions=False)
+ assert result.exit_code == 0
+ assert "FileGroupForCi" not in result.output
+ assert "FileGroupForCi" not in result.stderr
+
+
[email protected]("args", [[], ["--full"]])
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+
return_value=("providers/amazon/src/airflow/providers/amazon/hooks/s3.py",),
+)
+def test_static_checks_are_left_to_prek(mock_files, args: list[str]):
+ result = CliRunner().invoke(verify, args, catch_exceptions=False)
+ assert result.exit_code == 0
+ output = " ".join(result.output.split())
+ assert "Static checks are not listed. Run prek as usual." in output
+ assert "prek run --stage manual mypy-providers --from-ref main" in output
+ assert "migration-round-trip" not in output
+
+
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/docs/index.rst",),
+)
+def test_no_manual_prek_hint_when_no_hook_is_triggered(mock_files):
+ result = CliRunner().invoke(verify, [], catch_exceptions=False)
+ assert result.exit_code == 0
+ assert "--stage manual" not in " ".join(result.output.split())
+
+
[email protected](
+ ("files", "expanded"),
+ [
+ (("airflow-core/docs/index.rst",), False),
+ (("dev/breeze/src/airflow_breeze/breeze.py",), True),
+ ],
+)
+def test_full_suite_expansion_is_explained(files: tuple[str, ...], expanded:
bool):
+ with patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
autospec=True, return_value=files
+ ):
+ result = CliRunner().invoke(verify, [], catch_exceptions=False)
+ full = CliRunner().invoke(verify, ["--full"], catch_exceptions=False)
+ assert result.exit_code == 0
+ assert ("CI also runs the full suite" in " ".join(result.output.split()))
is expanded
+ assert "CI also runs the full suite" not in " ".join(full.output.split())
+ assert ("breeze testing core-tests" in " ".join(full.output.split())) is
expanded
+ assert "breeze testing core-tests" not in " ".join(result.output.split())
+
+
+@patch("airflow_breeze.commands.verify_commands.run_command", autospec=True)
+def test_changed_files_list_renames_like_ci_diff_tree(mock_run):
+ mock_run.side_effect = [
+ CompletedProcess(args=[], returncode=0, stdout="abc123\n", stderr=""),
+ CompletedProcess(args=[], returncode=0, stdout="new.py\nold.py\n",
stderr=""),
+ CompletedProcess(args=[], returncode=0, stdout="untracked.py\n",
stderr=""),
+ ]
+ assert get_changed_files_against("main") == ("new.py", "old.py",
"untracked.py")
+ assert mock_run.call_args_list[1].args[0] == ["git", "diff",
"--name-only", "--no-renames", "abc123"]
+
+
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/src/airflow/models/dag.py",),
+)
+def test_long_commands_are_folded_not_truncated(mock_files):
+ result = CliRunner().invoke(verify, [], catch_exceptions=False)
+ assert result.exit_code == 0
+ assert "\u2026" not in result.output
+ compact = re.sub(r"[\u2502\s]", "", result.output)
+ assert
"breezetestingcore-tests--use-xdist--skip-db-tests--no-db-cleanup--backendnone"
in compact
+
+
+FORK = "https://github.com/someone/airflow.git"
+APACHE_HTTPS = "https://github.com/apache/airflow.git"
+APACHE_SSH = "[email protected]:apache/airflow.git"
+
+
[email protected](
+ ("remotes", "fetched", "expected"),
+ [
+ pytest.param(
+ {"origin": FORK, "upstream": APACHE_HTTPS},
+ {"origin", "upstream"},
+ "upstream/main",
+ id="convention",
+ ),
+ pytest.param(
+ {"origin": FORK, "apache": APACHE_SSH}, {"origin", "apache"},
"apache/main", id="other-name-ssh"
+ ),
+ pytest.param(
+ {"aaa": APACHE_HTTPS, "upstream": APACHE_HTTPS},
+ {"aaa", "upstream"},
+ "upstream/main",
+ id="prefers-upstream",
+ ),
+ pytest.param(
+ {"aaa": APACHE_HTTPS, "upstream": APACHE_HTTPS}, {"aaa"},
"aaa/main", id="upstream-not-fetched"
+ ),
+ pytest.param(
+ {"origin": "https://github.com/apache/airflow-site.git"},
{"origin"}, None, id="other-apache-repo"
+ ),
+ pytest.param({"origin": FORK}, {"origin"}, None, id="fork-only"),
+ ],
+)
+@patch("airflow_breeze.commands.verify_commands.run_command", autospec=True)
+def test_default_base_ref_is_main_on_the_apache_remote(mock_run, remotes,
fetched, expected):
+ def fake_git(cmd, **kwargs):
+ if cmd[1] == "config":
+ stdout = "".join(f"remote.{name}.url {url}\n" for name, url in
remotes.items())
+ return CompletedProcess(args=cmd, returncode=0, stdout=stdout,
stderr="")
+ exists = cmd[-1].removeprefix("refs/remotes/").removesuffix("/main")
in fetched
+ return CompletedProcess(args=cmd, returncode=0 if exists else 1,
stdout="", stderr="")
+
+ mock_run.side_effect = fake_git
+ assert find_default_base_ref() == expected
+
+
[email protected](
+ ("found", "compared_with", "warned"),
+ [("upstream/main", "upstream/main", False), (None, "main", True)],
+)
+def test_verify_compares_with_the_default_base_ref(found, compared_with: str,
warned: bool):
+ with (
+ patch(
+ "airflow_breeze.commands.verify_commands.find_default_base_ref",
autospec=True, return_value=found
+ ),
+ patch(
+
"airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+ autospec=True,
+ return_value=False,
+ ),
+ patch(
+
"airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/docs/index.rst",),
+ ) as mock_files,
+ ):
+ result = CliRunner().invoke(verify, [], catch_exceptions=False)
+ assert result.exit_code == 0
+ mock_files.assert_called_once_with(compared_with)
+ output = " ".join(ANSI.sub("", result.output).split())
+ assert ("No git remote points at apache/airflow" in output) is warned
+ assert f"changed file(s) against {compared_with}," in output
+
+
+def _git(repo, *args: str) -> None:
+ subprocess.run(
+ [
+ "git",
+ "-c",
+ "user.name=t",
+ "-c",
+ "user.email=t@t",
+ "-c",
+ "commit.gpgsign=false",
+ "-c",
+ f"core.hooksPath={os.devnull}",
+ *args,
+ ],
+ cwd=repo,
+ check=True,
+ capture_output=True,
+ )
+
+
+def _commit(repo, name: str) -> None:
+ (repo / name).write_text(name)
+ _git(repo, "add", name)
+ _git(repo, "commit", "-m", name)
+
+
[email protected](
+ ("merge_main", "base", "expected"),
+ [
+ pytest.param(False, "stale-copy", False, id="never-merged"),
+ pytest.param(True, "stale-copy", True, id="merged-newer-than-base"),
+ pytest.param(True, "main", False, id="merged-and-base-fetched"),
+ ],
+)
+def test_detects_merged_commits_the_base_does_not_have(tmp_path, merge_main:
bool, base: str, expected: bool):
+ _git(tmp_path, "init", "-b", "main")
+ _commit(tmp_path, "B")
+ _git(tmp_path, "branch", "stale-copy")
+ _git(tmp_path, "switch", "-c", "feature")
+ _commit(tmp_path, "X")
+ _git(tmp_path, "switch", "main")
+ _commit(tmp_path, "C")
+ _git(tmp_path, "switch", "feature")
+ if merge_main:
+ _git(tmp_path, "merge", "--no-edit", "main")
+ with patch("airflow_breeze.commands.verify_commands.AIRFLOW_ROOT_PATH",
tmp_path):
+ assert has_merged_commits_missing_from(base) is expected
+
+
[email protected]("as_json", [False, True])
+@patch(
+ "airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+ autospec=True,
+ return_value=True,
+)
+@patch(
+ "airflow_breeze.commands.verify_commands.get_changed_files_against",
+ autospec=True,
+ return_value=("airflow-core/docs/index.rst",),
+)
+def test_merged_commits_missing_from_the_base_are_warned_about(mock_files,
mock_merged, as_json: bool):
+ result = CliRunner().invoke(
+ verify, ["--base-ref", "upstream/main", *(["--json"] if as_json else
[])], catch_exceptions=False
+ )
+ assert result.exit_code == 0
+ mock_merged.assert_called_once_with("upstream/main")
+ warning = "Your branch has merged commits that are not in upstream/main"
+ stream = result.stderr if as_json else result.stdout
+ assert warning in " ".join(ANSI.sub("", stream).split())
+ if as_json:
+ assert json.loads(result.stdout)["base_ref"] == "upstream/main"
diff --git a/dev/skill-evals/last-eval-hash.txt
b/dev/skill-evals/last-eval-hash.txt
index 6e5a9b0264e..451e4d1e3d4 100644
--- a/dev/skill-evals/last-eval-hash.txt
+++ b/dev/skill-evals/last-eval-hash.txt
@@ -1,3 +1,3 @@
# Generated by dev/skill-evals/eval.py — do not edit or resolve conflicts by
hand.
# Run `prek run run-skill-eval --hook-stage manual --all-files` to regenerate.
-062e019ffb2c0045c5676dd0b286ccfd25879eaa674abd768abe891a8ff3f70e
+d0e8ed3ebe9973449495c65634f02bfe2fb1a41e38bb4a09aa7d0199502b3f72