This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 8d1b1c9ac8 [Improve][CI] Scope backend matrix for fork changes (#11290)
8d1b1c9ac8 is described below
commit 8d1b1c9ac853152178733a60ee9ae997567c3100
Author: Daniel <[email protected]>
AuthorDate: Mon Jul 27 22:45:17 2026 +0800
[Improve][CI] Scope backend matrix for fork changes (#11290)
---
.github/workflows/backend.yml | 32 ++++++-
docs/en/developer/contribution-path.md | 15 ++++
docs/zh/developer/contribution-path.md | 15 ++++
tools/update_modules_check/ci_scope.py | 134 ++++++++++++++++++++++++++++
tools/update_modules_check/test_ci_scope.py | 128 ++++++++++++++++++++++++++
5 files changed, 321 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 6ec74cd558..e70dc458c7 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -58,6 +58,8 @@ jobs:
run: ./mvnw -B -T 1 clean test -D"license.skipAddThirdParty"=true -pl
seatunnel-ci-tools -am --no-snapshot-updates
env:
MAVEN_OPTS: -Xmx512m
+ - name: Check CI scope rules
+ run: python3 -m unittest discover -s tools/update_modules_check -p
'test_*.py'
- name: Check for .class files in git
run: |
echo "Checking for .class files tracked by git..."
@@ -209,10 +211,25 @@ jobs:
echo "edge-agent=$true_or_false" >> $GITHUB_OUTPUT
echo "edge-agent_files=$file_list" >> $GITHUB_OUTPUT
+ dist_files=`python tools/update_modules_check/check_file_updates.py
ua $workspace apache/dev origin/$current_branch "seatunnel-dist/**"
"bin/install-plugin.sh"`
+ true_or_false=${dist_files%%$'\n'*}
+ file_list=${dist_files#*$'\n'}
+ echo "dist=$true_or_false" >> $GITHUB_OUTPUT
+ echo "dist_files=$file_list" >> $GITHUB_OUTPUT
+
api_files=`python tools/update_modules_check/check_file_updates.py
ua $workspace apache/dev origin/$current_branch "seatunnel-api/**"
"seatunnel-common/**" "seatunnel-config/**" "seatunnel-core/**"
"seatunnel-e2e/seatunnel-e2e-common/**" "seatunnel-formats/**"
"seatunnel-plugin-discovery/**" "seatunnel-transforms-v2/**"
"seatunnel-translation/**" "seatunnel-e2e/seatunnel-transforms-v2-e2e/**"
"pom.xml" "**/workflows/**" "tools/**" "seatunnel-dist/**"`
true_or_false=${api_files%%$'\n'*}
file_list=${api_files#*$'\n'}
- if [[ $repository_owner == 'apache' ]];then
+ # Apache dev, main, master, and numeric *-release pushes force full
API coverage.
+ # On fork pushes, workflow files, tools/update_modules_check,
seatunnel-dist, and
+ # bin/install-plugin.sh do not trigger the full connector matrix by
themselves.
+ ci_scope_ref="${GITHUB_BASE_REF:-$GITHUB_REF}"
+ if ! true_or_false=$(python tools/update_modules_check/ci_scope.py
"$repository_owner" "$ci_scope_ref" --api-changed "$true_or_false"
--api-files-json "$file_list"); then
+ echo "::warning::CI scope helper failed; forcing full API coverage"
+ true_or_false='true'
+ fi
+ if [[ $true_or_false != 'true' && $true_or_false != 'false' ]]; then
+ echo "::warning::CI scope helper returned an invalid value;
forcing full API coverage"
true_or_false='true'
fi
echo "api=$true_or_false" >> $GITHUB_OUTPUT
@@ -266,9 +283,13 @@ jobs:
- name: Make unit test modules
id: ut-modules
timeout-minutes: 60
- if: ${{ steps.filter.outputs.api == 'false' &&
(steps.engine-modules.outputs.modules != '' ||
steps.cv2-modules.outputs.modules != '') }}
+ if: ${{ steps.filter.outputs.api == 'false' &&
(steps.engine-modules.outputs.modules != '' ||
steps.cv2-modules.outputs.modules != '' || steps.filter.outputs.dist == 'true')
}}
run: |
- modules='${{ steps.engine-modules.outputs.modules }}${{
steps.cv2-modules.outputs.modules }}'
+ dist_modules=''
+ if [[ '${{ steps.filter.outputs.dist }}' == 'true' ]]; then
+ dist_modules=',seatunnel-dist'
+ fi
+ modules='${{ steps.engine-modules.outputs.modules }}${{
steps.cv2-modules.outputs.modules }}'$dist_modules
modules=${modules: 1}
pl_modules=`python
tools/update_modules_check/update_modules_check.py replace "$modules"`
# remove deleted modules
@@ -283,6 +304,11 @@ jobs:
engine_modules='${{ steps.engine-modules.outputs.modules }}'
connector_modules='${{ steps.cv2-modules.outputs.modules }}'
+ if [[ "zz${engine_modules}${connector_modules}" == "zz" &&
"zz"$dist_modules != "zz" ]];then
+ echo $pl_modules
+ echo "modules=$pl_modules" >> $GITHUB_OUTPUT
+ exit 0
+ fi
if [[ "zz"$connector_modules == "zz" && "zz"$engine_modules != "zz"
]];then
# Engine changes already route the full downstream engine test set
through engine=true.
# Keep the fixed smoke modules as the output without resolving the
Maven dependency tree.
diff --git a/docs/en/developer/contribution-path.md
b/docs/en/developer/contribution-path.md
index 23595e7195..e675fbca3f 100644
--- a/docs/en/developer/contribution-path.md
+++ b/docs/en/developer/contribution-path.md
@@ -118,6 +118,21 @@ A contribution is much easier to review when it includes:
For code contributions, avoid mixing unrelated cleanup with the real fix.
+## Understand Backend CI Coverage
+
+The required Backend `Build` on a pull request mirrors the `push` workflow run
in the PR head repository, which is usually the contributor's fork. It is not a
separate full-matrix run in the base repository.
+
+The workflow selects jobs as follows:
+
+- pushes in the Apache repository to `dev`, `main`, `master`, and numeric
release branches such as `2.3.13-release` force the full API-triggered backend
matrix
+- broad API-impacting changes, including API, core, common, format, transform,
translation, and root build changes, trigger the full API matrix
+- connector-only changes use changed-module detection to select
integration-test shards; when the unit-test job is selected, it still verifies
all modules
+- engine changes keep their existing engine and connector integration-test
paths
+- fork pushes that only change `.github/workflows/**`,
`tools/update_modules_check/**`, `seatunnel-dist/**`, or
`bin/install-plugin.sh` do not trigger every connector integration-test shard
by themselves; the full unit-test job can still run when a changed module
requires it
+- if the CI scope helper fails or returns an invalid result, the workflow
falls back to full API coverage
+
+Check the jobs listed under the Backend `Build` before assuming that a green
result includes every connector integration-test shard.
+
## Good First Contribution Shapes
These contribution shapes tend to land faster:
diff --git a/docs/zh/developer/contribution-path.md
b/docs/zh/developer/contribution-path.md
index aa10f775db..342233637c 100644
--- a/docs/zh/developer/contribution-path.md
+++ b/docs/zh/developer/contribution-path.md
@@ -118,6 +118,21 @@ title: 贡献路径
对代码贡献来说,不要把无关清理和真实修复混在一起。
+## 理解 Backend CI 的覆盖范围
+
+Pull Request 上要求通过的 Backend `Build`,映射自 PR head 仓库(通常是贡献者的 fork)中的 `push`
workflow;它不是 base 仓库另外执行的一次完整矩阵。
+
+workflow 按下面的规则选择 jobs:
+
+- 推送到 Apache 仓库的 `dev`、`main`、`master` 和数字版本发布分支(例如 `2.3.13-release`)时,强制运行由
API 变更触发的完整后端矩阵
+- 修改 API、core、common、format、transform、translation 和根构建文件等具有广泛 API 影响的范围时,运行完整
API 矩阵
+- 仅修改 connector 时,通过 changed-module 判断选择集成测试分片;如果命中 unit-test job,该 job
仍会验证所有模块
+- 修改 engine 时,沿用现有的 engine 和 connector 集成测试路径
+- fork 分支仅修改
`.github/workflows/**`、`tools/update_modules_check/**`、`seatunnel-dist/**` 或
`bin/install-plugin.sh` 时,这些文件本身不会触发所有 connector 集成测试分片;如果变更模块要求运行单元测试,完整
unit-test job 仍可能执行
+- 如果 CI 范围判断工具执行失败或返回非法结果,workflow 会回退到完整 API 覆盖
+
+判断一次绿色 Backend `Build` 是否包含所有 connector 集成测试分片前,请先查看该 Build 实际列出的 jobs。
+
## 哪类首个贡献更容易落地
这些类型通常更容易合入:
diff --git a/tools/update_modules_check/ci_scope.py
b/tools/update_modules_check/ci_scope.py
new file mode 100644
index 0000000000..8f15f6d988
--- /dev/null
+++ b/tools/update_modules_check/ci_scope.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Helpers for deciding when the backend workflow must keep full API
coverage."""
+
+import argparse
+import json
+import re
+from typing import Iterable
+
+
+PROTECTED_BRANCH_NAMES = {"dev", "main", "master"}
+RELEASE_BRANCH_PATTERN = re.compile(r"\d+\.\d+(?:\.\d+)?-release")
+LIGHTWEIGHT_API_FILE_PREFIXES = (
+ ".github/workflows/",
+ "seatunnel-dist/",
+ "tools/update_modules_check/",
+)
+LIGHTWEIGHT_API_FILES = {"bin/install-plugin.sh"}
+
+
+def should_force_full_api_check(repository_owner: str, github_ref: str) ->
bool:
+ """Return whether the backend workflow should bypass incremental CI
scoping."""
+
+ if repository_owner != "apache":
+ return False
+
+ ref_prefix = "refs/heads/"
+ if github_ref.startswith(ref_prefix):
+ ref_name = github_ref[len(ref_prefix) :]
+ elif github_ref.startswith("refs/"):
+ return False
+ else:
+ # GITHUB_BASE_REF is a bare branch name on pull_request events.
+ ref_name = github_ref
+
+ return (
+ ref_name in PROTECTED_BRANCH_NAMES
+ or RELEASE_BRANCH_PATTERN.fullmatch(ref_name) is not None
+ )
+
+
+def is_lightweight_api_file(path: str) -> bool:
+ """Return true when a broad API glob matched a file that has a cheaper
check path."""
+
+ return path in LIGHTWEIGHT_API_FILES or any(
+ path.startswith(prefix) for prefix in LIGHTWEIGHT_API_FILE_PREFIXES
+ )
+
+
+def should_run_full_api_check(
+ repository_owner: str,
+ github_ref: str,
+ api_changed: bool,
+ api_changed_files: Iterable[str],
+) -> bool:
+ """Return whether the backend workflow should run the full API-triggered
matrix."""
+
+ if should_force_full_api_check(repository_owner, github_ref):
+ return True
+
+ if not api_changed:
+ return False
+
+ return any(not is_lightweight_api_file(path) for path in api_changed_files)
+
+
+def parse_bool(value: str) -> bool:
+ """Parse the shell-friendly booleans emitted by check_file_updates.py."""
+
+ if value == "true":
+ return True
+ if value == "false":
+ return False
+ raise ValueError(f"invalid boolean: {value}")
+
+
+def parse_changed_files(value: str) -> list[str]:
+ """Parse a JSON list of changed file paths."""
+
+ parsed = json.loads(value)
+ if not isinstance(parsed, list) or not all(isinstance(item, str) for item
in parsed):
+ raise ValueError("api files must be a JSON array of strings")
+ return parsed
+
+
+def main() -> None:
+ """Print a shell-friendly boolean for the workflow caller."""
+
+ parser = argparse.ArgumentParser(
+ description="Decide whether the backend workflow should force full API
coverage."
+ )
+ parser.add_argument("repository_owner", help="Value of
GITHUB_REPOSITORY_OWNER")
+ parser.add_argument("github_ref", help="Value of GITHUB_BASE_REF or
GITHUB_REF")
+ parser.add_argument(
+ "--api-changed",
+ choices=("true", "false"),
+ help="Whether API-scoped globs matched changed files",
+ )
+ parser.add_argument(
+ "--api-files-json",
+ default="[]",
+ help="JSON array emitted by check_file_updates.py for API-scoped
globs",
+ )
+ args = parser.parse_args()
+
+ if args.api_changed is None:
+ result = should_force_full_api_check(args.repository_owner,
args.github_ref)
+ else:
+ result = should_run_full_api_check(
+ args.repository_owner,
+ args.github_ref,
+ parse_bool(args.api_changed),
+ parse_changed_files(args.api_files_json),
+ )
+
+ print("true" if result else "false")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/update_modules_check/test_ci_scope.py
b/tools/update_modules_check/test_ci_scope.py
new file mode 100644
index 0000000000..886cd0ebea
--- /dev/null
+++ b/tools/update_modules_check/test_ci_scope.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Regression tests for backend workflow CI scoping decisions."""
+
+import unittest
+
+from ci_scope import should_force_full_api_check, should_run_full_api_check
+
+
+class CiScopeTest(unittest.TestCase):
+ """Verify that heavyweight CI only runs when a change really needs it."""
+
+ def test_force_full_api_check_on_dev_branch(self) -> None:
+ self.assertTrue(should_force_full_api_check("apache",
"refs/heads/dev"))
+
+ def test_force_full_api_check_on_main_and_master_branches(self) -> None:
+ for branch_name in ("main", "master"):
+ with self.subTest(branch_name=branch_name):
+ self.assertTrue(
+ should_force_full_api_check("apache",
f"refs/heads/{branch_name}")
+ )
+
+ def test_force_full_api_check_on_release_branch(self) -> None:
+ self.assertTrue(should_force_full_api_check("apache",
"refs/heads/2.3.13-release"))
+
+ def test_force_full_api_check_on_two_component_release_branch(self) ->
None:
+ self.assertTrue(should_force_full_api_check("apache",
"refs/heads/2.4-release"))
+
+ def test_force_full_api_check_on_pull_request_base_branch(self) -> None:
+ self.assertTrue(should_force_full_api_check("apache", "dev"))
+
+ def test_skip_force_full_api_check_for_fork_owner(self) -> None:
+ self.assertFalse(should_force_full_api_check("DanielLeens",
"refs/heads/dev"))
+
+ def test_skip_force_full_api_check_on_pull_request_merge_ref(self) -> None:
+ self.assertFalse(should_force_full_api_check("apache",
"refs/pull/123/merge"))
+
+ def test_skip_force_full_api_check_on_non_version_release_branch(self) ->
None:
+ self.assertFalse(
+ should_force_full_api_check("apache", "refs/heads/hotfix-release")
+ )
+
+ def test_skip_force_full_api_check_on_release_tag(self) -> None:
+ self.assertFalse(
+ should_force_full_api_check("apache", "refs/tags/2.3.13-release")
+ )
+
+ def test_skip_full_api_check_for_ci_only_fork_change(self) -> None:
+ self.assertFalse(
+ should_run_full_api_check(
+ "DanielLeens",
+ "refs/heads/ci-scope",
+ True,
+ [
+ ".github/workflows/backend.yml",
+ "tools/update_modules_check/ci_scope.py",
+ ],
+ )
+ )
+
+ def test_skip_full_api_check_for_dist_only_fork_change(self) -> None:
+ self.assertFalse(
+ should_run_full_api_check(
+ "DanielLeens",
+ "refs/heads/install-plugin",
+ True,
+ [
+ "bin/install-plugin.sh",
+ "seatunnel-dist/src/test/java/"
+
"org/apache/seatunnel/installer/InstallPluginScriptTest.java",
+ ],
+ )
+ )
+
+ def test_run_full_api_check_for_source_api_change(self) -> None:
+ self.assertTrue(
+ should_run_full_api_check(
+ "DanielLeens",
+ "refs/heads/api-change",
+ True,
+ [
+ "seatunnel-api/src/main/java/"
+ "org/apache/seatunnel/api/table/type/SeaTunnelRow.java"
+ ],
+ )
+ )
+
+ def test_run_full_api_check_for_mixed_lightweight_and_source_change(self)
-> None:
+ self.assertTrue(
+ should_run_full_api_check(
+ "DanielLeens",
+ "refs/heads/mixed-change",
+ True,
+ [
+ ".github/workflows/backend.yml",
+ "seatunnel-core/seatunnel-starter/src/main/java/"
+ "org/apache/seatunnel/core/starter/SeaTunnel.java",
+ ],
+ )
+ )
+
+ def test_protected_branch_keeps_full_api_for_lightweight_change(self) ->
None:
+ self.assertTrue(
+ should_run_full_api_check(
+ "apache",
+ "refs/heads/dev",
+ True,
+ [".github/workflows/backend.yml"],
+ )
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()