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 4c110e645d0 Stop labelling fork CI PRs that change CI files for 
workflow approval (#76289)
4c110e645d0 is described below

commit 4c110e645d02d12afbe5177a97008485ae157f9c
Author: Andrew Chang <[email protected]>
AuthorDate: Mon Nov 2 23:43:55 2026 +0800

    Stop labelling fork CI PRs that change CI files for workflow approval 
(#76289)
    
    A pull request whose CI runs in the contributor's fork gets its
    project CI runs held when it changes CI definition files. It got the
    "need workflow approval" label for that, and "needs project ci" once
    its fork run was green. Without "use project ci", approving the held
    runs does not start project CI, so the first label asked maintainers
    for a step that does not help. It also stayed on pull requests whose
    fork run failed.
---
 .github/workflows/auto-approve-workflow-runs.yml   |  8 ++---
 contributing-docs/25_maintainer_pr_triage.md       |  2 +-
 dev/breeze/doc/08_ci_tasks.rst                     |  4 +--
 dev/breeze/doc/ci/05_workflows.md                  |  4 +--
 .../doc/ci/08_fork_ci_for_external_contributors.md |  9 ++---
 .../airflow_breeze/utils/workflow_run_approval.py  | 15 +++++++--
 dev/breeze/tests/test_workflow_run_approval.py     | 38 ++++++++++++++++++++++
 7 files changed, 64 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/auto-approve-workflow-runs.yml 
b/.github/workflows/auto-approve-workflow-runs.yml
index ef635daf243..876ab4756eb 100644
--- a/.github/workflows/auto-approve-workflow-runs.yml
+++ b/.github/workflows/auto-approve-workflow-runs.yml
@@ -19,10 +19,10 @@
 # Approves the waiting workflow runs of external contributors' PRs to main 
when the change is safe to
 # run without a maintainer's look, and labels the others "need workflow 
approval" with a pending
 # "Workflow approval" commit status saying why. A PR whose CI runs in the 
contributor's fork (AIP-120)
-# is approved unless it changes CI definition files, and then also gets a 
comment saying why its runs
-# wait. When the runs start, mark-workflow-runs-approved.yml removes the label 
and turns the status
-# green. PRs from the repositories of trusted stakeholders (Amazon, Google, 
Astronomer) are always
-# approved. The rules are in
+# is approved unless it changes CI definition files, and then gets a comment 
saying why its runs wait
+# instead of the label. When the runs start, mark-workflow-runs-approved.yml 
removes the label and
+# turns the status green. PRs from the repositories of trusted stakeholders 
(Amazon, Google,
+# Astronomer) are always approved. The rules are in
 # dev/breeze/src/airflow_breeze/utils/workflow_run_approval.py and documented 
in
 # contributing-docs/05_pull_requests.rst.
 #
diff --git a/contributing-docs/25_maintainer_pr_triage.md 
b/contributing-docs/25_maintainer_pr_triage.md
index 3d873e17942..ddbc647895d 100644
--- a/contributing-docs/25_maintainer_pr_triage.md
+++ b/contributing-docs/25_maintainer_pr_triage.md
@@ -238,7 +238,7 @@ The triage workflow uses the following labels and states:
 | Label / State | Meaning |
 |---|---|
 | `ready for maintainer review` | PR has passed Stage 1 and is queued for 
human review. Applied only after the skill has verified that no workflow runs 
are awaiting approval. |
-| `need workflow approval` | The PR's CI workflow runs wait for a maintainer, 
because the `Auto approve workflow runs` workflow did not approve them: a PR to 
a branch other than `main`, a change to CI definition files, a full-tests or 
dependency change, a change whose CI jobs are estimated at more than 310 
job-minutes, or a change of more than 100 lines of source code (see [Pull 
Requests](05_pull_requests.rst)). For a PR whose CI runs in the contributor's 
fork, only a change to CI defini [...]
+| `need workflow approval` | The PR's CI workflow runs wait for a maintainer, 
because the `Auto approve workflow runs` workflow did not approve them: a PR to 
a branch other than `main`, a change to CI definition files, a full-tests or 
dependency change, a change whose CI jobs are estimated at more than 310 
job-minutes, or a change of more than 100 lines of source code (see [Pull 
Requests](05_pull_requests.rst)). The workflow also tries to add the label when 
processing the PR failed; its  [...]
 | `closed because of multiple quality violations` | PR was closed because the 
author has multiple open PRs with quality issues. |
 | `suspicious changes detected` | PR (and all open PRs by the same author) was 
closed because the diff contained suspicious patterns (secret exfiltration, 
malicious CI modifications, etc.). |
 | `closed because of open PR limit` | PR was closed in the one-time closure 
that introduced the [open PR limit](32_open_pull_request_limit.rst). Applied by 
`dev/close_prs_over_open_pr_limit.py`, never by the triage skill. If the author 
reopens the PR, triage it like any other. |
diff --git a/dev/breeze/doc/08_ci_tasks.rst b/dev/breeze/doc/08_ci_tasks.rst
index 36eb3743eb4..7b7760f3bc4 100644
--- a/dev/breeze/doc/08_ci_tasks.rst
+++ b/dev/breeze/doc/08_ci_tasks.rst
@@ -209,8 +209,8 @@ than 310 job-minutes, that change at most 100 lines of 
source code and that do n
 approves the runs and they start, the ``mark-workflow-runs-approved`` workflow 
removes the label and
 turns the status green. PRs whose CI runs in
 the contributor's fork (AIP-120) only run the fork CI gate in 
``apache/airflow``, so their runs are
-approved whatever their size unless they change CI definition files; such a PR 
also gets a comment
-explaining that its CI change needs a maintainer. The runs of PRs from the 
repositories of trusted
+approved whatever their size unless they change CI definition files; such a PR 
gets a comment
+instead of the label, explaining that its CI change needs a maintainer. The 
runs of PRs from the repositories of trusted
 stakeholders (Amazon, Google, Astronomer) are always approved. For every PR it 
also
 estimates the CI jobs selective checks will run and how long they take, and 
writes them to the job
 summary. Use ``--report-only`` to see the decisions without approving runs or 
changing labels or
diff --git a/dev/breeze/doc/ci/05_workflows.md 
b/dev/breeze/doc/ci/05_workflows.md
index a371d363004..bfb56b73dd6 100644
--- a/dev/breeze/doc/ci/05_workflows.md
+++ b/dev/breeze/doc/ci/05_workflows.md
@@ -124,8 +124,8 @@ them, for the contributors the repository's approval 
setting covers. The
 A PR to `main` whose CI runs in the contributor's fork (AIP-120: the `Fork CI 
gate` job would skip
 project CI for it) is decided by one rule instead, because its `Tests (AMD)` 
run only executes the
 gate: its runs are approved whatever its size, cost or dependency changes, 
unless it changes one of
-those CI-defining files. Such a PR gets the `need workflow approval` label and 
one comment telling
-the author that the CI change needs a maintainer; see
+those CI-defining files. Such a PR gets one comment instead of the `need 
workflow approval` label,
+telling the author that the CI change needs a maintainer; see
 [Approving gated 
runs](08_fork_ci_for_external_contributors.md#approving-gated-runs).
 
 The runs of PRs whose head repository belongs to a trusted stakeholder 
(`astronomer/airflow` for
diff --git a/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md 
b/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
index e4efe527cd3..06b4a88741f 100644
--- a/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
+++ b/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
@@ -385,13 +385,14 @@ maintainer. One rule applies instead:
   "Trusting a green fork run"). The approved `ci-amd.yml` run executes only 
the gate, and the other
   `pull_request` workflows (CodeQL for the changed languages, the newsfragment 
check, dependency
   review) run on GitHub-hosted runners.
-- Otherwise the runs wait for a maintainer, the pull request is labelled `need 
workflow approval`,
-  its head commit gets a pending `Workflow approval` status naming the CI 
files (as for every
-  pull request whose runs wait), and the workflow posts one comment explaining 
that the change to the listed CI files needs a
+- Otherwise the runs wait for a maintainer, its head commit gets a pending 
`Workflow approval`
+  status naming the CI files (as for every pull request whose runs wait), and 
the workflow posts one comment explaining that the change to the listed CI 
files needs a
   maintainer to review it and approve the runs. The comment starts with the 
marker
   `<!-- workflow-approval: ci-change -->` and is posted once per pull request; 
only comments by
   `github-actions[bot]` with the marker count. A change to `.github/` can 
remove the gate or add a
-  workflow that does run, so such a run must not start without a review.
+  workflow that does run, so such a run must not start without a review. The 
pull request does
+  not get the `need workflow approval` label, and loses it if it has it. 
Approving its runs does
+  not start project CI. A maintainer adds `use project ci` for that.
 
 A run started before `use fork ci` or `use project ci` was last added or 
removed is approved too,
 so a committer can add `use fork ci` after the PR's first runs were created 
and before they are
diff --git a/dev/breeze/src/airflow_breeze/utils/workflow_run_approval.py 
b/dev/breeze/src/airflow_breeze/utils/workflow_run_approval.py
index d32d0455f1b..2dba4b3842a 100644
--- a/dev/breeze/src/airflow_breeze/utils/workflow_run_approval.py
+++ b/dev/breeze/src/airflow_breeze/utils/workflow_run_approval.py
@@ -168,6 +168,8 @@ class ApprovalDecision:
     estimate: CiMinutesEstimate | None = None
     # Posted on the PR once, when the runs are left for a maintainer.
     comment: str | None = None
+    # Whether a PR whose runs are left for a maintainer gets the ``need 
workflow approval`` label.
+    add_approval_label: bool = True
     # The labels the decision depends on: runs started before one of them 
changed are not approved.
     decision_labels: frozenset[str] = TEST_SELECTION_LABELS
     # Runs started before this are not approved either.
@@ -253,6 +255,8 @@ def decide_gated_approval(*, changed_files: 
list[ChangedFile]) -> ApprovalDecisi
             approve=False,
             reasons=[f"its CI runs in the fork, but it changes CI definition 
files: {', '.join(ci_files)}"],
             comment=ci_change_approval_comment(ci_files=ci_files),
+            # Approving does not start project CI; a maintainer adds `use 
project ci` for that.
+            add_approval_label=False,
         )
     return ApprovalDecision(
         approve=True,
@@ -871,15 +875,18 @@ def apply_decision(
         return
     # Reasons can contain contributor-chosen file names, which must not be 
read as Rich markup.
     console_print(f"[warning]PR #{pr_number} needs a maintainer: {escape('; 
'.join(decision.reasons))}[/]")
-    if not has_label:
+    label_added = decision.add_approval_label and not has_label
+    if label_added:
         api.add_label(pr_number, NEED_WORKFLOW_APPROVAL_LABEL)
+    elif has_label and not decision.add_approval_label:
+        api.remove_label(pr_number, NEED_WORKFLOW_APPROVAL_LABEL)
     status_changed = update_approval_status(
         api,
         sha=pr["head"]["sha"],
         state="pending",
         description=build_status_description("Waits for a maintainer", 
decision.reasons),
     )
-    if (not has_label or status_changed) and clear_if_runs_were_approved(api, 
pr=pr, run_ids=run_ids):
+    if (label_added or status_changed) and clear_if_runs_were_approved(api, 
pr=pr, run_ids=run_ids):
         return
     # Once per PR: every later push would otherwise repeat it while the change 
stays.
     if decision.comment is not None and not api.has_bot_comment(pr_number, 
CI_CHANGE_MARKER):
@@ -917,8 +924,10 @@ def write_step_summary(results: list[PullRequestResult], 
github_repository: str)
             decision = "failed"
         elif result.decision.approve:
             decision = "approved"
-        else:
+        elif result.decision.add_approval_label:
             decision = f"`{NEED_WORKFLOW_APPROVAL_LABEL}`"
+        else:
+            decision = "waits for a maintainer"
         lines.append(
             f"| 
[#{result.pr_number}](https://github.com/{github_repository}/pull/{result.pr_number})
 "
             f"| `{result.head_sha[:10]}` | {decision} | 
{format_estimate(result.decision.estimate)} "
diff --git a/dev/breeze/tests/test_workflow_run_approval.py 
b/dev/breeze/tests/test_workflow_run_approval.py
index 6aa5d85d5ee..86c9511cc74 100644
--- a/dev/breeze/tests/test_workflow_run_approval.py
+++ b/dev/breeze/tests/test_workflow_run_approval.py
@@ -494,6 +494,27 @@ def 
test_write_step_summary_shows_failed_pull_requests_as_failed(tmp_path, monke
     assert NEED_WORKFLOW_APPROVAL_LABEL not in text
 
 
+def 
test_write_step_summary_does_not_show_the_label_on_a_pull_request_left_without_it(tmp_path,
 monkeypatch):
+    summary = tmp_path / "summary.md"
+    monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary))
+
+    write_step_summary(
+        [
+            PullRequestResult(
+                pr_number=10,
+                head_sha="abcdef0123456789",
+                run_ids=[1],
+                decision=ApprovalDecision(approve=False, reasons=["it changes 
CI"], add_approval_label=False),
+            )
+        ],
+        "apache/airflow",
+    )
+
+    text = summary.read_text()
+    assert "| `abcdef0123` | waits for a maintainer |" in text
+    assert NEED_WORKFLOW_APPROVAL_LABEL not in text
+
+
 @pytest.mark.parametrize(
     ("paths", "approve"),
     [
@@ -900,6 +921,7 @@ def test_decide_for_pull_request_gated_to_fork_ci(
     else:
         assert decision.comment is not None
         assert all(f"`{path}`" in decision.comment for path in ci_files)
+        assert decision.add_approval_label is False
 
 
 @pytest.mark.parametrize(
@@ -1295,6 +1317,22 @@ def 
test_apply_decision_does_not_recheck_the_runs_when_nothing_was_written():
     api.set_approval_status.assert_not_called()
 
 
[email protected](
+    ("labels", "expected_remove_label_calls"),
+    [((), []), ((NEED_WORKFLOW_APPROVAL_LABEL,), [mock.call(10, 
NEED_WORKFLOW_APPROVAL_LABEL)])],
+)
+def test_apply_decision_without_the_approval_label_removes_it(labels, 
expected_remove_label_calls):
+    api = mock.MagicMock(spec=GitHubApi)
+    api.get_approval_status.return_value = _status("pending", "Waits for a 
maintainer: it changes CI")
+    decision = ApprovalDecision(approve=False, reasons=["it changes CI"], 
add_approval_label=False)
+
+    apply_decision(api=api, pr=_pr(10, "aaa", labels=labels), run_ids=[1], 
decision=decision)
+
+    api.add_label.assert_not_called()
+    assert api.remove_label.call_args_list == expected_remove_label_calls
+    api.is_run_waiting_for_approval.assert_not_called()
+
+
 @mock.patch(f"{MODULE}.decide_for_pull_request", autospec=True)
 def 
test_process_waiting_runs_clears_a_failed_pull_request_whose_runs_were_approved_meanwhile(mock_decide):
     api = _api_with_prs([_pr(10, "aaa")], [_run(1, "aaa")])

Reply via email to