This is an automated email from the ASF dual-hosted git repository. potiuk pushed a commit to branch backport-a5af91d-chart/v1-2x-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit f10c2e730ceab00b65339c4b13a68c84c33f0253 Author: Jarek Potiuk <[email protected]> AuthorDate: Sat Oct 24 14:55:00 2026 +0200 [chart/v1-2x-test] Enable auto-merge, gated on a dedicated CI passed check (#75142) Adapted backport: this branch still runs CI from the combined ci-amd-arm.yml, so the CI passed gate job and the check-ci-gate-needs hook target that workflow. The .asf.yaml rulesets, the newsfragment workflow trigger and the committer docs only take effect on main and are left out. (cherry picked from commit a5af91d2c52fb410a58eb6939ab83e44612afdfe) Generated-by: Claude Opus 5 --- .github/workflows/ci-amd-arm.yml | 53 +++++++++++++++++ .pre-commit-config.yaml | 7 +++ scripts/ci/prek/check_ci_gate_needs.py | 70 +++++++++++++++++++++++ scripts/tests/ci/prek/test_check_ci_gate_needs.py | 60 +++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml index 5dd58c09617..5339c71b873 100644 --- a/.github/workflows/ci-amd-arm.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -934,6 +934,59 @@ jobs: use-uv: ${{ needs.build-info.outputs.use-uv }} debug-resources: ${{ needs.build-info.outputs.debug-resources }} + ci-passed: + # A skipped build-info means project CI did not run at all: the fork CI gate (AIP-120) left the + # pull request to CI in the contributor's fork, and every job here is skipped. Reporting + # "CI passed" then would let it merge on no CI, so the job reports under another name and + # the fork CI reconciler posts "CI passed" from the fork run instead. Until the gate lands, + # build-info has no condition and this always reports "CI passed". + name: >- + ${{ needs.build-info.result == 'skipped' && 'CI passed (project CI not run)' || 'CI passed' }} + # The one check the release branches' ruleset requires, so auto-merge waits for CI to finish. + # GitHub treats a skipped required check as passed, so this job always runs and fails itself + # when any job it needs failed or was cancelled. Jobs skipped by selective checks count as passed. + if: always() + needs: + - additional-ci-image-checks + - additional-prod-image-tests + - basic-tests + - build-ci-images + - build-info + - build-prod-images + - ci-image-checks + - finalize-tests + - generate-constraints + - print-platform-amd + - print-platform-arm + - providers + - tests-airflow-ctl + - tests-go-sdk + - tests-helm + - tests-integration-system + - tests-kubernetes + - tests-mysql-core + - tests-mysql-providers + - tests-non-db-core + - tests-non-db-providers + - tests-postgres-core + - tests-postgres-providers + - tests-special + - tests-sqlite-core + - tests-sqlite-providers + - tests-task-sdk + - tests-with-lowest-direct-resolution-core + - tests-with-lowest-direct-resolution-providers + - verify-release-calendar + runs-on: ["ubuntu-slim"] + steps: + - name: "Fail if any CI job failed or was cancelled" + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + echo "At least one CI job failed or was cancelled." + exit 1 + - name: "All CI jobs passed or were skipped" + run: echo "All CI jobs passed or were skipped." + notify-slack: name: "Notify Slack" needs: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80d0464bb73..b64e03679b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -399,6 +399,13 @@ repos: language: python pass_filenames: false files: ^shared/.*$ + - id: check-ci-gate-needs + name: Check the CI passed gate job needs every CI job + description: Auto-merge waits for "CI passed"; a job missing from its needs could fail without stopping a merge + entry: ./scripts/ci/prek/check_ci_gate_needs.py + language: python + pass_filenames: false + files: ^\.github/workflows/ci-amd-arm\.yml$|^scripts/ci/prek/check_ci_gate_needs\.py$ - id: check-shared-distributions-usage name: Check shared distributions usage entry: ./scripts/ci/prek/check_shared_distributions_usage.py diff --git a/scripts/ci/prek/check_ci_gate_needs.py b/scripts/ci/prek/check_ci_gate_needs.py new file mode 100755 index 00000000000..f696a59a125 --- /dev/null +++ b/scripts/ci/prek/check_ci_gate_needs.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# /// script +# requires-python = ">=3.11,<3.12" +# dependencies = [ +# "pyyaml>=6.0.3", +# ] +# /// +"""Check that the ``CI passed`` gate job in ``ci-amd-arm.yml`` needs every other job. + +``CI passed`` is the check the release branches' ruleset requires, and auto-merge waits for it. It +only fails when a job it needs failed, so a job missing from its ``needs`` could fail and the PR +would still auto-merge. Reporting-only jobs that cannot make a PR wrong are exempt. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import yaml + +AIRFLOW_ROOT = Path(__file__).parents[3] +CI_WORKFLOW = AIRFLOW_ROOT / ".github/workflows/ci-amd-arm.yml" +GATE_JOB = "ci-passed" +EXEMPT_JOBS = {"notify-slack", "summarize-warnings"} + + +def find_problems(jobs: dict) -> list[str]: + if GATE_JOB not in jobs: + return [f"job {GATE_JOB!r} is missing"] + needs = jobs[GATE_JOB].get("needs") or [] + needs = {needs} if isinstance(needs, str) else set(needs) + expected = set(jobs) - EXEMPT_JOBS - {GATE_JOB} + problems = [ + f"job {job!r} is not in {GATE_JOB}.needs, so its failure would not stop auto-merge" + for job in sorted(expected - needs) + ] + problems += [f"{GATE_JOB}.needs lists {job!r}, which is not a job" for job in sorted(needs - set(jobs))] + problems += [ + f"{GATE_JOB}.needs lists exempt reporting job {job!r}" for job in sorted(needs & EXEMPT_JOBS) + ] + return problems + + +def main() -> int: + jobs = yaml.safe_load(CI_WORKFLOW.read_text())["jobs"] + problems = find_problems(jobs) + for problem in problems: + print(f"{CI_WORKFLOW.relative_to(AIRFLOW_ROOT)}: {problem}") + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/ci/prek/test_check_ci_gate_needs.py b/scripts/tests/ci/prek/test_check_ci_gate_needs.py new file mode 100644 index 00000000000..77248def0ab --- /dev/null +++ b/scripts/tests/ci/prek/test_check_ci_gate_needs.py @@ -0,0 +1,60 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest +import yaml +from check_ci_gate_needs import CI_WORKFLOW, find_problems + + +def test_real_workflow_gate_needs_every_job(): + assert find_problems(yaml.safe_load(CI_WORKFLOW.read_text())["jobs"]) == [] + + [email protected]( + ("jobs", "expected"), + [ + pytest.param( + {"build": {}, "test": {}, "notify-slack": {}, "ci-passed": {"needs": ["build", "test"]}}, + [], + id="complete", + ), + pytest.param({"build": {}}, ["job 'ci-passed' is missing"], id="no-gate"), + pytest.param( + {"build": {}, "test": {}, "ci-passed": {"needs": ["build"]}}, + ["job 'test' is not in ci-passed.needs, so its failure would not stop auto-merge"], + id="missing-job", + ), + pytest.param( + {"build": {}, "ci-passed": {"needs": "build"}}, + [], + id="needs-as-string", + ), + pytest.param( + {"build": {}, "ci-passed": {"needs": ["build", "gone"]}}, + ["ci-passed.needs lists 'gone', which is not a job"], + id="unknown-job", + ), + pytest.param( + {"build": {}, "summarize-warnings": {}, "ci-passed": {"needs": ["build", "summarize-warnings"]}}, + ["ci-passed.needs lists exempt reporting job 'summarize-warnings'"], + id="exempt-job-listed", + ), + ], +) +def test_find_problems(jobs, expected): + assert find_problems(jobs) == expected
