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 cf4525db121 Report CI run started for release branches that do not run 
ci-amd.yml (#76226)
cf4525db121 is described below

commit cf4525db1215fb8f32f0c0ca917fdd24c26eae07
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun Nov 1 16:01:05 2026 +0100

    Report CI run started for release branches that do not run ci-amd.yml 
(#76226)
    
    Both CI rulesets require `CI run started` on every release branch, but
    the fork CI monitor reported it only once a `ci-amd.yml` run existed.
    Pull requests to the airflow-ctl release branches run their CI in
    `airflow-ctl-tests.yml`, release branches cut before `ci-amd.yml` run
    `ci-amd-arm.yml`, and the older v2 and providers release branches run
    `ci.yml`, so their check stayed expected forever and the pull requests
    could not merge without a bypass, even with `CI passed` green.
    
    Generated-by: Claude Opus 5
---
 .../doc/ci/08_fork_ci_for_external_contributors.md | 10 ++-
 .../src/airflow_breeze/utils/fork_ci_reconcile.py  | 22 +++++--
 dev/breeze/tests/test_fork_ci_reconcile.py         | 73 +++++++++++++++-------
 3 files changed, 75 insertions(+), 30 deletions(-)

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 181180b5516..aad1a304e74 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
@@ -937,13 +937,17 @@ with state `success` linking the run:
 - for a gated pull request, once the fork has a run of `ci-amd.yml` for the 
head commit, whatever
   its state; the fork run is linked, so the contributor can follow it before 
the state comment
   appears.
-- for every other open pull request, once project CI has a `ci-amd.yml` 
`pull_request` run for the
-  head commit that is not waiting for a maintainer's approval. One GraphQL 
search lists the open
+- for every other open pull request, once project CI has a `pull_request` run 
for the head commit
+  that is not waiting for a maintainer's approval. Which workflow runs project 
CI depends on the
+  base branch: `ci-amd.yml` on `main` and the release branches it was 
backported to,
+  `ci-amd-arm.yml` on release branches cut before it, `ci.yml` on the older v2 
and providers
+  release branches, and `airflow-ctl-tests.yml` on the airflow-ctl release 
branches; a run of any
+  of them counts. One GraphQL search lists the open
   pull requests without `fork ci required`, most recently updated first, with 
the head commit's
   `CI run started` status and its GitHub Actions check suites. Pull requests 
discovery labelled in
   the same run are skipped, since the search index can still list them without 
the label. When the
   last commit listed is not the head, as after a rebase that keeps old commit 
dates, two REST calls
-  read the head commit's statuses and latest run instead.
+  read the head commit's statuses and its `pull_request` runs instead.
 
 Folding the check into the scheduled monitor costs no runner per push, at the 
price of a pull
 request waiting for the next monitor run before it can merge, which a CI run 
takes longer than
diff --git a/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py 
b/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
index d940d8a21ce..80dd7242a3e 100644
--- a/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
+++ b/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
@@ -85,6 +85,12 @@ from airflow_breeze.utils.fork_ci_messages import (
 )
 
 CI_WORKFLOW_FILE = "ci-amd.yml"
+# The workflows that run a pull request's CI in apache/airflow, depending on 
its base branch:
+# ci-amd.yml on main and on release branches it was backported to, 
ci-amd-arm.yml on release
+# branches cut before ci-amd.yml existed, ci.yml on the older v2 and providers 
release branches,
+# airflow-ctl-tests.yml on the airflow-ctl release branches. A run of any of 
them reports
+# `CI run started`, which both CI rulesets require on every base.
+PROJECT_CI_WORKFLOW_FILES = frozenset({CI_WORKFLOW_FILE, "ci-amd-arm.yml", 
"ci.yml", "airflow-ctl-tests.yml"})
 # The files the gate job of ci-amd.yml reads from a pull request's base branch.
 GATE_CONFIG_PATH = ".github/fork-ci-gate.toml"
 GATE_SCRIPT_PATH = "dev/breeze/src/airflow_breeze/utils/fork_ci_gate.py"
@@ -981,7 +987,7 @@ query($q: String!, $cursor: String) {
 
 def find_project_ci_run_url(node: dict[str, Any]) -> str | None:
     """
-    The latest ci-amd.yml pull_request run of a pull request's head commit 
that still needs `CI run started`.
+    The latest project CI pull_request run of a pull request's head commit 
that still needs `CI run started`.
 
     None when the commit already has the status, when its latest run has not 
started because it waits
     for a maintainer's approval or there is none yet, and when the last commit 
listed is not the head:
@@ -997,7 +1003,7 @@ def find_project_ci_run_url(node: dict[str, Any]) -> str | 
None:
         suite
         for suite in (commit.get("checkSuites") or {}).get("nodes") or []
         if (run := suite.get("workflowRun") or {}).get("event") == 
"pull_request"
-        and _workflow_file((run.get("workflow") or {}).get("resourcePath")) == 
CI_WORKFLOW_FILE
+        and _workflow_file((run.get("workflow") or {}).get("resourcePath")) in 
PROJECT_CI_WORKFLOW_FILES
     ]
     if not runs:
         return None
@@ -1021,10 +1027,14 @@ def read_project_ci_run_url(client: GitHubClient, repo: 
str, head_sha: str) -> s
         status["context"] == CI_RUN_STARTED_CONTEXT and status["state"] == 
"success" for status in statuses
     ):
         return None
-    runs = client.rest_get(
-        f"repos/{repo}/actions/workflows/{CI_WORKFLOW_FILE}/runs"
-        f"?event=pull_request&head_sha={head_sha}&per_page=1"
-    ).get("workflow_runs", [])
+    # Newest first, and across every workflow: which one runs CI depends on 
the base branch.
+    runs = [
+        run
+        for run in client.rest_get(
+            
f"repos/{repo}/actions/runs?event=pull_request&head_sha={head_sha}&per_page=100"
+        ).get("workflow_runs", [])
+        if _workflow_file(run.get("path")) in PROJECT_CI_WORKFLOW_FILES
+    ]
     if not runs or runs[0].get("conclusion") == "action_required":
         return None
     return runs[0]["html_url"]
diff --git a/dev/breeze/tests/test_fork_ci_reconcile.py 
b/dev/breeze/tests/test_fork_ci_reconcile.py
index 9c72881dc46..417a08540af 100644
--- a/dev/breeze/tests/test_fork_ci_reconcile.py
+++ b/dev/breeze/tests/test_fork_ci_reconcile.py
@@ -1583,6 +1583,14 @@ def _suite(
     }
 
 
+def _run_json(run_id, workflow_file="ci-amd.yml", *, path=None, 
conclusion=None):
+    return {
+        "html_url": f"https://github.com/apache/airflow/actions/runs/{run_id}";,
+        "path": path or f".github/workflows/{workflow_file}",
+        "conclusion": conclusion,
+    }
+
+
 @pytest.mark.parametrize(
     ("node", "expected"),
     [
@@ -1605,6 +1613,27 @@ def _suite(
             None,
             id="other-runs-only",
         ),
+        pytest.param(
+            _project_node(
+                1,
+                suites=[
+                    _suite(7, path=".github/workflows/airflow-ctl-tests.yml"),
+                    _suite(8, path=".github/workflows/codeql.yml", 
created="2026-11-10T11:00:00Z"),
+                ],
+            ),
+            "https://github.com/apache/airflow/actions/runs/7";,
+            id="airflow-ctl-release-branch",
+        ),
+        pytest.param(
+            _project_node(1, suites=[_suite(7, 
path=".github/workflows/ci-amd-arm.yml")]),
+            "https://github.com/apache/airflow/actions/runs/7";,
+            id="release-branch-without-ci-amd",
+        ),
+        pytest.param(
+            _project_node(1, suites=[_suite(7, 
path=".github/workflows/ci.yml")]),
+            "https://github.com/apache/airflow/actions/runs/7";,
+            id="v2-or-providers-release-branch",
+        ),
         pytest.param(
             _project_node(1, suites=[_suite(7, conclusion="ACTION_REQUIRED")]),
             None,
@@ -1620,28 +1649,37 @@ def test_find_project_ci_run_url(node, expected):
 @pytest.mark.parametrize(
     ("statuses", "runs", "expected"),
     [
+        pytest.param([], [_run_json(9)], 
"https://github.com/apache/airflow/actions/runs/9";, id="run-found"),
+        pytest.param(
+            [],
+            [
+                _run_json(9, path=".github/workflows/codeql-analysis.yml"),
+                _run_json(8, "airflow-ctl-tests.yml"),
+            ],
+            "https://github.com/apache/airflow/actions/runs/8";,
+            id="airflow-ctl-release-branch",
+        ),
+        pytest.param(
+            [],
+            [_run_json(9, "ci-amd-arm.yml")],
+            "https://github.com/apache/airflow/actions/runs/9";,
+            id="release-branch-without-ci-amd",
+        ),
         pytest.param(
             [],
-            [{"html_url": "https://github.com/apache/airflow/actions/runs/9";, 
"conclusion": None}],
+            [_run_json(9, "ci.yml")],
             "https://github.com/apache/airflow/actions/runs/9";,
-            id="run-found",
+            id="v2-or-providers-release-branch",
         ),
         pytest.param(
             [{"context": CI_RUN_STARTED_CONTEXT, "state": "success"}],
-            [{"html_url": "https://github.com/apache/airflow/actions/runs/9";, 
"conclusion": None}],
+            [_run_json(9)],
             None,
             id="already-reported",
         ),
+        pytest.param([], [_run_json(9, conclusion="action_required")], None, 
id="waiting-for-approval"),
         pytest.param(
-            [],
-            [
-                {
-                    "html_url": 
"https://github.com/apache/airflow/actions/runs/9";,
-                    "conclusion": "action_required",
-                }
-            ],
-            None,
-            id="waiting-for-approval",
+            [], [_run_json(9, path=".github/workflows/codeql-analysis.yml")], 
None, id="other-runs-only"
         ),
         pytest.param([], [], None, id="no-run-yet"),
     ],
@@ -1651,10 +1689,7 @@ def test_read_project_ci_run_url(client, statuses, runs, 
expected):
         if path == f"repos/{REPO}/commits/sha1/status?per_page=100":
             assert (paginate, items_key) == (True, "statuses")
             return statuses
-        assert (
-            path
-            == 
f"repos/{REPO}/actions/workflows/ci-amd.yml/runs?event=pull_request&head_sha=sha1&per_page=1"
-        )
+        assert path == 
f"repos/{REPO}/actions/runs?event=pull_request&head_sha=sha1&per_page=100"
         return {"workflow_runs": runs}
 
     client.rest_get.side_effect = rest_get
@@ -1666,11 +1701,7 @@ def 
test_report_project_ci_runs_started_reads_a_head_commit_the_search_did_not_l
     client.graphql.return_value = _search_result(_project_node(1, 
suites=[_suite(7)], oid="older"))
     client.rest_get.side_effect = [
         [],
-        {
-            "workflow_runs": [
-                {"html_url": 
"https://github.com/apache/airflow/actions/runs/9";, "conclusion": None}
-            ]
-        },
+        {"workflow_runs": [_run_json(9)]},
     ]
 
     assert report_project_ci_runs_started(client, repo=REPO, skip=set()) == [1]

Reply via email to