diff --git a/actions/dispatch_actions.py b/actions/dispatch_actions.py index 96821c8..f527b5d 100644 --- a/actions/dispatch_actions.py +++ b/actions/dispatch_actions.py @@ -9,8 +9,14 @@ from datetime import datetime from .utils import ACTIONS_CREDIT, GITHUB_API_URL, Action # Configuration -RUN_CI_KEYWORD = "@ultralytics/run-ci" # and then to merge "@ultralytics/run-ci-and-merge" -WORKFLOW_FILES = ["ci.yml", "docker.yml"] +RUN_CI_KEYWORD = "@ultralytics/run-ci" +RUN_ALL_KEYWORD = "@ultralytics/run-all" +RUN_DOCKER_KEYWORD = "@ultralytics/run-docker" +WORKFLOW_FILES = { + RUN_CI_KEYWORD: ["ci.yml"], + RUN_ALL_KEYWORD: ["ci.yml", "docker.yml"], + RUN_DOCKER_KEYWORD: ["docker.yml"], +} def get_pr_branch(event) -> tuple[str, str | None]: @@ -66,21 +72,23 @@ def get_pr_branch(event) -> tuple[str, str | None]: return head.get("ref", "main"), None -def trigger_and_get_workflow_info(event, branch: str, temp_branch: str | None = None) -> list[dict]: +def trigger_and_get_workflow_info( + event, branch: str, workflow_files: list[str], temp_branch: str | None = None +) -> list[dict]: """Triggers workflows and returns their information, deleting temp branch if provided.""" repo = event.repository results = [] try: # Trigger all workflows - for file in WORKFLOW_FILES: + for file in workflow_files: event.post(f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/dispatches", json={"ref": branch}) # Wait for workflows to be created and start time.sleep(60) # Collect information about all workflows - for file in WORKFLOW_FILES: + for file in workflow_files: # Get workflow name response = event.get(f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}") name = file.replace(".yml", "").title() @@ -108,7 +116,7 @@ def trigger_and_get_workflow_info(event, branch: str, temp_branch: str | None = return results -def update_comment(event, comment_body: str, triggered_actions: list[dict]): +def update_comment(event, comment_body: str, command: str, triggered_actions: list[dict]): """Updates the comment with workflow information.""" if not triggered_actions: return @@ -120,7 +128,8 @@ def update_comment(event, comment_body: str, triggered_actions: list[dict]): {ACTIONS_CREDIT} -GitHub Actions below triggered via workflow dispatch for this PR at {timestamp} with `{RUN_CI_KEYWORD}` command: +GitHub Actions below triggered via workflow dispatch for this PR at {timestamp} with `{command}` command +(available commands are `{RUN_ALL_KEYWORD}`, `{RUN_CI_KEYWORD}`, and `{RUN_DOCKER_KEYWORD}`): """ @@ -128,7 +137,7 @@ GitHub Actions below triggered via workflow dispatch for this PR at {timestamp} run_info = f" run {action['run_number']}" if action["run_number"] else "" summary += f"* ✅ [{action['name']}]({action['url']}): `{action['file']}`{run_info}\n" - new_body = comment_body.replace(RUN_CI_KEYWORD, summary).strip() + new_body = comment_body.replace(command, summary).strip() comment_id = event.event_data["comment"]["id"] event.patch(f"{GITHUB_API_URL}/repos/{event.repository}/issues/comments/{comment_id}", json={"body": new_body}) @@ -149,11 +158,15 @@ def main(*args, **kwargs): comment_body = event.event_data["comment"].get("body") or "" username = event.event_data["comment"]["user"]["login"] - # Check for keyword without surrounding backticks to avoid triggering on replies - has_keyword = RUN_CI_KEYWORD in comment_body and comment_body.count(RUN_CI_KEYWORD) > comment_body.count( - f"`{RUN_CI_KEYWORD}`" + command = next( + ( + keyword + for keyword in WORKFLOW_FILES + if keyword in comment_body and comment_body.count(keyword) > comment_body.count(f"`{keyword}`") + ), + None, ) - if not has_keyword or not event.is_org_member(username): + if not command or not event.is_org_member(username): return # Get branch, trigger workflows, and update comment @@ -161,8 +174,8 @@ def main(*args, **kwargs): branch, temp_branch = get_pr_branch(event) print(f"Triggering workflows on branch: {branch}" + (" (temp)" if temp_branch else "")) - triggered_actions = trigger_and_get_workflow_info(event, branch, temp_branch) - update_comment(event, comment_body, triggered_actions) + triggered_actions = trigger_and_get_workflow_info(event, branch, WORKFLOW_FILES[command], temp_branch) + update_comment(event, comment_body, command, triggered_actions) event.toggle_eyes_reaction(False) diff --git a/tests/test_dispatch_actions.py b/tests/test_dispatch_actions.py index 86f069c..eef0333 100644 --- a/tests/test_dispatch_actions.py +++ b/tests/test_dispatch_actions.py @@ -4,7 +4,9 @@ from datetime import datetime from unittest.mock import MagicMock, patch from actions.dispatch_actions import ( + RUN_ALL_KEYWORD, RUN_CI_KEYWORD, + RUN_DOCKER_KEYWORD, get_pr_branch, main, trigger_and_get_workflow_info, @@ -79,8 +81,8 @@ def test_trigger_and_get_workflow_info(): mock_event.get.side_effect = get_side_effect # Use patch to skip time.sleep and limit to one workflow - with patch("time.sleep"), patch("actions.dispatch_actions.WORKFLOW_FILES", ["ci.yml"]): - results = trigger_and_get_workflow_info(mock_event, "feature-branch") + with patch("time.sleep"): + results = trigger_and_get_workflow_info(mock_event, "feature-branch", ["ci.yml"]) # Check results assert len(results) == 1 @@ -113,8 +115,7 @@ def test_trigger_and_get_workflow_info_with_temp_branch(): mock_event.get.side_effect = get_side_effect with patch("time.sleep"): - with patch("actions.dispatch_actions.WORKFLOW_FILES", ["ci.yml"]): - trigger_and_get_workflow_info(mock_event, "temp-ci-123-456", temp_branch="temp-ci-123-456") + trigger_and_get_workflow_info(mock_event, "temp-ci-123-456", ["ci.yml"], temp_branch="temp-ci-123-456") # Verify temp branch was deleted mock_event.delete.assert_called_once_with("https://api.github.com/repos/test/repo/git/refs/heads/temp-ci-123-456") @@ -140,7 +141,7 @@ def test_update_comment_function(): with patch("actions.dispatch_actions.datetime") as mock_datetime: mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0) # Call without capturing return value - update_comment(mock_event, comment_body, triggered_actions) + update_comment(mock_event, comment_body, RUN_CI_KEYWORD, triggered_actions) # Check that patch was called with expected content mock_event.patch.assert_called_once() @@ -151,12 +152,15 @@ def test_update_comment_function(): assert "Actions Trigger" in kwargs["json"]["body"] assert "CI Workflow" in kwargs["json"]["body"] assert "2023-01-01 12:00:00 UTC" in kwargs["json"]["body"] + assert ( + f"(available commands are `{RUN_ALL_KEYWORD}`, `{RUN_CI_KEYWORD}`, and `{RUN_DOCKER_KEYWORD}`):" + in kwargs["json"]["body"] + ) -def test_main_triggers_workflows(): - """Test main function when comment contains trigger keyword.""" +def test_main_triggers_ci_only(): + """Test main function triggers only CI for the CI command.""" with patch("actions.dispatch_actions.Action") as MockAction: - # Configure mock mock_event = MockAction.return_value mock_event.event_name = "issue_comment" mock_event.repository = "test/repo" @@ -167,21 +171,62 @@ def test_main_triggers_workflows(): } mock_event.is_org_member.return_value = True - # Create minimal patches for the functions called by main with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: with patch("actions.dispatch_actions.update_comment"): - # Set return values mock_get_branch.return_value = ("feature-branch", None) mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] - - # Call the function main() - # Verify main component calls were made - mock_event.is_org_member.assert_called_once_with("testuser") - mock_get_branch.assert_called_once() - mock_trigger.assert_called_once() + mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["ci.yml"], None) + + +def test_main_triggers_all_workflows(): + """Test main function triggers CI and Docker for the all command.""" + with patch("actions.dispatch_actions.Action") as MockAction: + mock_event = MockAction.return_value + mock_event.event_name = "issue_comment" + mock_event.repository = "test/repo" + mock_event.event_data = { + "action": "created", + "issue": {"pull_request": {}}, + "comment": {"body": f"Please run all {RUN_ALL_KEYWORD}", "user": {"login": "testuser"}, "id": 789}, + } + mock_event.is_org_member.return_value = True + + with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: + with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: + with patch("actions.dispatch_actions.update_comment"): + mock_get_branch.return_value = ("feature-branch", None) + mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] + main() + + mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["ci.yml", "docker.yml"], None) + + +def test_main_triggers_docker_only(): + """Test main function triggers only docker workflow for docker command.""" + with patch("actions.dispatch_actions.Action") as MockAction: + mock_event = MockAction.return_value + mock_event.event_name = "issue_comment" + mock_event.repository = "test/repo" + mock_event.event_data = { + "action": "created", + "issue": {"pull_request": {}}, + "comment": {"body": f"Please run Docker {RUN_DOCKER_KEYWORD}", "user": {"login": "testuser"}, "id": 789}, + } + mock_event.is_org_member.return_value = True + + with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: + with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: + with patch("actions.dispatch_actions.update_comment"): + mock_get_branch.return_value = ("feature-branch", None) + mock_trigger.return_value = [ + {"name": "Docker", "file": "docker.yml", "url": "url", "run_number": 1} + ] + main() + + mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["docker.yml"], None) def test_main_skips_non_pr_comments():