Skip to content
Closed
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
47 changes: 47 additions & 0 deletions build_tools/github_actions/configure_external_repo_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
TypeVar,
)

from github_actions_api import gha_load_github_event

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -194,6 +196,47 @@ def get_modified_paths_api(
return {f["filename"] for f in files}


@retry(
max_attempts=3,
delay_seconds=2,
exceptions=(subprocess.TimeoutExpired, subprocess.CalledProcessError),
)
def get_pr_modified_paths_api(github_repo: str, pr_number: int) -> Optional[Set[str]]:
"""Get paths of files changed in a PR using GitHub API (PR files endpoint).

Returns None if the result is truncated (3000+ files).
"""
result = subprocess.run(
[
"gh",
"api",
"--paginate",
f"repos/{github_repo}/pulls/{pr_number}/files?per_page=100",
"--jq",
".[].filename",
],
capture_output=True,
text=True,
check=True,
timeout=120,
)
files = result.stdout.splitlines()
# GitHub PR files API returns max 3000 files
if len(files) >= 3000:
logger.warning("PR files API returned 3000+ files, result may be truncated")
return None
return set(files)


def get_event_pr_number() -> Optional[int]:
"""Return the PR number from the GitHub Actions event payload, if any."""
try:
event = gha_load_github_event()
except KeyError:
return None
return event.get("pull_request", {}).get("number")


def matches_patterns(paths: Iterable[str], patterns: Iterable[str]) -> bool:
"""Check if any path matches any pattern."""
for path in paths:
Expand Down Expand Up @@ -329,6 +372,10 @@ def configure(
if event_name == "pull_request" and base_sha and head_sha:
logger.info(f"Getting PR diff via API: {base_sha}...{head_sha}")
modified_paths = get_modified_paths_api(github_repo, base_sha, head_sha)
pr_number = get_event_pr_number() if modified_paths is None else None
if pr_number:
logger.info(f"Getting PR #{pr_number} files via API")
modified_paths = get_pr_modified_paths_api(github_repo, pr_number)
elif event_name == "push" and base_sha and head_sha:
# For push, the caller passes github.event.before as base_sha;
# the compare API does not understand git "^" ancestry syntax.
Expand Down
25 changes: 24 additions & 1 deletion build_tools/tests/configure_external_repo_ci_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,9 @@ def test_project_change_returns_changed_projects(self, mock_config, mock_api):
self.assertEqual(result.changed_projects, "projects/rocblas")
self.assertEqual(result.run_all_tests, False)

@patch("configure_external_repo_ci.get_event_pr_number", return_value=None)
@patch("configure_external_repo_ci.get_modified_paths_api")
def test_truncated_api_response_runs_all_tests(self, mock_api):
def test_truncated_api_response_runs_all_tests(self, mock_api, _mock_pr_number):
mock_api.return_value = None # Signals truncated response
result = configure(
event_name="pull_request",
Expand All @@ -284,6 +285,28 @@ def test_truncated_api_response_runs_all_tests(self, mock_api):
)
self.assertEqual(result.run_all_tests, True)

@patch("configure_external_repo_ci.get_pr_modified_paths_api")
@patch("configure_external_repo_ci.get_event_pr_number", return_value=123)
@patch("configure_external_repo_ci.get_modified_paths_api", return_value=None)
Comment on lines +288 to +290
@patch("configure_external_repo_ci.load_repo_config")
def test_truncated_compare_falls_back_to_pr_files(
self, mock_config, _mock_api, _mock_pr_number, mock_pr_files
):
mock_pr_files.return_value = {"projects/rocblas/src/main.cpp"}
mock_config.return_value = [
RepoEntry(name="rocblas", url="", branch="", category="projects"),
]
result = configure(
event_name="pull_request",
github_repo="ROCm/rocm-libraries",
base_sha="abc123",
head_sha="def456",
config_path=".github/repos-config.json",
)
mock_pr_files.assert_called_once_with("ROCm/rocm-libraries", 123)
self.assertEqual(result.changed_projects, "projects/rocblas")
self.assertEqual(result.run_all_tests, False)

def test_no_shas_provided_runs_all_tests(self):
result = configure(
event_name="pull_request",
Expand Down
Loading