This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch fork-ci-6-reconciler-followups
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit e3a1772093b178bcd8358dcbd6dbad12d09ad9d8
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Oct 17 02:58:50 2026 +0200

    Ignore fork-only workflows that skipped every job in the fork CI tampering 
check
    
    A workflow or trigger the base branch does not have always counted as
    project CI, so a pull request adding a fork-only workflow that its jobs
    guard off in apache/airflow was closed for gate tampering although nothing
    ran.
    
    Generated-by: Claude Opus 5
---
 .../doc/ci/08_fork_ci_for_external_contributors.md |  8 +++--
 .../src/airflow_breeze/utils/fork_ci_reconcile.py  | 34 +++++++++++++++-------
 dev/breeze/tests/test_fork_ci_reconcile.py         |  9 +++++-
 3 files changed, 37 insertions(+), 14 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 540000aab23..f5482f19b19 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
@@ -512,7 +512,10 @@ bypass when both signals hold:
 1. **The run did project CI work.** Either the default branch's version of the 
run's workflow
    is not triggered by the run's event, which covers an injected workflow and 
a trigger the
    pull request added to an existing one (for example `pull_request` on 
`ci-arm.yml`, which
-   has no gate), or it is a `ci-amd.yml` run that lasted more than 5 minutes 
and whose jobs
+   has no gate), and one of its jobs was not skipped (judged as for 
`ci-amd.yml` below when it
+   is that workflow, and counted as work when the jobs cannot be listed), so a 
fork-only
+   workflow the pull request adds and guards off in `apache/airflow` does not 
count; or it is
+   a `ci-amd.yml` run that lasted more than 5 minutes and whose jobs
    (`GET .../actions/runs/<id>/jobs`) show more than the gate: a job other 
than `Fork CI gate`
    that was not skipped, a missing or duplicated gate job, or a gate job that 
ran for more
    than 5 minutes. The last case catches project CI work done in a job renamed 
to the gate's
@@ -789,7 +792,8 @@ The remote-parsing block currently inside `breeze ci 
upgrade` is factored into
   CI-defining files keeps the pull request a draft, decided once per commit) 
and the gate
   tampering check: a tampered run on an earlier commit is caught after a clean 
push, each
   signal alone does not close, short runs and gate-only jobs do not count, an 
in-progress
-  non-gate job, a removed, duplicated or long-running gate job and an injected 
workflow do,
+  non-gate job, a removed, duplicated or long-running gate job and an injected 
workflow with a
+  job that ran do, an added workflow whose jobs all skipped does not,
   runs before the last `use project ci` removal or the last reopen are 
ignored, an
   uncheckable 300-file change set is reported, `label_fork_ci` pull requests 
are exempt and
   `pilot` pull requests are checked. Discovery labels a pilot participant's
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 22ec0e15b2b..7e49ed5395b 100644
--- a/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
+++ b/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
@@ -869,8 +869,8 @@ class BypassFinder:
     signals of the design must hold for a run: it did project CI work, and its 
own commit changes
     `.github/workflows/`. A run did project CI work when the base branch's 
version of its
     workflow is not triggered by the run's event (an injected workflow, or a 
trigger the pull
-    request added to an existing one, such as `pull_request` on ci-arm.yml), 
or when it is a long
-    ci-amd.yml run whose jobs show more than the gate.
+    request added to an existing one, such as `pull_request` on ci-arm.yml) 
and a job of it ran,
+    or when it is a long ci-amd.yml run whose jobs show more than the gate.
     """
 
     def __init__(
@@ -903,21 +903,33 @@ class BypassFinder:
         self._did_project_ci: dict[int, bool] = {}
         self._changes_workflows: dict[tuple[str, str], bool | None] = {}
 
+    def _list_jobs(self, run: WorkflowRun) -> list[dict[str, Any]]:
+        return self._client.rest_get(
+            
f"repos/{self._repo}/actions/runs/{run.id}/jobs?filter=latest&per_page=100",
+            paginate=True,
+            items_key="jobs",
+        )
+
     def _did_run_project_ci(self, run: WorkflowRun) -> bool:
         if run.id not in self._did_project_ci:
             if run.event not in self._base_triggers.get(run.workflow_file, 
frozenset()):
-                # Counts even when every job skipped; tracked at
-                # https://github.com/apache/airflow/issues/74905
-                result = True
+                # A workflow or trigger the base branch does not have, for 
example a fork-only
+                # workflow the pull request adds, did no work in 
apache/airflow if its jobs all
+                # skipped there. Jobs that cannot be listed count as work.
+                try:
+                    jobs = self._list_jobs(run)
+                except GitHubError:
+                    result = True
+                else:
+                    result = (
+                        jobs_show_project_ci(jobs, self._now)
+                        if run.workflow_file == CI_WORKFLOW_FILE
+                        else any(job.get("conclusion") != "skipped" for job in 
jobs)
+                    )
             elif run.workflow_file != CI_WORKFLOW_FILE or not 
run.is_long_running(self._now):
                 result = False
             else:
-                jobs = self._client.rest_get(
-                    
f"repos/{self._repo}/actions/runs/{run.id}/jobs?filter=latest&per_page=100",
-                    paginate=True,
-                    items_key="jobs",
-                )
-                result = jobs_show_project_ci(jobs, self._now)
+                result = jobs_show_project_ci(self._list_jobs(run), self._now)
             self._did_project_ci[run.id] = result
         return self._did_project_ci[run.id]
 
diff --git a/dev/breeze/tests/test_fork_ci_reconcile.py 
b/dev/breeze/tests/test_fork_ci_reconcile.py
index e7e929917a9..059da404cac 100644
--- a/dev/breeze/tests/test_fork_ci_reconcile.py
+++ b/dev/breeze/tests/test_fork_ci_reconcile.py
@@ -723,7 +723,7 @@ def test_finder_ignores_legitimate_runs(client, runs, jobs, 
compare_files, pr_ov
     ],
 )
 def test_finder_catches_triggers_the_base_branch_does_not_have(client, run):
-    finder = _finder(client, [run])
+    finder = _finder(client, [run], jobs=[_job("Tests", "success")])
 
     assert finder.find(_pr()) == 
"https://github.com/apache/airflow/actions/runs/9";
 
@@ -1201,3 +1201,10 @@ def 
test_nudge_is_posted_before_the_state_comment_records_awaiting_push(client):
 
     assert "Push this branch again" in client.create_comment.call_args.args[1]
     client.update_comment.assert_not_called()
+
+
+def test_finder_ignores_an_added_workflow_whose_jobs_all_skipped(client):
+    run = _run(9, path=".github/workflows/fork-only.yml", minutes=1)
+    finder = _finder(client, [run], jobs=[_job("Fork only"), _job("Also fork 
only")])
+
+    assert finder.find(_pr()) is None

Reply via email to