You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by bl...@apache.org on 2026/09/11 14:08:33 UTC

(datafusion-comet) branch db/ci-merge-queue-gate updated: ci: simplify aggregate check to accept successful or skipped jobs

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

blaginin pushed a commit to branch db/ci-merge-queue-gate
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/db/ci-merge-queue-gate by this push:
     new 2496f8dc94 ci: simplify aggregate check to accept successful or skipped jobs
2496f8dc94 is described below

commit 2496f8dc94abe79c827f07f6ff6b7647409a3bc4
Author: blaginin <gi...@blaginin.me>
AuthorDate: Fri Sep 11 15:08:27 2026 +0100

    ci: simplify aggregate check to accept successful or skipped jobs
---
 .github/actions/check-ci-result/action.yaml |  31 ---------
 .github/workflows/ci.yml                    |  12 ++--
 dev/ci/check-ci-result.py                   |  69 -------------------
 dev/ci/test_ci_queue.py                     | 102 ++++++++++++----------------
 4 files changed, 51 insertions(+), 163 deletions(-)

diff --git a/.github/actions/check-ci-result/action.yaml b/.github/actions/check-ci-result/action.yaml
deleted file mode 100644
index d54142b092..0000000000
--- a/.github/actions/check-ci-result/action.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-# 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.
-
-name: Check CI result
-description: Require every planned CI workflow to finish successfully
-inputs:
-  needs-json:
-    description: JSON of the caller job's complete needs context
-    required: true
-runs:
-  using: composite
-  steps:
-    - name: Verify planned CI results
-      shell: bash
-      env:
-        CI_NEEDS_JSON: ${{ inputs.needs-json }}
-      run: python3 "$GITHUB_WORKSPACE/dev/ci/check-ci-result.py"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98aaf80000..faef9b0023 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -339,10 +339,14 @@ jobs:
       - iceberg_1_10
       - iceberg_1_11
     runs-on: ubuntu-slim
+    timeout-minutes: 5
     permissions:
       contents: read
     steps:
-      - uses: actions/checkout@v7
-      - uses: ./.github/actions/check-ci-result
-        with:
-          needs-json: ${{ toJSON(needs) }}
+      - name: Check CI jobs
+        shell: bash
+        env:
+          NEEDS: ${{ toJSON(needs) }}
+        run: |
+          jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$NEEDS"
+          jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<< "$NEEDS"
diff --git a/dev/ci/check-ci-result.py b/dev/ci/check-ci-result.py
deleted file mode 100644
index 9ae801a833..0000000000
--- a/dev/ci/check-ci-result.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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.
-
-"""Validate the umbrella's results, including jobs legitimately skipped by policy."""
-
-import json
-import os
-import sys
-
-# Routing output -> reusable caller id. Keep synchronized with ci.yml.
-JOBS = {
-    "build_linux": "pr_build_linux",
-    "build_macos": "pr_build_macos",
-    "benchmark": "pr_benchmark_check",
-    "docs": "docs",
-    "spark_3_4": "spark_3_4",
-    "spark_3_5": "spark_3_5",
-    "spark_4_0": "spark_4_0",
-    "spark_4_1": "spark_4_1",
-    "iceberg_1_8": "iceberg_1_8",
-    "iceberg_1_9": "iceberg_1_9",
-    "iceberg_1_10": "iceberg_1_10",
-    "iceberg_1_11": "iceberg_1_11",
-}
-
-
-def failures(needs):
-    errors = []
-    expected = {"preflight", "changes", *JOBS.values()}
-    if set(needs) != expected:
-        errors.append(f"Unexpected dependency set: missing {sorted(expected - set(needs))}, "
-                      f"extra {sorted(set(needs) - expected)}")
-    for job in ("preflight", "changes"):
-        if needs.get(job, {}).get("result") != "success":
-            errors.append(f"{job} must succeed")
-    plan = needs.get("changes", {}).get("outputs", {})
-    for output, job in JOBS.items():
-        flag = plan.get(output)
-        result = needs.get(job, {}).get("result")
-        if flag not in ("true", "false"):
-            errors.append(f"Missing or invalid plan for {job}: {flag!r}")
-        elif flag == "true" and result != "success":
-            errors.append(f"Required {job}: {result!r}")
-        elif flag == "false" and result != "skipped":
-            errors.append(f"Unplanned {job}: {result!r} (expected skipped)")
-    return errors
-
-
-if __name__ == "__main__":
-    errors = failures(json.loads(os.environ["CI_NEEDS_JSON"]))
-    for error in errors:
-        print(error, file=sys.stderr)
-    if errors:
-        sys.exit(1)
-    print("All planned CI checks passed")
diff --git a/dev/ci/test_ci_queue.py b/dev/ci/test_ci_queue.py
index 13c18eaf85..314e42126c 100644
--- a/dev/ci/test_ci_queue.py
+++ b/dev/ci/test_ci_queue.py
@@ -16,8 +16,12 @@
 # under the License.
 
 import importlib.util
+import json
+import os
 from pathlib import Path
 import re
+import subprocess
+import textwrap
 import unittest
 
 ROOT = Path(__file__).resolve().parents[2]
@@ -31,7 +35,6 @@ def load(name):
 
 
 routing = load("compute-changes")
-gate = load("check-ci-result")
 PR_JOBS = {"build_linux", "build_macos", "benchmark", "spark_3_5", "spark_4_1", "iceberg_1_11"}
 OTHER_VERSIONS = {"spark_3_4", "spark_4_0", "iceberg_1_8", "iceberg_1_9", "iceberg_1_10"}
 QUEUE_JOBS = PR_JOBS | OTHER_VERSIONS
@@ -80,65 +83,46 @@ class QueuePolicyTest(unittest.TestCase):
 
 
 class RequiredGateTest(unittest.TestCase):
-    def fixture(self, planned=()):
-        outputs = {key: str(key in planned).lower() for key in gate.JOBS}
-        needs = {"preflight": {"result": "success"},
-                 "changes": {"result": "success", "outputs": outputs}}
-        needs.update({job: {"result": "success" if key in planned else "skipped"}
-                      for key, job in gate.JOBS.items()})
-        return needs
-
-    def test_docs_skip_and_full_queue_success(self):
-        self.assertEqual(gate.failures(self.fixture()), [])
-        self.assertEqual(gate.failures(self.fixture(QUEUE_JOBS)), [])
-
-    def test_any_required_group_failure_cancellation_or_skip_blocks(self):
-        for job in gate.JOBS.values():
-            for status in ("failure", "cancelled", "skipped", None):
-                with self.subTest(job=job, status=status):
-                    needs = self.fixture(gate.JOBS)
-                    needs[job]["result"] = status
-                    self.assertTrue(gate.failures(needs))
-
-    def test_preflight_and_planner_cannot_hide_behind_skipped_children(self):
-        for job in ("preflight", "changes"):
-            for status in ("failure", "cancelled", "skipped"):
-                needs = self.fixture()
-                needs[job]["result"] = status
-                self.assertTrue(gate.failures(needs))
-
-    def test_missing_plan_and_unknown_dependency_fail_closed(self):
-        needs = self.fixture()
-        del needs["changes"]["outputs"]["iceberg_1_11"]
-        self.assertTrue(gate.failures(needs))
-        needs = self.fixture()
-        needs["unknown"] = {"result": "success"}
-        self.assertTrue(gate.failures(needs))
-        needs = self.fixture()
-        del needs["spark_4_1"]
-        self.assertTrue(gate.failures(needs))
-
-    def test_unplanned_failure_is_not_ignored(self):
-        needs = self.fixture()
-        needs["spark_4_1"]["result"] = "failure"
-        self.assertTrue(gate.failures(needs))
-
-    def test_workflow_wiring_covers_every_caller(self):
-        workflow = (ROOT / ".github/workflows/ci.yml").read_text()
-        blocks = dict(re.findall(r"^  ([\w-]+):\n(.*?)(?=^  [\w-]+:|\Z)", workflow.split("\njobs:\n", 1)[1], re.M | re.S))
-        self.assertEqual(set(blocks), {"preflight", "changes", "ci-required", *gate.JOBS.values()})
-        required = blocks["ci-required"]
-        for job in {"preflight", "changes", *gate.JOBS.values()}:
-            self.assertIn(f"      - {job}\n", required)
-        for key, job in gate.JOBS.items():
-            self.assertIn(f"if: needs.changes.outputs.{key} == 'true'", blocks[job])
-        self.assertIn("if: ${{ always() }}", required)
-        self.assertIn("github.event.action == 'labeled'", required)
-        self.assertIn("'CI Optional' || 'CI Required'", required)
-        self.assertIn("  merge_group:\n    types: [checks_requested]", workflow)
-        self.assertIn('"${QUEUE_BASE_SHA:?}".."${QUEUE_HEAD_SHA:?}"', workflow)
+    @classmethod
+    def setUpClass(cls):
+        cls.workflow = (ROOT / ".github/workflows/ci.yml").read_text()
+        cls.blocks = dict(re.findall(
+            r"^  ([\w-]+):\n(.*?)(?=^  [\w-]+:|\Z)",
+            cls.workflow.split("\njobs:\n", 1)[1], re.M | re.S))
+        cls.required = cls.blocks["ci-required"]
+        # Exercise the actual shell step, not a separate copy of its predicate.
+        cls.script = textwrap.dedent(cls.required.split("        run: |\n", 1)[1])
+
+    def run_gate(self, results):
+        needs = {job: {"result": result} for job, result in results.items()}
+        return subprocess.run(
+            ["bash", "-e", "-o", "pipefail", "-c", self.script],
+            env={**os.environ, "NEEDS": json.dumps(needs)},
+            capture_output=True, text=True).returncode
+
+    def test_success_and_skipped_jobs_pass_without_a_job_registry(self):
+        self.assertEqual(self.run_gate({"preflight": "success", "changes": "success",
+                                       "new-test-job": "success", "docs": "skipped"}), 0)
+        self.assertEqual(self.run_gate({"optional-job": "skipped"}), 0)
+
+    def test_failed_cancelled_or_missing_result_blocks(self):
+        for job in ("preflight", "changes", "new-test-job"):
+            for result in ("failure", "cancelled", None):
+                with self.subTest(job=job, result=result):
+                    results = {"preflight": "success", "changes": "success",
+                               "new-test-job": "success", "docs": "skipped"}
+                    results[job] = result
+                    self.assertNotEqual(self.run_gate(results), 0)
+
+    def test_workflow_wiring_covers_every_job(self):
+        dependencies = set(re.findall(r"^      - ([\w-]+)$", self.required, re.M))
+        self.assertEqual(dependencies, set(self.blocks) - {"ci-required"})
+        self.assertIn("if: ${{ always() }}", self.required)
+        self.assertIn("github.event.action == 'labeled'", self.required)
+        self.assertIn("'CI Optional' || 'CI Required'", self.required)
+        self.assertIn("  merge_group:\n    types: [checks_requested]", self.workflow)
+        self.assertIn('"${QUEUE_BASE_SHA:?}".."${QUEUE_HEAD_SHA:?}"', self.workflow)
         self.assertIn('          - "CI Required"', (ROOT / ".asf.yaml").read_text())
-        self.assertEqual(set(gate.JOBS), set(routing.POLICY))
 
 
 if __name__ == "__main__":


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org