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 e00dfce5260 Add comment-only action and passing PRs review to 
auto-triage (#63318)
e00dfce5260 is described below

commit e00dfce5260eeb6ef5003021b69f58e166db6e9e
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Mar 11 09:48:59 2026 +0100

    Add comment-only action and passing PRs review to auto-triage (#63318)
    
    - Add COMMENT action ("a") that posts findings without converting to
      draft. Default when CI passes and only conflicts or unresolved
      comments are found.
    - Use a softer comment message for comment-only action that doesn't
      mention draft conversion.
    - Present PRs that pass all checks (deterministic + LLM) with author
      info, allowing the user to mark them as ready for review or skip.
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../src/airflow_breeze/commands/pr_commands.py     | 106 ++++++++++++++++++++-
 dev/breeze/src/airflow_breeze/utils/confirm.py     |   4 +-
 2 files changed, 105 insertions(+), 5 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands.py 
b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
index 0755ec8494b..d7b6cf84b9a 100644
--- a/dev/breeze/src/airflow_breeze/commands/pr_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
@@ -937,9 +937,18 @@ def _load_what_to_do_next() -> str:
 
 
 def _build_comment(
-    pr_author: str, violations: list, pr_number: int, commits_behind: int, 
base_ref: str
+    pr_author: str,
+    violations: list,
+    pr_number: int,
+    commits_behind: int,
+    base_ref: str,
+    comment_only: bool = False,
 ) -> str:
-    """Build the comment to post on a PR being converted to draft."""
+    """Build the comment to post on a flagged PR.
+
+    When comment_only is True, the comment just lists findings without
+    mentioning draft conversion.
+    """
     violation_lines = []
     for v in violations:
         icon = "x" if v.severity == "error" else "warning"
@@ -960,6 +969,17 @@ def _build_comment(
             "Please rebase your branch and push again to get up-to-date CI 
results."
         )
 
+    if comment_only:
+        return (
+            f"@{pr_author} This PR has a few issues that need to be addressed 
before it can be "
+            f"reviewed — please see our {QUALITY_CRITERIA_LINK}.\n\n"
+            f"**Issues found:**\n{violations_text}{rebase_note}\n\n"
+            f"**What to do next:**\n{what_to_do}\n\n"
+            "Please address the issues above and push again. "
+            "If you have questions, feel free to ask on the "
+            "[Airflow Slack](https://s.apache.org/airflow-slack)."
+        )
+
     return (
         f"@{pr_author} This PR has been converted to **draft** because it does 
not yet meet "
         f"our {QUALITY_CRITERIA_LINK}.\n\n"
@@ -1006,13 +1026,21 @@ def _compute_default_action(
     """Compute the suggested default triage action and reason for a flagged 
PR."""
     reason_parts: list[str] = []
 
-    if pr.mergeable == "CONFLICTING":
+    has_conflicts = pr.mergeable == "CONFLICTING"
+    if has_conflicts:
         reason_parts.append("has merge conflicts")
 
     failed_count = len(pr.failed_checks)
-    if failed_count > 0:
+    has_ci_failures = failed_count > 0
+    if has_ci_failures:
         reason_parts.append(f"{failed_count} CI failure{'s' if failed_count != 
1 else ''}")
 
+    has_unresolved_comments = pr.unresolved_review_comments > 0
+    if has_unresolved_comments and not any("unresolved" in p for p in 
reason_parts):
+        reason_parts.append(
+            f"{pr.unresolved_review_comments} unresolved review comment{'s' if 
pr.unresolved_review_comments != 1 else ''}"
+        )
+
     if assessment.summary:
         reason_parts.append(assessment.summary.lower())
 
@@ -1020,6 +1048,9 @@ def _compute_default_action(
     if count > 3:
         reason_parts.append(f"author has {count} flagged {'PRs' if count != 1 
else 'PR'}")
         action = TriageAction.CLOSE
+    elif not has_ci_failures and (has_conflicts or has_unresolved_comments):
+        # CI passes, no LLM issues — only conflicts or unresolved comments; 
just add a comment
+        action = TriageAction.COMMENT
     else:
         action = TriageAction.DRAFT
 
@@ -1027,6 +1058,7 @@ def _compute_default_action(
     reason = reason[0].upper() + reason[1:]
     action_label = {
         TriageAction.DRAFT: "draft",
+        TriageAction.COMMENT: "add comment",
         TriageAction.CLOSE: "close",
     }[action]
     return action, f"{reason} — suggesting {action_label}"
@@ -1625,6 +1657,7 @@ def auto_triage(
     # PRs with NOT_RUN checks are separated for workflow approval instead of 
LLM assessment.
     assessments: dict[int, PRAssessment] = {}
     llm_candidates: list[PRData] = []
+    passing_prs: list[PRData] = []
     pending_approval: list[PRData] = []
     total_deterministic_flags = 0
 
@@ -1686,6 +1719,7 @@ def auto_triage(
                 f"\n[info]--check-mode=ci: skipping LLM assessment for 
{len(llm_candidates)} "
                 f"{'PRs' if len(llm_candidates) != 1 else 'PR'}.[/]\n"
             )
+            passing_prs.extend(llm_candidates)
     elif llm_candidates:
         skipped_detail = f"{total_deterministic_flags} CI/conflicts/comments"
         if pending_approval:
@@ -1714,6 +1748,7 @@ def auto_triage(
                     continue
                 if not assessment.should_flag:
                     get_console().print(f"  [success]PR {_pr_link(pr)} passes 
quality check.[/]")
+                    passing_prs.append(pr)
                     continue
                 assessments[pr.number] = assessment
 
@@ -1733,6 +1768,7 @@ def auto_triage(
 
     # Phase 5: Present flagged PRs interactively, grouped by author
     total_converted = 0
+    total_commented = 0
     total_closed = 0
     total_ready = 0
     total_skipped_action = 0
@@ -1762,6 +1798,14 @@ def auto_triage(
         comment = _build_comment(
             pr.author_login, assessment.violations, pr.number, 
pr.commits_behind, pr.base_ref
         )
+        comment_only = _build_comment(
+            pr.author_login,
+            assessment.violations,
+            pr.number,
+            pr.commits_behind,
+            pr.base_ref,
+            comment_only=True,
+        )
         close_comment = _build_close_comment(
             pr.author_login,
             assessment.violations,
@@ -1778,6 +1822,7 @@ def auto_triage(
         if dry_run:
             action_label = {
                 TriageAction.DRAFT: "draft",
+                TriageAction.COMMENT: "add comment",
                 TriageAction.CLOSE: "close",
                 TriageAction.READY: "ready",
                 TriageAction.SKIP: "skip",
@@ -1814,6 +1859,15 @@ def auto_triage(
                 get_console().print(f"  [warning]Failed to add label to PR 
{_pr_link(pr)}.[/]")
             continue
 
+        if action == TriageAction.COMMENT:
+            get_console().print(f"  Posting comment on PR {_pr_link(pr)}...")
+            if _post_comment(token, pr.node_id, comment_only):
+                get_console().print(f"  [success]Comment posted on PR 
{_pr_link(pr)}.[/]")
+                total_commented += 1
+            else:
+                get_console().print(f"  [error]Failed to post comment on PR 
{_pr_link(pr)}.[/]")
+            continue
+
         if action == TriageAction.DRAFT:
             get_console().print(f"  Converting PR {_pr_link(pr)} to draft...")
             if _convert_pr_to_draft(token, pr.node_id):
@@ -1852,6 +1906,48 @@ def auto_triage(
             else:
                 get_console().print(f"  [error]Failed to post comment on PR 
{_pr_link(pr)}.[/]")
 
+    # Phase 5b: Present passing PRs for optional ready-for-review marking
+    if not quit_early and passing_prs:
+        passing_prs.sort(key=lambda p: (p.author_login.lower(), p.number))
+        get_console().print(
+            f"\n[info]{len(passing_prs)} {'PRs pass' if len(passing_prs) != 1 
else 'PR passes'} "
+            f"all checks — review to mark as ready:[/]\n"
+        )
+        for pr in passing_prs:
+            author_profile = _fetch_author_profile(token, pr.author_login, 
github_repository)
+            _display_pr_info_panels(pr, author_profile)
+
+            if dry_run:
+                get_console().print("[warning]Dry run — skipping.[/]")
+                continue
+
+            action = prompt_triage_action(
+                f"Action for PR {_pr_link(pr)}?",
+                default=TriageAction.SKIP,
+                forced_answer=answer_triage,
+            )
+
+            if action == TriageAction.QUIT:
+                get_console().print("[warning]Quitting.[/]")
+                quit_early = True
+                break
+
+            if action == TriageAction.READY:
+                get_console().print(
+                    f"  [info]Marking PR {_pr_link(pr)} as ready "
+                    f"— adding '{_READY_FOR_REVIEW_LABEL}' label.[/]"
+                )
+                if _add_label(token, github_repository, pr.node_id, 
_READY_FOR_REVIEW_LABEL):
+                    get_console().print(
+                        f"  [success]Label '{_READY_FOR_REVIEW_LABEL}' added 
to PR {_pr_link(pr)}.[/]"
+                    )
+                    total_ready += 1
+                else:
+                    get_console().print(f"  [warning]Failed to add label to PR 
{_pr_link(pr)}.[/]")
+            else:
+                get_console().print(f"  [info]Skipping PR {_pr_link(pr)} — no 
action taken.[/]")
+                total_skipped_action += 1
+
     # Phase 6: Present NOT_RUN PRs for workflow approval
     total_workflows_approved = 0
     if not quit_early and pending_approval:
@@ -2037,7 +2133,9 @@ def auto_triage(
     summary_table.add_row("Flagged by LLM", str(total_flagged - 
total_deterministic_flags))
     summary_table.add_row("LLM errors (skipped)", str(total_llm_errors))
     summary_table.add_row("Total flagged", str(total_flagged))
+    summary_table.add_row("PRs passing all checks", str(len(passing_prs)))
     summary_table.add_row("PRs converted to draft", str(total_converted))
+    summary_table.add_row("PRs commented (not drafted)", str(total_commented))
     summary_table.add_row("PRs closed", str(total_closed))
     summary_table.add_row("PRs marked ready for review", str(total_ready))
     summary_table.add_row("PRs skipped (no action)", str(total_skipped_action))
diff --git a/dev/breeze/src/airflow_breeze/utils/confirm.py 
b/dev/breeze/src/airflow_breeze/utils/confirm.py
index 7aad11a3403..d700bff93fb 100644
--- a/dev/breeze/src/airflow_breeze/utils/confirm.py
+++ b/dev/breeze/src/airflow_breeze/utils/confirm.py
@@ -116,6 +116,7 @@ def confirm_action(
 
 class TriageAction(Enum):
     DRAFT = "d"
+    COMMENT = "a"
     CLOSE = "c"
     READY = "r"
     SKIP = "s"
@@ -139,6 +140,7 @@ def prompt_triage_action(
 
     _LABELS = {
         TriageAction.DRAFT: "draft",
+        TriageAction.COMMENT: "add comment",
         TriageAction.CLOSE: "close",
         TriageAction.READY: "ready",
         TriageAction.SKIP: "skip",
@@ -188,7 +190,7 @@ def prompt_triage_action(
             for action in TriageAction:
                 if upper == action.value.upper():
                     return action
-            print(f"Invalid input '{user_input}'. Please enter one of: 
d/c/r/s/q")
+            print(f"Invalid input '{user_input}'. Please enter one of: 
d/a/c/r/s/q")
         except TimeoutOccurred:
             return default
         except KeyboardInterrupt:

Reply via email to