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