Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,14 @@ 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"}
if any(path in CI_ROUTING_PATHS for path in paths):
companions = classify(
[path for path in paths if path not in CI_ROUTING_PATHS]
)
# Reuse normal path rules: accompanying lint/support edits must keep
# their checks, while build-related or unknown inputs remain broad.
if companions <= {"python_quality", "shell"}:
return companions | {"python_quality"}
return selected


Expand Down
61 changes: 61 additions & 0 deletions .github/scripts/test_ci_aggregates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Execute the matrix verdict commands with success, skip and failure inputs."""

import os
import re
import subprocess
import textwrap
import unittest
from pathlib import Path

WORKFLOW = Path(__file__).resolve().parents[1] / "workflows" / "test.yaml"


class MatrixSummaryTest(unittest.TestCase):
def test_matrix_verdicts_and_dependencies(self):
workflow = WORKFLOW.read_text()
for job, dependency in (
("clang-result", "clang"),
("core-consumer-result", "core-consumer"),
):
block = re.search(
rf"^ {job}:\n(.*?)(?=^ [\w-]+:|\Z)",
workflow,
re.MULTILINE | re.DOTALL,
).group(1)
self.assertIn(f"needs: [changes, {dependency}]", block)
self.assertIn("if: ${{ always() }}", block)
self.assertIn(f"MATRIX_RESULT: ${{{{ needs.{dependency}.result }}}}", block)
self.assertIn("CHANGES_RESULT: ${{ needs.changes.result }}", block)
self.assertIn("CPP_SELECTED: ${{ needs.changes.outputs.cpp }}", block)
command = textwrap.dedent(block.split(" run: |\n", 1)[1])
for detection in ("success", "failure", "cancelled", "skipped"):
for selected in ("true", "false", ""):
for matrix in ("success", "failure", "cancelled", "skipped", ""):
expected = matrix == "success" or (
detection == "success"
and selected == "false"
and matrix == "skipped"
)
with self.subTest(
job=job,
detection=detection,
selected=selected,
matrix=matrix,
):
result = subprocess.run(
["bash", "-e", "-c", command],
env=dict(
os.environ,
CHANGES_RESULT=detection,
CPP_SELECTED=selected,
MATRIX_RESULT=matrix,
),
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode == 0, expected)


if __name__ == "__main__":
unittest.main()
53 changes: 53 additions & 0 deletions .github/scripts/test_ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,59 @@ def test_ci_routing_changes_run_full_pr_ci_but_not_repeat_on_tagged_main(self):
detect("push", {"before": "base"}, "main"), expected
)

def test_tagged_main_routing_changes_preserve_lightweight_companion_checks(self):
# Complete changed-file set from PR #148, including its lint helper.
routing = [
".github/scripts/ci_changes.py",
".github/scripts/test_ci_changes.py",
".github/workflows/test.yaml",
"docs/development.md",
"docs/maintaining.md",
]
for companions, expected in (
(["scripts/check-python.sh"], {"python_quality", "shell"}),
(["sample/run_cpp.sh"], {"python_quality", "shell"}),
(["benchmarks/plot_homepage.py"], {"python_quality"}),
([".github/ISSUE_TEMPLATE/config.yml"], {"python_quality"}),
(["src/core/api.cpp"], set(GROUPS)),
(["python_bindings/__init__.py"], set(GROUPS)),
(["scripts/build-macos-openmp.sh"], set(GROUPS)),
(["scripts/check-tidy.sh"], set(GROUPS)),
(["CMakeLists.txt"], set(GROUPS)),
(["pyproject.toml"], set(GROUPS)),
(["unknown-build-input"], set(GROUPS)),
(["scripts/check-python.sh", "src/core/api.cpp"], set(GROUPS)),
(["python/ivf.py", "sample/python/clustering_cli/ivf.py"], set(GROUPS)),
):
changed = "\0".join(routing + companions) + "\0"
for event, ref, tags, result in (
("push", "main", "v0.7.0", expected),
("push", "main", "", set(GROUPS)),
("push", "master", None, set(GROUPS)),
("pull_request", "148/merge", None, set(GROUPS)),
):
replies = (
["merge-base", changed] if event == "pull_request" else [changed]
)
if tags is not None:
replies.append(tags)
payload = (
{"pull_request": {"base": {"sha": "base"}}}
if event == "pull_request"
else {"before": "base"}
)
with self.subTest(
companions=companions, event=event, ref=ref, tags=tags
):
with (
patch("ci_changes.git", side_effect=replies),
patch(
"ci_changes.Path.read_text",
return_value='[project]\nversion = "0.7.0"',
),
):
self.assertEqual(detect(event, payload, ref), result)

def test_release_events_and_new_branches_run_everything(self):
for event, payload, ref in (
("workflow_dispatch", {}, "main"),
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,25 @@ jobs:
-DRABITQ_USE_OPENMP=${{ matrix.openmp }}
-P tests/core_consumer/run.cmake

clang-result:
name: Clang validation
needs: [changes, clang]
if: ${{ always() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require selected matrix checks
env:
CHANGES_RESULT: ${{ needs.changes.result }}
CPP_SELECTED: ${{ needs.changes.outputs.cpp }}
MATRIX_RESULT: ${{ needs.clang.result }}
run: |
if [ "$CHANGES_RESULT" = success ] && [ "$CPP_SELECTED" = false ]; then
test "$MATRIX_RESULT" = skipped || test "$MATRIX_RESULT" = success
else
test "$MATRIX_RESULT" = success
fi

windows:
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }}
Expand Down Expand Up @@ -346,6 +365,25 @@ jobs:
-DRABITQ_USE_OPENMP=${{ matrix.openmp }}
-P tests/core_consumer/run.cmake

core-consumer-result:
name: Core and FastScan validation
needs: [changes, core-consumer]
if: ${{ always() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require selected matrix checks
env:
CHANGES_RESULT: ${{ needs.changes.result }}
CPP_SELECTED: ${{ needs.changes.outputs.cpp }}
MATRIX_RESULT: ${{ needs.core-consumer.result }}
run: |
if [ "$CHANGES_RESULT" = success ] && [ "$CPP_SELECTED" = false ]; then
test "$MATRIX_RESULT" = skipped || test "$MATRIX_RESULT" = success
else
test "$MATRIX_RESULT" = success
fi

linux-arm64:
name: Native Linux ARM64 C++
needs: changes
Expand Down
24 changes: 21 additions & 3 deletions docs/maintaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,29 @@ change detection fails, so a detection error cannot silently waive a check.

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,
released version reaches `main`, a push containing routing edits avoids repeated
platform builds only when all accompanying files select at most Python quality
checks and ShellCheck under the normal path rules. Python lint and selector tests
always run in this case, and any accompanying shell checks are retained. For
example, routing plus `scripts/check-python.sh` runs Python quality and ShellCheck;
routing plus library code, build configuration, wheel-related inputs or unknown
paths keeps the full selection. PR routing checks remain broad. An untagged version on `main` still forces full C++ and Python CI,
even for a subsequent docs-only commit, before automatic publishing can proceed.

## Matrix summary checks

`Clang validation` and `Core and FastScan validation` provide stable check names
for the corresponding matrices. They run even when a dependency fails or is
skipped. A successful matrix passes. A skipped matrix passes only when change
detection succeeded and explicitly selected no C++ checks; failure, cancellation,
or an unexpected skip fails the summary. If detection fails, the existing
fallback runs the matrix and its success is required.

After these checks have appeared and been validated on GitHub, add their exact
names to main's required status checks. Workflow definitions do not update branch
protection automatically. Keep existing protections until that transition is
complete; these summaries do not cover unrelated jobs.

## Publishing a release

Update the stable `X.Y.Z` version in both `pyproject.toml` and `CMakeLists.txt`,
Expand Down
Loading