From 5f493c1d2fed97ca2825c6b4b9d64d2b7d85cfa4 Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Fri, 25 Sep 2026 12:52:21 +0800 Subject: [PATCH] Avoid repeating CI builds after routing-only merges --- .github/scripts/ci_changes.py | 25 ++++++++++++++++++-- .github/scripts/test_ci_changes.py | 38 ++++++++++++++++++++++++++++++ MAINTAINING.md | 9 ++++--- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/.github/scripts/ci_changes.py b/.github/scripts/ci_changes.py index c59127c7..1393a5d3 100644 --- a/.github/scripts/ci_changes.py +++ b/.github/scripts/ci_changes.py @@ -18,6 +18,17 @@ CPP_SUFFIXES = (".c", ".cc", ".cpp", ".h", ".hpp") +# Keep broad pull-request checks for routing edits, then avoid repeating +# their builds after a CI-only merge into an already released main branch. +CI_ROUTING_PATHS = frozenset( + ( + ".github/scripts/ci_changes.py", + ".github/scripts/test_ci_changes.py", + ".github/workflows/changes.yml", + ".github/workflows/test.yaml", + ) +) + def classify(paths): selected = set() @@ -108,8 +119,14 @@ def detect(event_name, event, ref_name): base = event["before"] if not base or set(base) == {"0"}: return set(GROUPS) - paths = git("diff", "--name-only", "--no-renames", "-z", base, "HEAD").split("\0") - selected = classify(path for path in paths if path) + paths = [ + path + for path in git( + "diff", "--name-only", "--no-renames", "-z", base, "HEAD" + ).split("\0") + if path + ] + selected = classify(paths) if event_name == "push" and ref_name == "main": version = tomllib.loads(Path("pyproject.toml").read_text())["project"][ "version" @@ -117,6 +134,10 @@ def detect(event_name, event, ref_name): if not git("tag", "--list", f"v{version}"): # A docs-only fix after failed release CI must not bypass that CI. return set(GROUPS) + if any(path in CI_ROUTING_PATHS for path in paths) and all( + path in CI_ROUTING_PATHS or path.endswith(".md") for path in paths + ): + return {"python_quality"} return selected diff --git a/.github/scripts/test_ci_changes.py b/.github/scripts/test_ci_changes.py index 0d82b85a..1dc9b3d8 100644 --- a/.github/scripts/test_ci_changes.py +++ b/.github/scripts/test_ci_changes.py @@ -127,6 +127,44 @@ def test_unreleased_version_forces_full_ci_even_for_docs(self): ): self.assertEqual(detect("push", {"before": "base"}, "main"), expected) + def test_ci_routing_changes_run_full_pr_ci_but_not_repeat_on_tagged_main(self): + paths = [ + ".github/scripts/ci_changes.py", + ".github/scripts/test_ci_changes.py", + ".github/workflows/changes.yml", + ".github/workflows/test.yaml", + "MAINTAINING.md", + ] + changed = "\0".join(paths) + "\0" + with patch("ci_changes.git", side_effect=["merge-base", changed]): + self.assertEqual( + detect( + "pull_request", + {"pull_request": {"base": {"sha": "base"}}}, + "42/merge", + ), + set(GROUPS), + ) + for changed_paths, tags, expected in ( + (paths, "v0.3.3", {"python_quality"}), + (paths, "", set(GROUPS)), + (paths + ["src/simd/dispatch.cpp"], "v0.3.3", set(GROUPS)), + ): + with self.subTest(changed_paths=changed_paths, tags=tags): + with ( + patch( + "ci_changes.git", + side_effect=["\0".join(changed_paths) + "\0", tags], + ), + patch( + "ci_changes.Path.read_text", + return_value='[project]\nversion = "0.3.3"', + ), + ): + self.assertEqual( + detect("push", {"before": "base"}, "main"), expected + ) + def test_release_events_and_new_branches_run_everything(self): for event, payload, ref in ( ("workflow_dispatch", {}, "main"), diff --git a/MAINTAINING.md b/MAINTAINING.md index 6f328f74..c64cb902 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -28,9 +28,12 @@ checks. Pushes compare the complete pushed range; pull requests compare against their base. Renames and deletions are included. Relevant jobs also run if change detection fails, so a detection error cannot silently waive a check. -Release tags and manual wheel builds always run the full wheel build. An -untagged version on `main` also forces full C++ and Python CI, even for a -subsequent docs-only commit, before automatic publishing can proceed. +Release tags and manual wheel builds always run the full wheel build. CI +routing edits run the full relevant matrix on pull requests. After an already +released version reaches `main`, a push changing only the routing files and +Markdown reruns Python lint and selector tests without repeating platform +builds. An untagged version on `main` still forces full C++ and Python CI, +even for a subsequent docs-only commit, before automatic publishing can proceed. ## Publishing a release