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 8c4c9908b8c Add `breeze pr stats` command for open PR statistics by
area (#64667)
8c4c9908b8c is described below
commit 8c4c9908b8ca7c9fac0faf2b1b94bf58ae389576
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Apr 3 23:25:45 2026 +0200
Add `breeze pr stats` command for open PR statistics by area (#64667)
Add a new `breeze pr stats` command that produces aggregate statistics
of open PRs in the Airflow repo, split by area label. The command uses
GraphQL to fetch all open PRs and displays two tables:
1. **Triaged PRs — Final State**: Shows triaged PRs that were closed or
merged since 2026-03-11, with counts and percentages for closed,
merged, and author-responded.
2. **Triaged PRs — Still Open**: Shows all open PRs grouped by area with
draft/non-draft counts, contributor counts, triage status, response
rates, ready-for-review status, time since triager drafted the PR,
and time since the author's last interaction.
Features:
- Cached interaction data (keyed by updated_at) to avoid re-fetching
unchanged PRs on subsequent runs
- `--clear-cache` flag to force fresh data
- Detects triager draft conversions via GraphQL timeline events
- Tracks closed vs merged triaged PRs with area breakdown
- Footer header rows on both tables for readability
---
dev/breeze/doc/13_pr_tasks.rst | 10 +
dev/breeze/doc/images/output_pr.svg | 12 +-
dev/breeze/doc/images/output_pr.txt | 2 +-
dev/breeze/doc/images/output_pr_stats.svg | 116 ++++
dev/breeze/doc/images/output_pr_stats.txt | 1 +
.../output_setup_check-all-params-in-groups.svg | 74 ++-
.../output_setup_check-all-params-in-groups.txt | 2 +-
.../output_setup_regenerate-command-images.svg | 6 +-
.../output_setup_regenerate-command-images.txt | 2 +-
.../src/airflow_breeze/commands/pr_commands.py | 706 +++++++++++++++++++++
.../airflow_breeze/commands/pr_commands_config.py | 13 +-
dev/breeze/src/airflow_breeze/utils/pr_cache.py | 1 +
uv.lock | 14 +-
13 files changed, 909 insertions(+), 50 deletions(-)
diff --git a/dev/breeze/doc/13_pr_tasks.rst b/dev/breeze/doc/13_pr_tasks.rst
index 4084bee61b8..d0f411f0703 100644
--- a/dev/breeze/doc/13_pr_tasks.rst
+++ b/dev/breeze/doc/13_pr_tasks.rst
@@ -440,6 +440,16 @@ Example usage
# Verbose mode — show individual skip reasons during filtering
breeze pr auto-triage --verbose
+PR statistics
+"""""""""""""
+
+The ``breeze pr stats`` command produces aggregate statistics of open PRs
grouped by area label.
+
+.. image:: ./images/output_pr_stats.svg
+ :target:
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_pr_stats.svg
+ :width: 100%
+ :alt: Breeze PR stats
+
-----
Next step: Follow the `Advanced breeze topics
<14_advanced_breeze_topics.rst>`__ instructions to learn how to manage GitHub
diff --git a/dev/breeze/doc/images/output_pr.svg
b/dev/breeze/doc/images/output_pr.svg
index 97cc4d5efe9..1051b307b09 100644
--- a/dev/breeze/doc/images/output_pr.svg
+++ b/dev/breeze/doc/images/output_pr.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 318.4"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 342.79999999999995"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -42,7 +42,7 @@
<defs>
<clipPath id="breeze-pr-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="267.4" />
+ <rect x="0" y="0" width="1463.0" height="291.79999999999995" />
</clipPath>
<clipPath id="breeze-pr-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -74,9 +74,12 @@
<clipPath id="breeze-pr-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-pr-line-10">
+ <rect x="0" y="245.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="316.4" rx="8"/><text class="breeze-pr-title"
fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: pr</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="340.8" rx="8"/><text class="breeze-pr-title"
fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: pr</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -96,7 +99,8 @@
</text><text class="breeze-pr-r5" x="0" y="190.8" textLength="1464"
clip-path="url(#breeze-pr-line-7)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-pr-r1" x="1464" y="190.8" textLength="12.2"
clip-path="url(#breeze-pr-line-7)">
</text><text class="breeze-pr-r5" x="0" y="215.2" textLength="24.4"
clip-path="url(#breeze-pr-line-8)">╭─</text><text class="breeze-pr-r5" x="24.4"
y="215.2" textLength="158.6"
clip-path="url(#breeze-pr-line-8)"> PR commands </text><text
class="breeze-pr-r5" x="183" y="215.2" textLength="1256.6"
clip-path="url(#breeze-pr-line-8)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
class="breeze-pr-r5" x="1439. [...]
</text><text class="breeze-pr-r5" x="0" y="239.6" textLength="12.2"
clip-path="url(#breeze-pr-line-9)">│</text><text class="breeze-pr-r4" x="24.4"
y="239.6" textLength="158.6"
clip-path="url(#breeze-pr-line-9)">auto-triage  </text><text
class="breeze-pr-r1" x="207.4" y="239.6" textLength="1232.2"
clip-path="url(#breeze-pr-line-9)">Find open PRs from non-collaborators that don't meet quality criteria and convert to
[...]
-</text><text class="breeze-pr-r5" x="0" y="264" textLength="1464"
clip-path="url(#breeze-pr-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-pr-r1" x="1464" y="264" textLength="12.2"
clip-path="url(#breeze-pr-line-10)">
+</text><text class="breeze-pr-r5" x="0" y="264" textLength="12.2"
clip-path="url(#breeze-pr-line-10)">│</text><text class="breeze-pr-r4" x="24.4"
y="264" textLength="158.6"
clip-path="url(#breeze-pr-line-10)">stats        </text><text
class="breeze-pr-r1" x="207.4" y="264" textLength="1232.2"
clip-path="url(#breeze-pr-line-10)">Show statistics of open PRs grouped by area label.     &#
[...]
+</text><text class="breeze-pr-r5" x="0" y="288.4" textLength="1464"
clip-path="url(#breeze-pr-line-11)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-pr-r1" x="1464" y="288.4" textLength="12.2"
clip-path="url(#breeze-pr-line-11)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_pr.txt
b/dev/breeze/doc/images/output_pr.txt
index 070bb74360c..37a9b1ce0a6 100644
--- a/dev/breeze/doc/images/output_pr.txt
+++ b/dev/breeze/doc/images/output_pr.txt
@@ -1 +1 @@
-e48502d9b2f145e867ce6608e8cd8c9d
+38d9d08de73f3c9bedb3ed7d3739b2c2
diff --git a/dev/breeze/doc/images/output_pr_stats.svg
b/dev/breeze/doc/images/output_pr_stats.svg
new file mode 100644
index 00000000000..3a6f8e38ade
--- /dev/null
+++ b/dev/breeze/doc/images/output_pr_stats.svg
@@ -0,0 +1,116 @@
+<svg class="rich-terminal" viewBox="0 0 1482 391.59999999999997"
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-pr-stats-matrix {
+ font-family: Fira Code, monospace;
+ font-size: 20px;
+ line-height: 24.4px;
+ font-variant-east-asian: full-width;
+ }
+
+ .breeze-pr-stats-title {
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }
+
+ .breeze-pr-stats-r1 { fill: #c5c8c6 }
+.breeze-pr-stats-r2 { fill: #d0b344 }
+.breeze-pr-stats-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-pr-stats-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-pr-stats-r5 { fill: #868887 }
+.breeze-pr-stats-r6 { fill: #8d7b39 }
+.breeze-pr-stats-r7 { fill: #98a84b;font-weight: bold }
+ </style>
+
+ <defs>
+ <clipPath id="breeze-pr-stats-clip-terminal">
+ <rect x="0" y="0" width="1463.0" height="340.59999999999997" />
+ </clipPath>
+ <clipPath id="breeze-pr-stats-line-0">
+ <rect x="0" y="1.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-1">
+ <rect x="0" y="25.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-2">
+ <rect x="0" y="50.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-3">
+ <rect x="0" y="74.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-4">
+ <rect x="0" y="99.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-5">
+ <rect x="0" y="123.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-6">
+ <rect x="0" y="147.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-7">
+ <rect x="0" y="172.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-8">
+ <rect x="0" y="196.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-9">
+ <rect x="0" y="221.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-10">
+ <rect x="0" y="245.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-11">
+ <rect x="0" y="269.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-pr-stats-line-12">
+ <rect x="0" y="294.3" 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="389.6" rx="8"/><text
class="breeze-pr-stats-title" fill="#c5c8c6" text-anchor="middle" x="740"
y="27">Command: pr stats</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-pr-stats-clip-terminal)">
+
+ <g class="breeze-pr-stats-matrix">
+ <text class="breeze-pr-stats-r1" x="1464" y="20" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-0)">
+</text><text class="breeze-pr-stats-r2" x="12.2" y="44.4" textLength="73.2"
clip-path="url(#breeze-pr-stats-line-1)">Usage:</text><text
class="breeze-pr-stats-r3" x="97.6" y="44.4" textLength="183"
clip-path="url(#breeze-pr-stats-line-1)">breeze pr stats</text><text
class="breeze-pr-stats-r1" x="292.8" y="44.4" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-1)">[</text><text
class="breeze-pr-stats-r4" x="305" y="44.4" textLength="85.4"
clip-path="url(#breeze-pr-stats-li [...]
+</text><text class="breeze-pr-stats-r1" x="1464" y="68.8" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-2)">
+</text><text class="breeze-pr-stats-r1" x="12.2" y="93.2" textLength="610"
clip-path="url(#breeze-pr-stats-line-3)">Show statistics of open PRs grouped by area label.</text><text
class="breeze-pr-stats-r1" x="1464" y="93.2" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-3)">
+</text><text class="breeze-pr-stats-r1" x="1464" y="117.6" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-4)">
+</text><text class="breeze-pr-stats-r5" x="0" y="142" textLength="24.4"
clip-path="url(#breeze-pr-stats-line-5)">╭─</text><text
class="breeze-pr-stats-r5" x="24.4" y="142" textLength="109.8"
clip-path="url(#breeze-pr-stats-line-5)"> Options </text><text
class="breeze-pr-stats-r5" x="134.2" y="142" textLength="1305.4"
clip-path="url(#breeze-pr-stats-line-5)">───────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
cl [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="166.4" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-6)">│</text><text
class="breeze-pr-stats-r4" x="24.4" y="166.4" textLength="231.8"
clip-path="url(#breeze-pr-stats-line-6)">--batch-size       </text><text
class="breeze-pr-stats-r1" x="329.4" y="166.4" textLength="500.2"
clip-path="url(#breeze-pr-stats-line-6)">Number of PRs to fetch per GraphQL page. </
[...]
+</text><text class="breeze-pr-stats-r5" x="0" y="190.8" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-7)">│</text><text
class="breeze-pr-stats-r4" x="24.4" y="190.8" textLength="231.8"
clip-path="url(#breeze-pr-stats-line-7)">--clear-cache      </text><text
class="breeze-pr-stats-r1" x="329.4" y="190.8" textLength="561.2"
clip-path="url(#breeze-pr-stats-line-7)">Clear cached interaction data before fetching.</text><text
clas [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="215.2" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-8)">│</text><text
class="breeze-pr-stats-r4" x="24.4" y="215.2" textLength="231.8"
clip-path="url(#breeze-pr-stats-line-8)">--github-token     </text><text
class="breeze-pr-stats-r1" x="329.4" y="215.2" textLength="512.4"
clip-path="url(#breeze-pr-stats-line-8)">The token used to authenticate to GitHub. </text><text
cla [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="239.6" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-9)">│</text><text
class="breeze-pr-stats-r4" x="24.4" y="239.6" textLength="231.8"
clip-path="url(#breeze-pr-stats-line-9)">--github-repository</text><text
class="breeze-pr-stats-r7" x="280.6" y="239.6" textLength="24.4"
clip-path="url(#breeze-pr-stats-line-9)">-g</text><text
class="breeze-pr-stats-r1" x="329.4" y="239.6" textLength="597.8"
clip-path="url(#breeze-pr-stats-line-9 [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="264" textLength="1464"
clip-path="url(#breeze-pr-stats-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-pr-stats-r1" x="1464" y="264" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-10)">
+</text><text class="breeze-pr-stats-r5" x="0" y="288.4" textLength="24.4"
clip-path="url(#breeze-pr-stats-line-11)">╭─</text><text
class="breeze-pr-stats-r5" x="24.4" y="288.4" textLength="195.2"
clip-path="url(#breeze-pr-stats-line-11)"> Common options </text><text
class="breeze-pr-stats-r5" x="219.6" y="288.4" textLength="1220"
clip-path="url(#breeze-pr-stats-line-11)">────────────────────────────────────────────────────────────────────────────────────────────────────</t
[...]
+</text><text class="breeze-pr-stats-r5" x="0" y="312.8" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-12)">│</text><text
class="breeze-pr-stats-r4" x="24.4" y="312.8" textLength="73.2"
clip-path="url(#breeze-pr-stats-line-12)">--help</text><text
class="breeze-pr-stats-r7" x="122" y="312.8" textLength="24.4"
clip-path="url(#breeze-pr-stats-line-12)">-h</text><text
class="breeze-pr-stats-r1" x="170.8" y="312.8" textLength="329.4"
clip-path="url(#breeze-pr-stats-line-12)">Show  [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="337.2" textLength="1464"
clip-path="url(#breeze-pr-stats-line-13)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-pr-stats-r1" x="1464" y="337.2" textLength="12.2"
clip-path="url(#breeze-pr-stats-line-13)">
+</text>
+ </g>
+ </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_pr_stats.txt
b/dev/breeze/doc/images/output_pr_stats.txt
new file mode 100644
index 00000000000..8d6aa1f2ce1
--- /dev/null
+++ b/dev/breeze/doc/images/output_pr_stats.txt
@@ -0,0 +1 @@
+1bcfe84fbd67e3e013325b945b3a8d8b
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 b33ae7f03e5..b43dd779612 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
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1148.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 1172.3999999999999"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -43,7 +43,7 @@
<defs>
<clipPath id="breeze-setup-check-all-params-in-groups-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="1097.0" />
+ <rect x="0" y="0" width="1463.0" height="1121.3999999999999" />
</clipPath>
<clipPath id="breeze-setup-check-all-params-in-groups-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -177,9 +177,12 @@
<clipPath id="breeze-setup-check-all-params-in-groups-line-43">
<rect x="0" y="1050.7" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-setup-check-all-params-in-groups-line-44">
+ <rect x="0" y="1075.1" 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="1146" rx="8"/><text
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: setup check-all-params-in-groups</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="1170.4" rx="8"/><text
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: setup check-all-params-in-groups</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -202,38 +205,39 @@
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="264"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-10)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="264"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-10)">| generate-migration-file | issues | issues:unassign | k8s | k8s:build-k8s-image | k8s:configure-cluster
[...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="288.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-11)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="288.4"
textLength="1171.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-11)">| k8s:create-cluster | k8s:delete-cluster | k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs |&#
[...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="312.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-12)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="312.8"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-12)">k8s:run-complete-tests | k8s:setup-env | k8s:shell | k8s:status | k8s:tests | k8s:upload-k8s-image |
[...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="337.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="337.2"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">pr:auto-triage | prod-image | prod-image:build | prod-image:load | prod-image:pull | prod-image:save |
[...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="361.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="361.6"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">prod-image:verify | registry | registry:backfill | registry:extract-data | registry:publish-versions | </te
[...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="386"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">release-management | release-management:add-back-references | release-management:check-release-files | </text><text
class="breeze-s [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="410.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="410.4"
textLength="1183.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">release-management:clean-old-provider-artifacts | release-management:constraints-version-check | </text><text
class="breeze-setup-check-a [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="434.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="434.8"
textLength="1012.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">release-management:create-minor-branch | release-management:generate-constraints | </text><text
class="breeze-setup-check-all-params-in-g [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="459.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="459.2"
textLength="1268.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">release-management:generate-issue-content-core | release-management:generate-issue-content-helm-chart | </text><text
class="breeze-setup- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="483.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="483.6"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">release-management:generate-issue-content-providers | release-management:generate-providers-metadata | </text><text
class="breeze-setup-c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="508"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="508"
textLength="1110.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">release-management:install-provider-distributions | release-management:merge-prod-images | </text><text
class="breeze-setup-check-all-params- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="532.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:prepare-airflow-ctl-distributions | release-management:prepare-airflow-distributions |</text><text
class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="556.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8"
textLength="1171.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:prepare-helm-chart-package | release-management:prepare-helm-chart-tarball | </text><text
class="breeze-setup-check-al [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="581.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2"
textLength="1268.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:prepare-provider-distributions | release-management:prepare-provider-documentation | </text><text
class="breeze-setup- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="605.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6"
textLength="976"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-python-client | release-management:prepare-tarball | </text><text
class="breeze-setup-check-all-params-in-groups- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630"
textLength="1049.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-task-sdk-distributions | release-management:publish-docs | </text><text
class="breeze-setup-check-all-params-in-gr [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="654.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4"
textLength="988.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:publish-docs-to-s3 | release-management:release-prod-images | </text><text
class="breeze-setup-check-all-params-in-grou [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="678.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:start-rc-process | release-management:start-release | release-management:tag-providers</text><text
class="breeze-setup-c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="703.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2"
textLength="1134.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">| release-management:update-constraints | release-management:update-providers-next-version | </text><text
class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="727.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6"
textLength="1244.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:verify-provider-distributions | release-management:verify-rc-by-pmc | run | sbom | </text><text
cl [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">sbom:build-all-airflow-images | sbom:export-dependency-information | sbom:generate-providers-requirements</text><text
class="breeze-setup-check [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="776.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4"
textLength="1183.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">| sbom:update-sbom-information | setup | setup:autocomplete | setup:check-all-params-in-groups | </text><text
cl [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="800.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">setup:config | setup:regenerate-command-images | setup:self-upgrade | setup:synchronize-local-mounts | </text><text
c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="825.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2"
textLength="1098"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:version | shell | start-airflow | testing | testing:airflow-ctl-integration-tests | </text><text
class= [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="849.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6"
textLength="1085.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">testing:airflow-ctl-tests | testing:airflow-e2e-tests | testing:core-integration-tests | </text><text
class="breeze-setup-check [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874"
textLength="890.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">testing:core-tests | testing:docker-compose-tests | testing:helm-tests | </text><text
class="breeze-setup-check-all-params-in-groups [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="898.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4"
textLength="1195.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">testing:providers-integration-tests | testing:providers-tests | testing:python-api-client-tests | </text><text
class="breeze-se [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="922.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="922.8"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">testing:system-tests | testing:task-sdk-integration-tests | testing:task-sdk-tests | testing:ui-e2e-tests</text><text
class="bree [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="947.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="947.2"
textLength="1268.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">| ui | ui:check-translation-completeness | ui:compile-assets | workflow-run | workflow-run:publish-docs)</text><
[...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="971.6" textLength="1464"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-setup-check-all-params-in-groups-r1" x="1464" 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-r5" x="0" y="996"
textLength="24.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">╭─</text><text
class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="996"
textLength="195.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)"> Common options </text><text
class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="996"
textLength="1220" clip-path="url(#breeze-setup-ch [...]
-</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-r4" x="24.4" y="1020.4"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">--verbose</text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1020.4"
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
-</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-r4" x="24.4" y="1044.8"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--dry-run</text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8"
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
-</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-r4" x="24.4" y="1069.2"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">--help   </text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1069.2"
textLength="24.4" clip-path="url(#breeze-setup-c [...]
-</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="337.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="337.2"
textLength="1171.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">pr:auto-triage | pr:stats | prod-image | prod-image:build | prod-image:load | prod-image:pull | <
[...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="361.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="361.6"
textLength="1134.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">prod-image:save | prod-image:verify | registry | registry:backfill | registry:extract-data | </text><text
c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="386"
textLength="1098"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">registry:publish-versions | release-management | release-management:add-back-references | </text><text
class="breeze-setup-check-all- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="410.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="410.4"
textLength="1110.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">release-management:check-release-files | release-management:clean-old-provider-artifacts | </text><text
class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="434.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="434.8"
textLength="1073.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">release-management:constraints-version-check | release-management:create-minor-branch | </text><text
class="breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="459.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="459.2"
textLength="1110.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">release-management:generate-constraints | release-management:generate-issue-content-core | </text><text
class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="483.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="483.6"
textLength="671"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">release-management:generate-issue-content-helm-chart | </text><text
class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="483.6"
textLength [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="508"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="508"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">release-management:generate-issue-content-providers | release-management:generate-providers-metadata | </text><text
class="breeze-setup-check [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="532.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4"
textLength="1110.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:install-provider-distributions | release-management:merge-prod-images | </text><text
class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="556.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:prepare-airflow-ctl-distributions | release-management:prepare-airflow-distributions |</text><text
class="breeze-setup-check- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="581.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2"
textLength="1171.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:prepare-helm-chart-package | release-management:prepare-helm-chart-tarball | </text><text
class="breeze-setup-check-al [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="605.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6"
textLength="1268.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-provider-distributions | release-management:prepare-provider-documentation | </text><text
class="breeze-setup- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630"
textLength="976"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-python-client | release-management:prepare-tarball | </text><text
class="breeze-setup-check-all-params-in-groups-r5" [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="654.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4"
textLength="1049.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-task-sdk-distributions | release-management:publish-docs | </text><text
class="breeze-setup-check-all-params-i [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="678.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8"
textLength="988.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:publish-docs-to-s3 | release-management:release-prod-images | </text><text
class="breeze-setup-check-all-params-in-grou [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="703.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:start-rc-process | release-management:start-release | release-management:tag-providers</text><text
class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="727.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6"
textLength="1134.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">| release-management:update-constraints | release-management:update-providers-next-version | </text><text
class="breeze-setup-check- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752"
textLength="1244.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:verify-provider-distributions | release-management:verify-rc-by-pmc | run | sbom | </text><text
class= [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="776.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">sbom:build-all-airflow-images | sbom:export-dependency-information | sbom:generate-providers-requirements</text><text
class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="800.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8"
textLength="1183.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">| sbom:update-sbom-information | setup | setup:autocomplete | setup:check-all-params-in-groups | </text><text
cl [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="825.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2"
textLength="1256.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:config | setup:regenerate-command-images | setup:self-upgrade | setup:synchronize-local-mounts | </text><text
c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="849.6" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6"
textLength="1098"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">setup:version | shell | start-airflow | testing | testing:airflow-ctl-integration-tests | </text><text
class= [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874"
textLength="1085.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">testing:airflow-ctl-tests | testing:airflow-e2e-tests | testing:core-integration-tests | </text><text
class="breeze-setup-check-all [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="898.4" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4"
textLength="890.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">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="922.8" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="922.8"
textLength="1195.6"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">testing:providers-integration-tests | testing:providers-tests | testing:python-api-client-tests | </text><text
class="breeze-se [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
y="947.2" textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="947.2"
textLength="1281"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">testing:system-tests | testing:task-sdk-integration-tests | testing:task-sdk-tests | testing:ui-e2e-tests</text><text
class="bree [...]
+</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="1268.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">| ui | ui:check-translation-completeness | ui:compile-assets | workflow-run | workflow-run:publish-docs)</text><
[...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996"
textLength="1464"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-setup-check-all-params-in-groups-r1" x="1464" 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-r5" x="0"
y="1020.4" textLength="24.4"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">╭─</text><text
class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="1020.4"
textLength="195.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)"> Common options </text><text
class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="1020.4"
textLength="1220" clip-path="url(#breeze [...]
+</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-r4" x="24.4" y="1044.8"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--verbose</text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8"
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</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-r4" x="24.4" y="1069.2"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">--dry-run</text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1069.2"
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0"
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-r4" x="24.4" y="1093.6"
textLength="109.8"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">--help   </text><text
class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1093.6"
textLength="24.4" clip-path="url(#breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="1118"
textLength="1464"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1118"
textLength="12.2"
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">
</text>
</g>
</g>
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 4b4b042063c..bd0ef796d4b 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 @@
-acfe23e5b4622df765994caf52f455d2
+eba55480dd7f88affd08c6f609ad8c40
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 d44be5b47ad..2d6b2bae76c 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -221,9 +221,9 @@
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="288.4"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-11)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="288.4"
textLength="1122.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-11)">cleanup | doctor | down | exec | generate-migration-file | issues | issues:unassign | k8s&#
[...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="312.8"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-12)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="312.8"
textLength="1073.6"
clip-path="url(#breeze-setup-regenerate-command-images-line-12)">k8s:build-k8s-image | k8s:configure-cluster | k8s:create-cluster | k8s:delete-cluster | </text><text
class="breeze-setup- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-13)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="337.2"
textLength="1244.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-13)">k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs | k8s:run-complete-tests | k8s:setup-env | k8s:shel
[...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="361.6"
textLength="1244.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">| k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | prod-image | prod-image:
[...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="386"
textLength="1061.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">| prod-image:load | prod-image:pull | prod-image:save | prod-image:verify | registry | </text><text
class="bre [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="410.4"
textLength="1134.6"
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">registry:backfill | registry:extract-data | registry:publish-versions | release-management | </text><text
class="breeze-s [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="361.6"
textLength="1171.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">| k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | pr:stats | prod-image
[...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="386"
textLength="1244.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">prod-image:build | prod-image:load | prod-image:pull | prod-image:save | prod-image:verify | registry </text><
[...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="410.4"
textLength="1159"
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">| registry:backfill | registry:extract-data | registry:publish-versions | release-management | </text><text
class="bre [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-17)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="434.8"
textLength="1000.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-17)">release-management:add-back-references | release-management:check-release-files | </text><text
class="breeze-setup-regenerate-command-images- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-18)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="459.2"
textLength="1183.4"
clip-path="url(#breeze-setup-regenerate-command-images-line-18)">release-management:clean-old-provider-artifacts | release-management:constraints-version-check | </text><text
class="breeze-setup-regenerate- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="483.6"
textLength="12.2"
clip-path="url(#breeze-setup-regenerate-command-images-line-19)">│</text><text
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="483.6"
textLength="1012.6"
clip-path="url(#breeze-setup-regenerate-command-images-line-19)">release-management:create-minor-branch | release-management:generate-constraints | </text><text
class="breeze-setup-regenerate-command-images [...]
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 5cb537dfca9..45cb2a115ab 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 @@
-cd2b1818493fedb67afad21a6a46f98d
+3b18680cf71a6fe2ac14af44b029a537
diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands.py
b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
index 104f4e9b062..bdf810ab3e0 100644
--- a/dev/breeze/src/airflow_breeze/commands/pr_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
@@ -10951,3 +10951,709 @@ def auto_triage(
import os as _os
_os._exit(0)
+
+
+# ---------------------------------------------------------------------------
+# breeze pr stats — aggregate statistics of open PRs by area
+# ---------------------------------------------------------------------------
+
+_AGE_BUCKET_LABELS = ["<1w", "1-2w", "2-4w", ">1m"]
+
+_AGE_BUCKET_THRESHOLDS: list[tuple[int, str]] = [
+ (7, "<1w"),
+ (14, "1-2w"),
+ (28, "2-4w"),
+]
+
+
+_DRAFT_AGE_BUCKET_LABELS = ["<1w", "1-2w", "2-4w", ">1m"]
+
+
+@dataclass
+class _AreaStats:
+ """Aggregated statistics for a single area label."""
+
+ total: int = 0
+ drafts: int = 0
+ non_drafts: int = 0
+ contributors: int = 0 # non-collaborator authors
+ triaged_waiting: int = 0
+ triaged_responded: int = 0
+ ready_for_review: int = 0
+ triager_drafted: int = 0
+ draft_age_buckets: dict[str, int] = field(
+ default_factory=lambda: {b: 0 for b in _DRAFT_AGE_BUCKET_LABELS}
+ )
+ age_buckets: dict[str, int] = field(default_factory=lambda: {b: 0 for b in
_AGE_BUCKET_LABELS})
+
+
+def _compute_age_bucket(last_interaction_iso: str) -> str:
+ """Map an ISO-8601 datetime string to an age bucket label."""
+ from datetime import datetime, timezone
+
+ try:
+ dt = datetime.fromisoformat(last_interaction_iso.replace("Z",
"+00:00"))
+ except (ValueError, AttributeError):
+ return ">1m"
+ delta = datetime.now(tz=timezone.utc) - dt
+ days = delta.total_seconds() / 86400
+ for threshold_days, label in _AGE_BUCKET_THRESHOLDS:
+ if days < threshold_days:
+ return label
+ return ">1m"
+
+
+def _fetch_all_open_prs(token: str, github_repository: str, batch_size: int =
100) -> list[PRData]:
+ """Fetch all open PRs from the repository, paginating through all
results."""
+ console = get_console()
+ all_prs: list[PRData] = []
+ after_cursor: str | None = None
+ page = 0
+
+ while True:
+ page += 1
+ prs, has_next_page, end_cursor, total_count = _fetch_prs_graphql(
+ token,
+ github_repository,
+ labels=(),
+ exclude_labels=(),
+ filter_user=None,
+ sort="created-desc",
+ batch_size=batch_size,
+ after_cursor=after_cursor,
+ quiet=page > 1,
+ )
+ all_prs.extend(prs)
+ if page == 1:
+ console.print(f"[info]Total open PRs: {total_count}[/]")
+ else:
+ console.print(f"[info] Fetched page {page}:
{len(all_prs)}/{total_count} PRs[/]")
+
+ if not has_next_page:
+ break
+ after_cursor = end_cursor
+
+ return all_prs
+
+
+@dataclass
+class _PRInteractionData:
+ """Interaction data for a single PR."""
+
+ last_interaction: str # ISO-8601 datetime of last author interaction
+ drafted_by_triager_at: str # ISO-8601 datetime when triager converted to
draft, or ""
+
+
+def _fetch_pr_interaction_data(
+ token: str, github_repository: str, prs: list[PRData], viewer_login: str
+) -> dict[int, _PRInteractionData]:
+ """Fetch interaction data for each PR: last author activity and triager
draft conversion.
+
+ Uses a file-based cache keyed by PR number and ``updated_at`` — if a PR
hasn't
+ been updated since the last run, the cached data is reused.
+ """
+ from airflow_breeze.utils.pr_cache import stats_interaction_cache
+
+ if not prs:
+ return {}
+
+ owner, repo = github_repository.split("/", 1)
+ result: dict[int, _PRInteractionData] = {}
+
+ # Check cache — only fetch PRs whose updated_at changed since last cache
write
+ uncached_prs: list[PRData] = []
+ for pr in prs:
+ cached = stats_interaction_cache.get(
+ github_repository, f"pr_{pr.number}", match={"updated_at":
pr.updated_at}
+ )
+ if cached and "last_interaction" in cached:
+ result[pr.number] = _PRInteractionData(
+ last_interaction=cached["last_interaction"],
+ drafted_by_triager_at=cached.get("drafted_by_triager_at", ""),
+ )
+ else:
+ uncached_prs.append(pr)
+
+ console = get_console()
+ cache_hits = len(prs) - len(uncached_prs)
+ console.print(
+ f"[info]Fetching PR interaction data: {cache_hits}/{len(prs)} cached, "
+ f"{len(uncached_prs)} to fetch...[/]"
+ )
+
+ if not uncached_prs:
+ return result
+
+ chunk_size = _COMMITS_BEHIND_BATCH_SIZE # reuse existing batch constant
(20)
+
+ for chunk_start in range(0, len(uncached_prs), chunk_size):
+ chunk = uncached_prs[chunk_start : chunk_start + chunk_size]
+
+ pr_fields = []
+ for pr in chunk:
+ alias = f"pr{pr.number}"
+ pr_fields.append(
+ f" {alias}: pullRequest(number: {pr.number}) {{\n"
+ f" comments(last: 10) {{\n"
+ f" nodes {{ author {{ login }} createdAt }}\n"
+ f" }}\n"
+ f" commits(last: 1) {{\n"
+ f" nodes {{ commit {{ committedDate }} }}\n"
+ f" }}\n"
+ f" timelineItems(last: 10, itemTypes:
CONVERT_TO_DRAFT_EVENT) {{\n"
+ f" nodes {{\n"
+ f" ... on ConvertToDraftEvent {{\n"
+ f" createdAt\n"
+ f" actor {{ login }}\n"
+ f" }}\n"
+ f" }}\n"
+ f" }}\n"
+ f" }}"
+ )
+
+ query = (
+ f'query {{\n repository(owner: "{owner}", name: "{repo}") {{\n'
+ + "\n".join(pr_fields)
+ + "\n }\n}"
+ )
+
+ try:
+ data = _graphql_request(token, query, {})
+ except SystemExit:
+ # On API failure, fall back to updated_at for this chunk
+ for pr in chunk:
+ result[pr.number] = _PRInteractionData(
+ last_interaction=pr.updated_at or pr.created_at,
+ drafted_by_triager_at="",
+ )
+ continue
+
+ repo_data = data.get("repository", {})
+ for pr in chunk:
+ alias = f"pr{pr.number}"
+ pr_data = repo_data.get(alias) or {}
+
+ # Start with PR creation as the baseline
+ latest = pr.created_at
+
+ # Check last commit date
+ commits = pr_data.get("commits", {}).get("nodes", [])
+ if commits:
+ commit_date = commits[0].get("commit",
{}).get("committedDate", "")
+ if commit_date and commit_date > latest:
+ latest = commit_date
+
+ # Check last comment from the PR author
+ comments = pr_data.get("comments", {}).get("nodes", [])
+ for comment in reversed(comments):
+ comment_author = (comment.get("author") or {}).get("login", "")
+ if comment_author == pr.author_login:
+ comment_date = comment.get("createdAt", "")
+ if comment_date and comment_date > latest:
+ latest = comment_date
+ break
+
+ # Check if the triager (viewer) converted this PR to draft
+ drafted_by_triager_at = ""
+ timeline_nodes = pr_data.get("timelineItems", {}).get("nodes", [])
+ for event in reversed(timeline_nodes):
+ actor = (event.get("actor") or {}).get("login", "")
+ if actor == viewer_login:
+ drafted_by_triager_at = event.get("createdAt", "")
+ break
+
+ result[pr.number] = _PRInteractionData(
+ last_interaction=latest,
+ drafted_by_triager_at=drafted_by_triager_at,
+ )
+ # Cache the result keyed by updated_at so it invalidates on any PR
change
+ stats_interaction_cache.save(
+ github_repository,
+ f"pr_{pr.number}",
+ {
+ "updated_at": pr.updated_at,
+ "last_interaction": latest,
+ "drafted_by_triager_at": drafted_by_triager_at,
+ },
+ )
+
+ if chunk_start + chunk_size < len(uncached_prs):
+ console.print(
+ f"[info] Fetched interactions: "
+ f"{min(chunk_start + chunk_size,
len(uncached_prs))}/{len(uncached_prs)}[/]"
+ )
+
+ return result
+
+
+def _aggregate_stats_by_area(
+ prs: list[PRData],
+ triage_classification: dict[str, set[int]],
+ interaction_data: dict[int, _PRInteractionData],
+) -> dict[str, _AreaStats]:
+ """Aggregate PR statistics grouped by area: labels."""
+ waiting = triage_classification.get("waiting", set())
+ responded = triage_classification.get("responded", set())
+ area_stats: dict[str, _AreaStats] = {}
+
+ for pr in prs:
+ # Extract area labels
+ areas = [label.removeprefix("area:") for label in pr.labels if
label.startswith("area:")]
+ if not areas:
+ areas = ["(no area)"]
+
+ pr_data = interaction_data.get(pr.number)
+ last_interaction = pr_data.last_interaction if pr_data else
pr.created_at
+ drafted_at = pr_data.drafted_by_triager_at if pr_data else ""
+
+ age_bucket = _compute_age_bucket(last_interaction)
+ is_ready = _READY_FOR_REVIEW_LABEL in pr.labels
+ is_triaged_waiting = pr.number in waiting
+ is_triaged_responded = pr.number in responded
+
+ is_contributor = pr.author_association not in
_COLLABORATOR_ASSOCIATIONS
+
+ for area in areas:
+ stats = area_stats.setdefault(area, _AreaStats())
+ stats.total += 1
+ if pr.is_draft:
+ stats.drafts += 1
+ else:
+ stats.non_drafts += 1
+ if is_contributor:
+ stats.contributors += 1
+ if is_triaged_waiting:
+ stats.triaged_waiting += 1
+ if is_triaged_responded:
+ stats.triaged_responded += 1
+ if is_ready:
+ stats.ready_for_review += 1
+ stats.age_buckets[age_bucket] += 1
+ if drafted_at:
+ stats.triager_drafted += 1
+ draft_bucket = _compute_age_bucket(drafted_at)
+ stats.draft_age_buckets[draft_bucket] += 1
+
+ return area_stats
+
+
+def _compute_totals(
+ prs: list[PRData],
+ triage_classification: dict[str, set[int]],
+ interaction_data: dict[int, _PRInteractionData],
+) -> _AreaStats:
+ """Compute totals across all PRs (unique counts, not sum of per-area)."""
+ waiting = triage_classification.get("waiting", set())
+ responded = triage_classification.get("responded", set())
+ totals = _AreaStats()
+ for pr in prs:
+ totals.total += 1
+ if pr.is_draft:
+ totals.drafts += 1
+ else:
+ totals.non_drafts += 1
+ if pr.author_association not in _COLLABORATOR_ASSOCIATIONS:
+ totals.contributors += 1
+ if pr.number in waiting:
+ totals.triaged_waiting += 1
+ if pr.number in responded:
+ totals.triaged_responded += 1
+ if _READY_FOR_REVIEW_LABEL in pr.labels:
+ totals.ready_for_review += 1
+ pr_data = interaction_data.get(pr.number)
+ last_interaction = pr_data.last_interaction if pr_data else
pr.created_at
+ drafted_at = pr_data.drafted_by_triager_at if pr_data else ""
+ totals.age_buckets[_compute_age_bucket(last_interaction)] += 1
+ if drafted_at:
+ totals.triager_drafted += 1
+ totals.draft_age_buckets[_compute_age_bucket(drafted_at)] += 1
+ return totals
+
+
+def _pct(numerator: int, denominator: int) -> str:
+ """Format a percentage, returning '-' when the denominator is zero."""
+ return f"{100 * numerator / denominator:.0f}%" if denominator else "-"
+
+
+def _render_stats_tables(
+ area_stats: dict[str, _AreaStats],
+ totals: _AreaStats,
+ github_repository: str,
+ closed_stats: dict[str, _ClosedTriagedAreaStats],
+ closed_since_date: str,
+) -> None:
+ """Render two tables: triaged final-state and triaged still-open."""
+ console = get_console()
+ closed_totals = closed_stats.get("__total__", _ClosedTriagedAreaStats())
+
+ # Collect all area names across both open and closed stats
+ all_areas = set(area_stats.keys()) | {k for k in closed_stats if k !=
"__total__"}
+ sorted_area_names = sorted(
+ all_areas, key=lambda a: (a == "(no area)", -(area_stats.get(a,
_AreaStats()).total))
+ )
+
+ # ── Table 1: Triaged PRs — Final State (closed/merged) ──────────
+ t1 = Table(
+ title=f"Triaged PRs — Final State since {closed_since_date}
({github_repository})",
+ title_style="bold",
+ show_lines=True,
+ show_footer=True,
+ )
+ t1.add_column("Area", style="bold cyan", min_width=12, footer="Area")
+ t1.add_column("Triaged\nTotal", justify="right", style="yellow",
footer="Triaged\nTotal")
+ t1.add_column("Closed", justify="right", style="red", footer="Closed")
+ t1.add_column("%Closed", justify="right", footer="%Closed")
+ t1.add_column("Merged", justify="right", style="green", footer="Merged")
+ t1.add_column("%Merged", justify="right", footer="%Merged")
+ t1.add_column("Responded", justify="right", footer="Responded")
+ t1.add_column("%Responded", justify="right", footer="%Responded")
+
+ for area in sorted_area_names:
+ cs = closed_stats.get(area, _ClosedTriagedAreaStats())
+ if cs.total == 0:
+ continue
+ t1.add_row(
+ area,
+ str(cs.total),
+ str(cs.closed),
+ cs.pct_closed,
+ str(cs.merged),
+ cs.pct_merged,
+ str(cs.responded_before_close),
+ cs.pct_responded,
+ )
+
+ t1.add_row(
+ "[bold white]TOTAL[/]",
+ f"[bold white]{closed_totals.total}[/]",
+ f"[bold white]{closed_totals.closed}[/]",
+ f"[bold white]{closed_totals.pct_closed}[/]",
+ f"[bold white]{closed_totals.merged}[/]",
+ f"[bold white]{closed_totals.pct_merged}[/]",
+ f"[bold white]{closed_totals.responded_before_close}[/]",
+ f"[bold white]{closed_totals.pct_responded}[/]",
+ style="on grey7",
+ end_section=True,
+ )
+
+ console.print()
+ console.print(t1)
+
+ # ── Table 2: Triaged PRs — Still Open ────────────────────────────
+ t2 = Table(
+ title=f"Triaged PRs — Still Open ({github_repository})",
+ title_style="bold",
+ show_lines=True,
+ show_footer=True,
+ )
+ t2.add_column("Area", style="bold cyan", min_width=12, footer="Area")
+ t2.add_column("Total", justify="right", footer="Total")
+ t2.add_column("Draft", justify="right", footer="Draft")
+ t2.add_column("%Draft", justify="right", footer="%Draft")
+ t2.add_column("Non-Draft", justify="right", footer="Non-Draft")
+ t2.add_column("Contrib.", justify="right", footer="Contrib.")
+ t2.add_column("%Contrib.", justify="right", footer="%Contrib.")
+ t2.add_column("Triaged", justify="right", style="yellow", footer="Triaged")
+ t2.add_column("Responded", justify="right", style="green",
footer="Responded")
+ t2.add_column("%Responded", justify="right", footer="%Responded")
+ t2.add_column("Ready", justify="right", style="bold green", footer="Ready")
+ t2.add_column("%Ready", justify="right", footer="%Ready")
+ t2.add_column("Drafted\nby triager", justify="right", style="magenta",
footer="Drafted\nby triager")
+ for bucket in _DRAFT_AGE_BUCKET_LABELS:
+ t2.add_column(f"Drafted\n{bucket}", justify="right", style="magenta
dim", footer=f"Drafted\n{bucket}")
+ for bucket in _AGE_BUCKET_LABELS:
+ t2.add_column(f"Author resp\n{bucket}", justify="right", style="dim",
footer=f"Author resp\n{bucket}")
+
+ for area in sorted_area_names:
+ s = area_stats.get(area)
+ if not s or s.total == 0:
+ continue
+ triaged = s.triaged_waiting + s.triaged_responded
+ t2.add_row(
+ area,
+ str(s.total),
+ str(s.drafts),
+ _pct(s.drafts, s.total),
+ str(s.non_drafts),
+ str(s.contributors),
+ _pct(s.contributors, s.total),
+ str(triaged),
+ str(s.triaged_responded),
+ _pct(s.triaged_responded, triaged),
+ str(s.ready_for_review),
+ _pct(s.ready_for_review, s.total),
+ str(s.triager_drafted),
+ *[str(s.draft_age_buckets.get(b, 0)) for b in
_DRAFT_AGE_BUCKET_LABELS],
+ *[str(s.age_buckets.get(b, 0)) for b in _AGE_BUCKET_LABELS],
+ )
+
+ total_triaged = totals.triaged_waiting + totals.triaged_responded
+ t2.add_row(
+ "[bold white]TOTAL[/]",
+ f"[bold white]{totals.total}[/]",
+ f"[bold white]{totals.drafts}[/]",
+ f"[bold white]{_pct(totals.drafts, totals.total)}[/]",
+ f"[bold white]{totals.non_drafts}[/]",
+ f"[bold white]{totals.contributors}[/]",
+ f"[bold white]{_pct(totals.contributors, totals.total)}[/]",
+ f"[bold white]{total_triaged}[/]",
+ f"[bold white]{totals.triaged_responded}[/]",
+ f"[bold white]{_pct(totals.triaged_responded, total_triaged)}[/]",
+ f"[bold white]{totals.ready_for_review}[/]",
+ f"[bold white]{_pct(totals.ready_for_review, totals.total)}[/]",
+ f"[bold white]{totals.triager_drafted}[/]",
+ *[f"[bold white]{totals.draft_age_buckets.get(b, 0)}[/]" for b in
_DRAFT_AGE_BUCKET_LABELS],
+ *[f"[bold white]{totals.age_buckets.get(b, 0)}[/]" for b in
_AGE_BUCKET_LABELS],
+ style="on grey7",
+ end_section=True,
+ )
+
+ console.print()
+ console.print(t2)
+
+ # ── Legend ────────────────────────────────────────────────────────
+ legend_lines = [
+ "[bold]Column legend:[/]",
+ " [bold]Contrib.[/] = PRs by non-collaborator contributors",
+ " [yellow]Triaged[/] = PRs where a triage comment was posted",
+ " [green]Responded[/] = author replied after the triage comment",
+ " [bold green]Ready[/] = PRs with the [bold]'ready for
maintainer review'[/] label",
+ " [magenta]Drafted by triager[/] = PRs converted to draft by the
triager",
+ "",
+ "[bold]Author resp[/] columns show time since the PR author's last
interaction "
+ "(comment, commit, or PR creation).",
+ "[bold magenta]Drafted[/] columns show time since the triager
converted the PR to draft.",
+ ]
+ console.print(Panel("\n".join(legend_lines), border_style="dim",
expand=False))
+ console.print()
+
+
+_CLOSED_TRIAGED_SINCE = "2026-03-11"
+
+_CLOSED_PRS_SEARCH_QUERY = """
+query($query: String!, $first: Int!, $after: String) {
+ search(query: $query, type: ISSUE, first: $first, after: $after) {
+ issueCount
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ nodes {
+ ... on PullRequest {
+ number
+ author { login }
+ authorAssociation
+ state
+ mergedAt
+ closedAt
+ labels(first: 20) {
+ nodes { name }
+ }
+ comments(last: 20) {
+ nodes { author { login } body createdAt }
+ }
+ }
+ }
+ }
+}
+"""
+
+
+@dataclass
+class _ClosedTriagedAreaStats:
+ """Per-area stats for triaged PRs that reached a final state."""
+
+ closed: int = 0
+ merged: int = 0
+ responded_before_close: int = 0
+ contributors: int = 0 # non-collaborator authors
+
+ @property
+ def total(self) -> int:
+ return self.closed + self.merged
+
+ @property
+ def pct_closed(self) -> str:
+ return f"{100 * self.closed / self.total:.0f}%" if self.total else "-"
+
+ @property
+ def pct_merged(self) -> str:
+ return f"{100 * self.merged / self.total:.0f}%" if self.total else "-"
+
+ @property
+ def pct_responded(self) -> str:
+ return f"{100 * self.responded_before_close / self.total:.0f}%" if
self.total else "-"
+
+ @property
+ def pct_contributors(self) -> str:
+ return f"{100 * self.contributors / self.total:.0f}%" if self.total
else "-"
+
+
+def _fetch_closed_triaged_prs(
+ token: str, github_repository: str, viewer_login: str, since: str
+) -> dict[str, _ClosedTriagedAreaStats]:
+ """Fetch closed/merged PRs since *since* that had a triage comment.
+
+ Returns a dict mapping area label (or "(no area)") to stats.
+ Also includes a "__total__" key with aggregate counts.
+ """
+ console = get_console()
+ console.print(f"[info]Fetching closed/merged triaged PRs since
{since}...[/]")
+
+ search_query = (
+ f"repo:{github_repository} type:pr is:closed closed:>={since} "
+ f"commenter:{viewer_login} sort:updated-desc"
+ )
+
+ area_stats: dict[str, _ClosedTriagedAreaStats] = {}
+ total_stats = _ClosedTriagedAreaStats()
+ after_cursor: str | None = None
+
+ while True:
+ variables: dict = {"query": search_query, "first": 100}
+ if after_cursor:
+ variables["after"] = after_cursor
+
+ try:
+ data = _graphql_request(token, _CLOSED_PRS_SEARCH_QUERY, variables)
+ except SystemExit:
+ break
+
+ search_data = data["search"]
+ for node in search_data.get("nodes") or []:
+ if not node:
+ continue
+ comments = (node.get("comments") or {}).get("nodes") or []
+ author_login = (node.get("author") or {}).get("login", "")
+
+ # Check if any comment from viewer contains the triage marker
+ triage_comment_date = ""
+ for comment in reversed(comments):
+ commenter = (comment.get("author") or {}).get("login", "")
+ body = comment.get("body", "")
+ if commenter == viewer_login and _TRIAGE_COMMENT_MARKER in
body:
+ triage_comment_date = comment.get("createdAt", "")
+ break
+
+ if not triage_comment_date:
+ continue
+
+ # Determine state: MERGED or CLOSED
+ is_merged = node.get("state") == "MERGED" or
bool(node.get("mergedAt"))
+ is_contributor = node.get("authorAssociation", "") not in
_COLLABORATOR_ASSOCIATIONS
+
+ # Extract area labels
+ labels = [n["name"] for n in (node.get("labels") or
{}).get("nodes") or []]
+ areas = [lbl.removeprefix("area:") for lbl in labels if
lbl.startswith("area:")]
+ if not areas:
+ areas = ["(no area)"]
+
+ # Check if author responded after triage
+ author_responded = False
+ for comment in comments:
+ commenter = (comment.get("author") or {}).get("login", "")
+ comment_date = comment.get("createdAt", "")
+ if commenter == author_login and comment_date >
triage_comment_date:
+ author_responded = True
+ break
+
+ # Update per-area stats
+ for area in areas:
+ stats = area_stats.setdefault(area, _ClosedTriagedAreaStats())
+ if is_merged:
+ stats.merged += 1
+ else:
+ stats.closed += 1
+ if author_responded:
+ stats.responded_before_close += 1
+ if is_contributor:
+ stats.contributors += 1
+
+ # Update totals
+ if is_merged:
+ total_stats.merged += 1
+ else:
+ total_stats.closed += 1
+ if author_responded:
+ total_stats.responded_before_close += 1
+ if is_contributor:
+ total_stats.contributors += 1
+
+ page_info = search_data.get("pageInfo", {})
+ if not page_info.get("hasNextPage", False):
+ break
+ after_cursor = page_info.get("endCursor")
+
+ area_stats["__total__"] = total_stats
+ console.print(
+ f"[info]Found {total_stats.total} triaged PRs in final state "
+ f"({total_stats.closed} closed, {total_stats.merged} merged) since
{since}.[/]"
+ )
+ return area_stats
+
+
+@pr_group.command(name="stats", help="Show statistics of open PRs grouped by
area label.")
+@option_github_token
+@option_github_repository
[email protected](
+ "--batch-size",
+ type=int,
+ default=100,
+ show_default=True,
+ help="Number of PRs to fetch per GraphQL page.",
+)
[email protected](
+ "--clear-cache",
+ is_flag=True,
+ default=False,
+ help="Clear cached interaction data before fetching.",
+)
+def stats(github_token: str | None, github_repository: str, batch_size: int,
clear_cache: bool) -> None:
+ """Produce aggregate statistics of open PRs, split by area."""
+ from airflow_breeze.utils.pr_cache import stats_interaction_cache
+
+ token = _resolve_github_token(github_token)
+ if not token:
+ console_print("[error]GitHub token is required. Use --github-token or
set GITHUB_TOKEN.[/]")
+ sys.exit(1)
+
+ console = get_console()
+
+ if clear_cache:
+ import shutil
+
+ cache_dir = stats_interaction_cache.cache_dir(github_repository)
+ shutil.rmtree(cache_dir, ignore_errors=True)
+ console.print("[info]Cleared interaction cache.[/]")
+
+ # Step 1: Fetch all open PRs
+ all_prs = _fetch_all_open_prs(token, github_repository, batch_size)
+ if not all_prs:
+ console.print("[warning]No open PRs found.[/]")
+ return
+
+ # Step 2: Resolve viewer login and classify triage status
+ viewer_login = _resolve_viewer_login(token)
+ console.print(f"[info]Classifying triage status (viewer:
{viewer_login})...[/]")
+ triage_classification = _classify_already_triaged_prs(token,
github_repository, all_prs, viewer_login)
+
+ # Step 3: Fetch interaction data (last author activity + triager draft
conversion)
+ interaction_data = _fetch_pr_interaction_data(token, github_repository,
all_prs, viewer_login)
+
+ # Step 4: Aggregate by area
+ area_stats = _aggregate_stats_by_area(all_prs, triage_classification,
interaction_data)
+
+ # Step 5: Compute totals (unique PR counts)
+ totals = _compute_totals(all_prs, triage_classification, interaction_data)
+
+ # Step 6: Fetch closed/merged triaged PRs since March 11, 2026
+ closed_stats = _fetch_closed_triaged_prs(token, github_repository,
viewer_login, _CLOSED_TRIAGED_SINCE)
+
+ # Step 7: Render
+ _render_stats_tables(
+ area_stats,
+ totals,
+ github_repository,
+ closed_stats=closed_stats,
+ closed_since_date=_CLOSED_TRIAGED_SINCE,
+ )
diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
b/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
index 237d42f2653..8311a29d26a 100644
--- a/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
@@ -18,7 +18,7 @@ from __future__ import annotations
PR_COMMANDS: dict[str, str | list[str]] = {
"name": "PR commands",
- "commands": ["auto-triage"],
+ "commands": ["auto-triage", "stats"],
}
PR_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
@@ -76,4 +76,15 @@ PR_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] =
{
],
},
],
+ "breeze pr stats": [
+ {
+ "name": "Options",
+ "options": [
+ "--batch-size",
+ "--clear-cache",
+ "--github-token",
+ "--github-repository",
+ ],
+ },
+ ],
}
diff --git a/dev/breeze/src/airflow_breeze/utils/pr_cache.py
b/dev/breeze/src/airflow_breeze/utils/pr_cache.py
index abe8b652f67..acd842e4e81 100644
--- a/dev/breeze/src/airflow_breeze/utils/pr_cache.py
+++ b/dev/breeze/src/airflow_breeze/utils/pr_cache.py
@@ -76,6 +76,7 @@ review_cache = CacheStore("review_cache")
classification_cache = CacheStore("classification_cache")
triage_cache = CacheStore("triage_cache")
status_cache = CacheStore("status_cache", ttl_seconds=4 * 3600)
+stats_interaction_cache = CacheStore("stats_interaction_cache")
# Convenience functions for common cache operations
diff --git a/uv.lock b/uv.lock
index f9a4ab162eb..ffccaa368a2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -12,7 +12,7 @@ resolution-markers = [
]
[options]
-exclude-newer = "2026-03-27T19:04:41.304277104Z"
+exclude-newer = "2026-03-30T10:47:15.148565Z"
exclude-newer-span = "P4D"
[manifest]
@@ -1774,7 +1774,7 @@ requires-dist = [
{ name = "rich-argparse", specifier = ">=1.0.0" },
{ name = "setproctitle", specifier = ">=1.3.3" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.48" },
- { name = "starlette", specifier = ">=0.45.0,<1" },
+ { name = "starlette", specifier = ">=0.45.0" },
{ name = "statsd", marker = "extra == 'statsd'", specifier = ">=3.3.0" },
{ name = "structlog", specifier = ">=25.4.0" },
{ name = "svcs", specifier = ">=25.1.0" },
@@ -2645,7 +2645,7 @@ docs = [
[package.metadata]
requires-dist = [
- { name = "aiobotocore", extras = ["boto3"], marker = "extra ==
'aiobotocore'", specifier = ">=2.26.0" },
+ { name = "aiobotocore", marker = "extra == 'aiobotocore'", specifier =
">=2.26.0" },
{ name = "apache-airflow", editable = "." },
{ name = "apache-airflow-providers-apache-hive", marker = "extra ==
'apache-hive'", editable = "providers/apache/hive" },
{ name = "apache-airflow-providers-cncf-kubernetes", marker = "extra ==
'cncf-kubernetes'", editable = "providers/cncf/kubernetes" },
@@ -4581,6 +4581,9 @@ dependencies = [
kerberos = [
{ name = "kerberos" },
]
+oauth = [
+ { name = "authlib" },
+]
[package.dev-dependencies]
dev = [
@@ -4588,6 +4591,7 @@ dev = [
{ name = "apache-airflow-devel-common" },
{ name = "apache-airflow-providers-common-compat" },
{ name = "apache-airflow-task-sdk" },
+ { name = "authlib" },
{ name = "kerberos" },
{ name = "requests-kerberos" },
]
@@ -4599,6 +4603,7 @@ docs = [
requires-dist = [
{ name = "apache-airflow", editable = "." },
{ name = "apache-airflow-providers-common-compat", editable =
"providers/common/compat" },
+ { name = "authlib", marker = "extra == 'oauth'", specifier = ">=1.0.0" },
{ name = "blinker", specifier = ">=1.6.2" },
{ name = "cachetools", specifier = ">=6.0" },
{ name = "flask", specifier = ">=2.2.1" },
@@ -4618,7 +4623,7 @@ requires-dist = [
{ name = "werkzeug", marker = "python_full_version >= '3.14'", specifier =
">=3.1.6" },
{ name = "wtforms", specifier = ">=3.0" },
]
-provides-extras = ["kerberos"]
+provides-extras = ["kerberos", "oauth"]
[package.metadata.requires-dev]
dev = [
@@ -4626,6 +4631,7 @@ dev = [
{ name = "apache-airflow-devel-common", editable = "devel-common" },
{ name = "apache-airflow-providers-common-compat", editable =
"providers/common/compat" },
{ name = "apache-airflow-task-sdk", editable = "task-sdk" },
+ { name = "authlib", specifier = ">=1.0.0" },
{ name = "kerberos", specifier = ">=1.3.0" },
{ name = "requests-kerberos", specifier = ">=0.14.0" },
]