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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4870725a62
Коммит
2073cf2172
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user