This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new f2cf8bf1d39 [v3-4-test] Report CI run started for release branches
that do not run ci-amd.yml (#76226) (#76232)
f2cf8bf1d39 is described below
commit f2cf8bf1d39dbc9b96223633dddd51067014454a
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Nov 1 16:13:11 2026 +0100
[v3-4-test] Report CI run started for release branches that do not run
ci-amd.yml (#76226) (#76232)
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.
(cherry picked from commit cf4525db1215fb8f32f0c0ca917fdd24c26eae07)
Generated-by: Claude Opus 5
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../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]