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
