blob: f5a7b02fc51f911b159da167519b805f75dcb239 [file]
# 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.
# Replacement for dorny/paths-filter, which is not on the apache org allow
# list. Reads a list of changed files (one per line) and emits per-job
# "<name>=true|false" lines suitable for $GITHUB_OUTPUT.
#
# Each output folds together two independent questions:
#
# 1. Did the change touch files this job covers? FILTERS, below. Pattern
# semantics match dorny/picomatch: "**" spans path segments, "*" stays
# within a segment, and a leading "!" marks an exclude pattern.
# 2. Does this event permit the job to run at all? POLICY, below.
#
# Question 2 used to live in ci.yml as a four-line `${{ }}` expression
# repeated on every heavy job. Keeping it here instead means the whole
# routing policy is in one place, is readable without evaluating GitHub
# expression syntax in your head, and is covered by the cases in
# dev/ci/check-ci-config.py, which YAML expressions never could be.
import json
import os
import re
import sys
from pathlib import Path
FILTERS = {
"build_linux": [
"native/**",
"common/**",
"spark/**",
"spark-integration/**",
"pom.xml",
"**/pom.xml",
".mvn/**",
"mvnw",
"Makefile",
"rust-toolchain.toml",
"dev/ci/**",
".github/workflows/ci.yml",
".github/workflows/pr_build_linux.yml",
".github/actions/setup-builder/**",
".github/actions/java-test/**",
".github/actions/rust-test/**",
".github/actions/upload-artifact-retry/**",
"!**.md",
"!native/core/benches/**",
"!native/spark-expr/benches/**",
"!spark/src/test/scala/org/apache/spark/sql/benchmark/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
],
"build_macos": [
"native/**",
"common/**",
"spark/**",
"spark-integration/**",
"pom.xml",
"**/pom.xml",
".mvn/**",
"mvnw",
"Makefile",
"rust-toolchain.toml",
"dev/ci/**",
".github/workflows/ci.yml",
".github/workflows/pr_build_macos.yml",
".github/actions/setup-macos-builder/**",
".github/actions/java-test/**",
".github/actions/upload-artifact-retry/**",
"!**.md",
"!native/core/benches/**",
"!native/spark-expr/benches/**",
"!spark/src/test/scala/org/apache/spark/sql/benchmark/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
],
"benchmark": [
"native/core/benches/**",
"native/spark-expr/benches/**",
"spark/src/test/scala/org/apache/spark/sql/benchmark/**",
],
"docs": [
".asf.yaml",
".github/workflows/docs.yaml",
"docs/**",
# Generated docs (configs.md, per-version expression compatibility pages) are
# built from these Scala sources by GenerateDocs, so changes to them must
# republish the site even when no docs/ file is touched.
"spark/src/main/scala/org/apache/comet/CometConf.scala",
"spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/src/main/scala/org/apache/comet/serde/**",
"spark/src/main/scala/org/apache/comet/expressions/**",
"spark/src/main/spark-*/**",
],
"spark_3_4": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/spark-3.5/**",
"!spark/src/main/spark-4.0/**",
"!spark/src/main/spark-4.1/**",
"!spark/src/main/spark-4.2/**",
"!spark/src/main/spark-4.x/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/3.4.3.diff",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/spark_sql_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-spark-builder/**",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"spark_3_5": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/spark-3.4/**",
"!spark/src/main/spark-4.0/**",
"!spark/src/main/spark-4.1/**",
"!spark/src/main/spark-4.2/**",
"!spark/src/main/spark-4.x/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/3.5.9.diff",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/spark_sql_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-spark-builder/**",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"spark_4_0": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/spark-3.4/**",
"!spark/src/main/spark-3.5/**",
"!spark/src/main/spark-3.x/**",
"!spark/src/main/spark-4.1/**",
"!spark/src/main/spark-4.2/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/4.0.4.diff",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/spark_sql_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-spark-builder/**",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"spark_4_1": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/spark-3.4/**",
"!spark/src/main/spark-3.5/**",
"!spark/src/main/spark-3.x/**",
"!spark/src/main/spark-4.0/**",
"!spark/src/main/spark-4.2/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/4.1.3.diff",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/spark_sql_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-spark-builder/**",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"iceberg_1_8": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/iceberg/**",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/iceberg_spark_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-iceberg-builder/**",
"dev/ci/iceberg-test-shards.gradle",
"dev/ci/check-iceberg-shards.py",
"dev/ci/test-iceberg-shards.py",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"iceberg_1_9": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/iceberg/**",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/iceberg_spark_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-iceberg-builder/**",
"dev/ci/iceberg-test-shards.gradle",
"dev/ci/check-iceberg-shards.py",
"dev/ci/test-iceberg-shards.py",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"iceberg_1_10": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/iceberg/**",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/iceberg_spark_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-iceberg-builder/**",
"dev/ci/iceberg-test-shards.gradle",
"dev/ci/check-iceberg-shards.py",
"dev/ci/test-iceberg-shards.py",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
"iceberg_1_11": [
"native/**/src/**",
"native/**/Cargo.toml",
"native/Cargo.lock",
"common/src/main/**",
"common/pom.xml",
"spark/src/main/**",
"!spark/src/main/scala/org/apache/comet/GenerateDocs.scala",
"spark/pom.xml",
"dev/diffs/iceberg/**",
"pom.xml",
"rust-toolchain.toml",
".github/workflows/ci.yml",
".github/workflows/iceberg_spark_test_reusable.yml",
".github/actions/setup-builder/**",
".github/actions/setup-iceberg-builder/**",
"dev/ci/iceberg-test-shards.gradle",
"dev/ci/check-iceberg-shards.py",
"dev/ci/test-iceberg-shards.py",
".github/actions/upload-artifact-retry/**",
".mvn/**",
"mvnw",
],
}
# Which events may run each job, independent of the path filters above.
#
# "pr" every pull request
# "push" push to main
# "label:<name>" a pull request carrying that label
# "queue" a merge group requesting checks
#
# workflow_dispatch always runs everything, so it is not listed. "pr" and
# "label:" are mutually exclusive -- a job is either unconditional on pull
# requests or opt-in, never both -- and check-ci-config.py rejects a job that
# lists both rather than letting the label quietly win.
POLICY = {
"build_linux": ["pr", "push", "queue"],
"build_macos": ["pr", "push", "queue"],
"benchmark": ["pr", "push", "queue"],
# docs deploys to asf-site, so it must not run from a PR or merge group.
"docs": ["push"],
"spark_3_4": ["push", "queue", "label:run-spark-3.4-tests"],
"spark_3_5": ["pr", "push", "queue"],
"spark_4_0": ["push", "queue", "label:run-spark-4.0-tests"],
"spark_4_1": ["pr", "push", "queue"],
"iceberg_1_8": ["push", "queue", "label:run-iceberg-tests"],
"iceberg_1_9": ["push", "queue", "label:run-iceberg-tests"],
"iceberg_1_10": ["push", "queue", "label:run-iceberg-tests"],
# Iceberg 1.11 is our only Spark 4.1 Iceberg coverage, so it is not opt-in.
"iceberg_1_11": ["pr", "push", "queue"],
}
def gating_labels(job):
return [t[len("label:"):] for t in POLICY[job] if t.startswith("label:")]
def event_allows(job, event):
"""Does `event` permit `job` to run, ignoring which files changed?
`event` is {"name", "action", "label", "labels"}: the workflow event name,
the pull_request action, the label just added on a `labeled` event, and the
labels currently on the pull request.
"""
tiers = POLICY[job]
name = event.get("name")
if name == "workflow_dispatch":
return True
if name == "push":
return "push" in tiers
if name == "merge_group":
return event.get("action") == "checks_requested" and "queue" in tiers
if name != "pull_request":
return False
gates = gating_labels(job)
if gates:
if not any(label in event.get("labels", []) for label in gates):
return False
elif "pr" not in tiers:
return False
# A `labeled` event fires at the same commit as the opened/synchronize run
# that already tested it, and GitHub cannot filter a pull_request trigger
# by label name. So on `labeled`, run only the job the new label gates;
# everything else would be duplicating a pipeline. See issue #5007 for what
# happens when this is expressed as a job-level `if:` instead.
if event.get("action") == "labeled":
return event.get("label") in gates
return True
def compute(files, event):
"""Return {job: bool}, folding the path filter and the event policy."""
return {
name: event_allows(name, event) and matches(patterns, files)
for name, patterns in FILTERS.items()
}
def event_from_env():
labels = os.environ.get("PR_LABELS", "")
return {
"name": os.environ.get("EVENT_NAME", ""),
"action": os.environ.get("EVENT_ACTION", ""),
"label": os.environ.get("LABEL_NAME", ""),
"labels": json.loads(labels) if labels.strip() else [],
}
def glob_to_regex(pat):
# Translate a picomatch-style glob to a regex. "**/" at the start or
# interior is optional ("(?:.*/)?") so that "**/pom.xml" matches at the
# repo root; bare "**" is greedy across path separators.
out = []
i = 0
while i < len(pat):
c = pat[i]
if c == "*" and i + 1 < len(pat) and pat[i + 1] == "*":
if i + 2 < len(pat) and pat[i + 2] == "/":
out.append("(?:.*/)?")
i += 3
else:
out.append(".*")
i += 2
elif c == "*":
out.append("[^/]*")
i += 1
elif c == "?":
out.append("[^/]")
i += 1
elif c in r".+(){}[]^$|\\":
out.append("\\" + c)
i += 1
else:
out.append(c)
i += 1
return "^" + "".join(out) + "$"
def matches(patterns, files):
includes = [re.compile(glob_to_regex(p)) for p in patterns if not p.startswith("!")]
excludes = [re.compile(glob_to_regex(p[1:])) for p in patterns if p.startswith("!")]
for f in files:
if any(r.match(f) for r in includes) and not any(r.match(f) for r in excludes):
return True
return False
if __name__ == "__main__":
event = event_from_env()
# workflow_dispatch has no meaningful base to diff against, so the caller
# passes an empty list and every path filter is treated as matched.
if event["name"] == "workflow_dispatch":
for name in FILTERS:
print(f"{name}=true")
sys.exit(0)
files_path = Path(sys.argv[1])
files = [line.strip() for line in files_path.read_text().splitlines() if line.strip()]
for name, flag in compute(files, event).items():
print(f"{name}={'true' if flag else 'false'}")