Add PR workflow commands (#742)

Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
Glenn Jocher
2026-04-17 13:46:53 +02:00
committed by GitHub
co-authored by UltralyticsAssistant
parent 000482b900
commit 1960672ebc
2 changed files with 88 additions and 30 deletions
+27 -14
View File
@@ -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)
+61 -16
View File
@@ -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():