Skip running TheRock CI on docs-only changes(#2246)

Following the pattern from ROCm/rocm-libraries#2679, add logic to skip
CI builds when only documentation files are modified.

Changes:
- Add SKIPPABLE_PATH_PATTERNS for docs, markdown, and .gitignore files
- Return empty projects list when only skippable paths are modified
- No workflow changes needed - existing projects != '[]' check handles it
- Add unit tests for doc-filtering logic
- Fix existing tests with proper subprocess mocking

Reference: https://github.com/ROCm/rocm-libraries/pull/2679
Этот коммит содержится в:
Dominic Widdows
2025-12-12 08:30:59 -08:00
коммит произвёл GitHub
родитель 4870725a62
Коммит 2073cf2172
2 изменённых файлов: 167 добавлений и 40 удалений
+122 -40
Просмотреть файл
@@ -2,72 +2,154 @@ from pathlib import Path
import os
import sys
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
import therock_configure_ci
class ConfigureCITest(unittest.TestCase):
def test_pull_request(self):
@patch("subprocess.run")
def test_pull_request(self, mock_run):
args = {
"is_pull_request": True,
"input_subtrees": "projects/rocprim\nprojects/hipcub"
"base_ref": "HEAD^"
}
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 1)
def test_pull_request_empty(self):
args = {
"is_pull_request": True,
"input_subtrees": ""
}
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 0)
def test_workflow_dispatch(self):
args = {
"is_workflow_dispatch": True,
"input_projects": "projects/rocprim projects/hipcub"
}
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 1)
def test_workflow_dispatch_bad_input(self):
args = {
"is_workflow_dispatch": True,
"input_projects": "projects/rocprim$$projects/hipcub"
}
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 0)
def test_workflow_dispatch_all(self):
args = {
"is_workflow_dispatch": True,
"input_projects": "all"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertGreaterEqual(len(project_to_run), 1)
def test_workflow_dispatch_empty(self):
@patch("subprocess.run")
def test_pull_request_empty(self, mock_run):
args = {
"is_pull_request": True,
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = ""
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
# Empty modified_paths should return empty list (no changes = no CI)
self.assertEqual(len(project_to_run), 0)
@patch("subprocess.run")
def test_workflow_dispatch(self, mock_run):
args = {
"is_workflow_dispatch": True,
"input_projects": ""
"input_projects": "projects/rocminfo projects/clr",
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertGreaterEqual(len(project_to_run), 1)
@patch("subprocess.run")
def test_workflow_dispatch_bad_input(self, mock_run):
args = {
"is_workflow_dispatch": True,
"input_projects": "projects/invalid$$projects/fake",
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 0)
def test_is_push(self):
@patch("subprocess.run")
def test_workflow_dispatch_all(self, mock_run):
args = {
"is_workflow_dispatch": True,
"input_projects": "all",
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertGreaterEqual(len(project_to_run), 1)
@patch("subprocess.run")
def test_workflow_dispatch_empty(self, mock_run):
args = {
"is_workflow_dispatch": True,
"input_projects": "",
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 0)
@patch("subprocess.run")
def test_is_push(self, mock_run):
args = {
"is_push": True,
"base_ref": "HEAD^"
}
mock_process = MagicMock()
mock_process.stdout = "projects/rocminfo/src/main.cpp"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertGreaterEqual(len(project_to_run), 1)
def test_is_path_skippable(self):
# Test skippable patterns
self.assertTrue(therock_configure_ci.is_path_skippable("README.md"))
self.assertTrue(therock_configure_ci.is_path_skippable("docs/guide.rst"))
self.assertTrue(therock_configure_ci.is_path_skippable("projects/rocminfo/README.md"))
self.assertTrue(therock_configure_ci.is_path_skippable("projects/rocminfo/docs/api.rst"))
self.assertTrue(therock_configure_ci.is_path_skippable(".gitignore"))
# Test non-skippable patterns
self.assertFalse(therock_configure_ci.is_path_skippable("projects/rocminfo/src/main.cpp"))
self.assertFalse(therock_configure_ci.is_path_skippable("CMakeLists.txt"))
self.assertFalse(therock_configure_ci.is_path_skippable("projects/rocminfo/test/test.cpp"))
def test_check_for_non_skippable_path(self):
# All skippable paths
skippable_paths = ["README.md", "docs/guide.rst", "projects/rocminfo/docs/api.md"]
self.assertFalse(therock_configure_ci.check_for_non_skippable_path(skippable_paths))
# Mixed paths (has non-skippable)
mixed_paths = ["README.md", "src/main.cpp"]
self.assertTrue(therock_configure_ci.check_for_non_skippable_path(mixed_paths))
# None input
self.assertFalse(therock_configure_ci.check_for_non_skippable_path(None))
@patch("subprocess.run")
def test_docs_only_change_returns_empty_list(self, mock_run):
args = {
"is_pull_request": True,
"base_ref": "HEAD^"
}
# Mock git diff to return only doc files
mock_process = MagicMock()
mock_process.stdout = "README.md\ndocs/guide.rst\nprojects/rocprim/docs/api.md"
mock_run.return_value = mock_process
project_to_run = therock_configure_ci.retrieve_projects(args)
self.assertEqual(len(project_to_run), 0)
if __name__ == "__main__":
unittest.main()
+45
Просмотреть файл
@@ -81,7 +81,52 @@ def check_for_workflow_file_related_to_ci(paths: Optional[Iterable[str]]) -> boo
return any(is_path_workflow_file_related_to_ci(p) for p in paths)
# Paths matching any of these patterns are considered to have no influence over
# build or test workflows so any related jobs can be skipped if all paths
# modified by a commit/PR match a pattern in this list.
SKIPPABLE_PATH_PATTERNS = [
"docs/*",
".gitignore",
"*.md",
"*.rst",
"projects/*/docs/*",
"projects/*/.gitignore",
"projects/*/*.md",
"projects/*/*.rst",
"shared/*/docs/*",
"shared/*/.gitignore",
"shared/*/*.md",
"shared/*/*.rst",
]
def is_path_skippable(path: str) -> bool:
"""Determines if a given relative path to a file matches any skippable patterns."""
return any(fnmatch.fnmatch(path, pattern) for pattern in SKIPPABLE_PATH_PATTERNS)
def check_for_non_skippable_path(paths: Optional[Iterable[str]]) -> bool:
"""Returns true if at least one path is not in the skippable set."""
if paths is None:
return False
return any(not is_path_skippable(p) for p in paths)
def retrieve_projects(args):
# Check if CI should be skipped based on modified paths
# (only for push and pull_request events, not workflow_dispatch or nightly)
if args.get("is_push") or args.get("is_pull_request"):
base_ref = args.get("base_ref")
modified_paths = get_modified_paths(base_ref)
paths_set = set(modified_paths)
contains_non_skippable_files = check_for_non_skippable_path(paths_set)
# If only skippable paths were modified, skip CI
if not contains_non_skippable_files:
logging.info("Only skippable paths were modified, skipping CI")
return []
if args.get("is_pull_request"):
subtrees = list(subtree_to_project_map.keys())