ultralytics-actions 0.0.99__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ultralytics-actions might be problematic. Click here for more details.
- {ultralytics_actions-0.0.99/ultralytics_actions.egg-info → ultralytics_actions-0.1.0}/PKG-INFO +3 -2
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/README.md +2 -1
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/__init__.py +2 -1
- ultralytics_actions-0.1.0/actions/review_pr.py +247 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/update_markdown_code_blocks.py +4 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/utils/github_utils.py +44 -56
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/utils/openai_utils.py +3 -1
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/pyproject.toml +1 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0/ultralytics_actions.egg-info}/PKG-INFO +3 -2
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/SOURCES.txt +1 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/entry_points.txt +1 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/LICENSE +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/dispatch_actions.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/first_interaction.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/summarize_pr.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/summarize_release.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/update_file_headers.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/utils/__init__.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/utils/common_utils.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/utils/version_utils.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/setup.cfg +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_cli_commands.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_common_utils.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_dispatch_actions.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_file_headers.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_first_interaction.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_github_utils.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_init.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_openai_utils.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_summarize_pr.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_summarize_release.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_update_markdown_codeblocks.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_urls.py +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/requires.txt +0 -0
- {ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/top_level.txt +0 -0
{ultralytics_actions-0.0.99/ultralytics_actions.egg-info → ultralytics_actions-0.1.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ultralytics-actions
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Ultralytics Actions for GitHub automation and PR management.
|
|
5
5
|
Author-email: Glenn Jocher <glenn.jocher@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
<a href="https://www.ultralytics.com/"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
|
|
40
40
|
|
|
41
|
-
# 🚀 Ultralytics Actions:
|
|
41
|
+
# 🚀 Ultralytics Actions: AI-powered formatting, labeling & PR summaries for Python and Markdown
|
|
42
42
|
|
|
43
43
|
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
|
44
44
|
|
|
@@ -46,6 +46,7 @@ Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) rep
|
|
|
46
46
|
|
|
47
47
|
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
|
48
48
|
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
|
49
|
+
[](https://github.com/ultralytics/actions/actions/workflows/open-prs.yml)
|
|
49
50
|
[](https://codecov.io/github/ultralytics/actions)
|
|
50
51
|
|
|
51
52
|
[](https://discord.com/invite/ultralytics)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<a href="https://www.ultralytics.com/"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
|
|
2
2
|
|
|
3
|
-
# 🚀 Ultralytics Actions:
|
|
3
|
+
# 🚀 Ultralytics Actions: AI-powered formatting, labeling & PR summaries for Python and Markdown
|
|
4
4
|
|
|
5
5
|
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) rep
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
|
11
|
+
[](https://github.com/ultralytics/actions/actions/workflows/open-prs.yml)
|
|
11
12
|
[](https://codecov.io/github/ultralytics/actions)
|
|
12
13
|
|
|
13
14
|
[](https://discord.com/invite/ultralytics)
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# │ │ ├── openai_utils.py
|
|
14
14
|
# │ │ └── common_utils.py
|
|
15
15
|
# │ ├── first_interaction.py
|
|
16
|
+
# │ ├── review_pr.py
|
|
16
17
|
# │ ├── summarize_pr.py
|
|
17
18
|
# │ ├── summarize_release.py
|
|
18
19
|
# │ └── update_markdown_code_blocks.py
|
|
@@ -22,4 +23,4 @@
|
|
|
22
23
|
# ├── test_summarize_pr.py
|
|
23
24
|
# └── ...
|
|
24
25
|
|
|
25
|
-
__version__ = "0.0
|
|
26
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .utils import GITHUB_API_URL, Action, get_completion
|
|
9
|
+
|
|
10
|
+
REVIEW_MARKER = "🔍 PR Review"
|
|
11
|
+
EMOJI_MAP = {"CRITICAL": "❗", "HIGH": "⚠️", "MEDIUM": "💡", "LOW": "📝", "SUGGESTION": "💭"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_diff_files(diff_text: str) -> dict:
|
|
15
|
+
"""Parse diff to extract file paths, valid line numbers, and line content for comments."""
|
|
16
|
+
files, current_file, current_line = {}, None, 0
|
|
17
|
+
|
|
18
|
+
for line in diff_text.split("\n"):
|
|
19
|
+
if line.startswith("diff --git"):
|
|
20
|
+
match = re.search(r" b/(.+)$", line)
|
|
21
|
+
current_file = match.group(1) if match else None
|
|
22
|
+
current_line = 0
|
|
23
|
+
if current_file:
|
|
24
|
+
files[current_file] = {}
|
|
25
|
+
elif line.startswith("@@") and current_file:
|
|
26
|
+
match = re.search(r"@@.*\+(\d+)(?:,\d+)?", line)
|
|
27
|
+
current_line = int(match.group(1)) if match else 0
|
|
28
|
+
elif current_file and current_line > 0:
|
|
29
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
30
|
+
files[current_file][current_line] = line[1:]
|
|
31
|
+
current_line += 1
|
|
32
|
+
elif not line.startswith("-"):
|
|
33
|
+
current_line += 1
|
|
34
|
+
|
|
35
|
+
return files
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_description: str) -> dict:
|
|
39
|
+
"""Generate comprehensive PR review with line-specific comments and overall assessment."""
|
|
40
|
+
if not diff_text or "**ERROR" in diff_text:
|
|
41
|
+
return {"comments": [], "summary": f"Unable to review: {diff_text if '**ERROR' in diff_text else 'diff empty'}"}
|
|
42
|
+
|
|
43
|
+
diff_files = parse_diff_files(diff_text)
|
|
44
|
+
if not diff_files:
|
|
45
|
+
return {"comments": [], "summary": "No files with changes detected in diff"}
|
|
46
|
+
|
|
47
|
+
file_list = list(diff_files.keys())
|
|
48
|
+
limit = round(128000 * 3.3 * 0.4)
|
|
49
|
+
diff_truncated = len(diff_text) > limit
|
|
50
|
+
lines_changed = sum(len(lines) for lines in diff_files.values())
|
|
51
|
+
|
|
52
|
+
valid_lines_text = "\n".join(
|
|
53
|
+
f" {file}: {sorted(list(lines.keys())[:20])}{' ...' if len(lines) > 20 else ''}"
|
|
54
|
+
for file, lines in list(diff_files.items())[:10]
|
|
55
|
+
) + ("\n ..." if len(diff_files) > 10 else "")
|
|
56
|
+
|
|
57
|
+
priority_guidance = (
|
|
58
|
+
"Prioritize the most critical/high-impact issues only"
|
|
59
|
+
if lines_changed >= 100
|
|
60
|
+
else "Prioritize commenting on different files/sections"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
content = (
|
|
64
|
+
"You are an expert code reviewer for Ultralytics. Provide detailed inline comments on specific code changes.\n\n"
|
|
65
|
+
"Focus on: Code quality, style, best practices, bugs, edge cases, error handling, performance, security, documentation, test coverage\n\n"
|
|
66
|
+
"FORMATTING: Use backticks for code, file names, branch names, function names, variable names, packages\n\n"
|
|
67
|
+
"CRITICAL RULES:\n"
|
|
68
|
+
f"1. Generate inline comments with recommended changes for clear bugs/security/syntax issues (up to 10)\n"
|
|
69
|
+
"2. Each comment MUST reference a UNIQUE line number(s)\n"
|
|
70
|
+
"3. If a section has multiple issues, combine ALL issues into ONE comment for that section\n"
|
|
71
|
+
"4. Never create separate comments for the same line number(s)\n"
|
|
72
|
+
f"5. {priority_guidance}\n\n"
|
|
73
|
+
"CODE SUGGESTIONS:\n"
|
|
74
|
+
"- ONLY provide 'suggestion' field when you have high certainty the code is problematic AND sufficient context for a confident fix\n"
|
|
75
|
+
"- If uncertain about the correct fix, omit 'suggestion' field and explain the concern in 'message' only\n"
|
|
76
|
+
"- Suggestions must be ready-to-merge code with NO comments, placeholders, or explanations\n"
|
|
77
|
+
"- When providing suggestions, ensure they are complete, correct, and maintain existing indentation\n"
|
|
78
|
+
"- It's better to flag an issue without a suggestion than provide a wrong or uncertain fix\n\n"
|
|
79
|
+
"Return JSON: "
|
|
80
|
+
'{"comments": [{"file": "exact/path", "line": N, "severity": "HIGH", "message": "...", "suggestion": "..."}], "summary": "..."}\n\n'
|
|
81
|
+
"Rules:\n"
|
|
82
|
+
"- Only comment on NEW lines (starting with + in diff)\n"
|
|
83
|
+
"- Use exact file paths from diff (no ./ prefix)\n"
|
|
84
|
+
"- Line numbers must match NEW file line numbers from @@ hunks\n"
|
|
85
|
+
"- When '- old' then '+ new', new line keeps SAME line number\n"
|
|
86
|
+
"- Severity: CRITICAL, HIGH, MEDIUM, LOW, SUGGESTION\n"
|
|
87
|
+
f"- Files changed: {len(file_list)} ({', '.join(file_list[:10])}{'...' if len(file_list) > 10 else ''})\n"
|
|
88
|
+
f"- Total changed lines: {lines_changed}\n"
|
|
89
|
+
f"- Diff {'truncated' if diff_truncated else 'complete'}: {len(diff_text[:limit])} chars{f' of {len(diff_text)}' if diff_truncated else ''}\n\n"
|
|
90
|
+
f"VALID LINE NUMBERS (use ONLY these):\n{valid_lines_text}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
messages = [
|
|
94
|
+
{"role": "system", "content": content},
|
|
95
|
+
{
|
|
96
|
+
"role": "user",
|
|
97
|
+
"content": f"Review PR '{repository}':\nTitle: {pr_title}\nDescription: {pr_description[:500]}\n\nDiff:\n{diff_text[:limit]}",
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
response = get_completion(messages, reasoning_effort="medium")
|
|
103
|
+
print("\n" + "=" * 80 + f"\nFULL AI RESPONSE:\n{response}\n" + "=" * 80 + "\n")
|
|
104
|
+
|
|
105
|
+
json_str = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", response, re.DOTALL)
|
|
106
|
+
review_data = json.loads(json_str.group(1) if json_str else response)
|
|
107
|
+
|
|
108
|
+
print(f"AI generated {len(review_data.get('comments', []))} comments")
|
|
109
|
+
|
|
110
|
+
# Validate, filter, and deduplicate comments
|
|
111
|
+
unique_comments = {}
|
|
112
|
+
for c in review_data.get("comments", []):
|
|
113
|
+
file_path, line_num = c.get("file"), c.get("line", 0)
|
|
114
|
+
if file_path in diff_files and line_num in diff_files[file_path]:
|
|
115
|
+
key = f"{file_path}:{line_num}"
|
|
116
|
+
if key not in unique_comments:
|
|
117
|
+
unique_comments[key] = c
|
|
118
|
+
else:
|
|
119
|
+
print(f"⚠️ AI duplicate for {key}: {c.get('severity')} - {c.get('message')[:60]}...")
|
|
120
|
+
else:
|
|
121
|
+
print(f"Filtered out {file_path}:{line_num} (available: {list(diff_files.get(file_path, {}))[:10]}...)")
|
|
122
|
+
|
|
123
|
+
review_data.update(
|
|
124
|
+
{"comments": list(unique_comments.values()), "diff_files": diff_files, "diff_truncated": diff_truncated}
|
|
125
|
+
)
|
|
126
|
+
print(f"Valid comments after filtering: {len(review_data['comments'])}")
|
|
127
|
+
return review_data
|
|
128
|
+
|
|
129
|
+
except json.JSONDecodeError as e:
|
|
130
|
+
print(f"JSON parsing failed: {e}\nAttempted: {json_str[:500] if 'json_str' in locals() else response[:500]}...")
|
|
131
|
+
return {"comments": [], "summary": "Review generation encountered a JSON parsing error"}
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Review generation failed: {e}")
|
|
134
|
+
import traceback
|
|
135
|
+
|
|
136
|
+
traceback.print_exc()
|
|
137
|
+
return {"comments": [], "summary": "Review generation encountered an error"}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def dismiss_previous_reviews(event: Action) -> int:
|
|
141
|
+
"""Dismiss previous bot reviews and delete inline comments, returns count for numbering."""
|
|
142
|
+
if not (pr_number := event.pr.get("number")) or not (bot_username := event.get_username()):
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
review_count = 0
|
|
146
|
+
reviews_url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}/reviews"
|
|
147
|
+
if (response := event.get(reviews_url)).status_code == 200:
|
|
148
|
+
for review in response.json():
|
|
149
|
+
if review.get("user", {}).get("login") == bot_username and REVIEW_MARKER in (review.get("body") or ""):
|
|
150
|
+
review_count += 1
|
|
151
|
+
if review.get("state") in ["APPROVED", "CHANGES_REQUESTED"] and (review_id := review.get("id")):
|
|
152
|
+
event.put(f"{reviews_url}/{review_id}/dismissals", json={"message": "Superseded by new review"})
|
|
153
|
+
|
|
154
|
+
# Delete previous inline comments
|
|
155
|
+
comments_url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}/comments"
|
|
156
|
+
if (response := event.get(comments_url)).status_code == 200:
|
|
157
|
+
for comment in response.json():
|
|
158
|
+
if comment.get("user", {}).get("login") == bot_username and (comment_id := comment.get("id")):
|
|
159
|
+
event.delete(
|
|
160
|
+
f"{GITHUB_API_URL}/repos/{event.repository}/pulls/comments/{comment_id}",
|
|
161
|
+
expected_status=[200, 204, 404],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return review_count + 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def post_review_comments(event: Action, review_data: dict) -> None:
|
|
168
|
+
"""Post inline review comments on specific lines of the PR."""
|
|
169
|
+
if not (pr_number := event.pr.get("number")) or not (commit_sha := event.pr.get("head", {}).get("sha")):
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}/comments"
|
|
173
|
+
diff_files = review_data.get("diff_files", {})
|
|
174
|
+
|
|
175
|
+
for comment in review_data.get("comments", [])[:50]:
|
|
176
|
+
if not (file_path := comment.get("file")) or not (line := comment.get("line", 0)):
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
severity = comment.get("severity", "SUGGESTION")
|
|
180
|
+
body = f"{EMOJI_MAP.get(severity, '💭')} **{severity}**: {comment.get('message', '')}"
|
|
181
|
+
|
|
182
|
+
if suggestion := comment.get("suggestion"):
|
|
183
|
+
original_line = diff_files.get(file_path, {}).get(line, "")
|
|
184
|
+
indent = len(original_line) - len(original_line.lstrip())
|
|
185
|
+
indented = "\n".join(" " * indent + l if l.strip() else l for l in suggestion.split("\n"))
|
|
186
|
+
body += f"\n\n**Suggested change:**\n```suggestion\n{indented}\n```"
|
|
187
|
+
|
|
188
|
+
event.post(url, json={"body": body, "commit_id": commit_sha, "path": file_path, "line": line, "side": "RIGHT"})
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def post_review_summary(event: Action, review_data: dict, review_number: int) -> None:
|
|
192
|
+
"""Post overall review summary as a PR review."""
|
|
193
|
+
if not (pr_number := event.pr.get("number")) or not (commit_sha := event.pr.get("head", {}).get("sha")):
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
review_title = f"{REVIEW_MARKER} {review_number}" if review_number > 1 else REVIEW_MARKER
|
|
197
|
+
comments = review_data.get("comments", [])
|
|
198
|
+
max_severity = max((c.get("severity") for c in comments), default="SUGGESTION") if comments else None
|
|
199
|
+
event_type = "APPROVE" if not comments or max_severity in ["LOW", "SUGGESTION"] else "REQUEST_CHANGES"
|
|
200
|
+
|
|
201
|
+
body = (
|
|
202
|
+
f"## {review_title}\n\n"
|
|
203
|
+
"<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)</sub>\n\n"
|
|
204
|
+
f"{review_data.get('summary', 'Review completed')}\n\n"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if comments:
|
|
208
|
+
shown = min(len(comments), 50)
|
|
209
|
+
body += f"💬 Posted {shown} inline comment{'s' if shown != 1 else ''}{' (50 shown, more available)' if len(comments) > 50 else ''}\n"
|
|
210
|
+
|
|
211
|
+
if review_data.get("diff_truncated"):
|
|
212
|
+
body += "\n⚠️ **Large PR**: Review focused on critical issues. Some details may not be covered.\n"
|
|
213
|
+
|
|
214
|
+
event.post(
|
|
215
|
+
f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}/reviews",
|
|
216
|
+
json={"commit_id": commit_sha, "body": body, "event": event_type},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main(*args, **kwargs):
|
|
221
|
+
"""Main entry point for PR review action."""
|
|
222
|
+
event = Action(*args, **kwargs)
|
|
223
|
+
|
|
224
|
+
# Handle review requests
|
|
225
|
+
if event.event_name == "pull_request" and event.event_data.get("action") == "review_requested":
|
|
226
|
+
if event.event_data.get("requested_reviewer", {}).get("login") != event.get_username():
|
|
227
|
+
return
|
|
228
|
+
print(f"Review requested from {event.get_username()}")
|
|
229
|
+
|
|
230
|
+
if not event.pr or event.pr.get("state") != "open":
|
|
231
|
+
print(f"Skipping: PR state is {event.pr.get('state') if event.pr else 'None'}")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
print(f"Starting PR review for #{event.pr['number']}")
|
|
235
|
+
review_number = dismiss_previous_reviews(event)
|
|
236
|
+
|
|
237
|
+
diff = event.get_pr_diff()
|
|
238
|
+
review = generate_pr_review(event.repository, diff, event.pr.get("title", ""), event.pr.get("body", ""))
|
|
239
|
+
|
|
240
|
+
post_review_summary(event, review, review_number)
|
|
241
|
+
print(f"Posting {len(review.get('comments', []))} inline comments")
|
|
242
|
+
post_review_comments(event, review)
|
|
243
|
+
print("PR review completed")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
main()
|
{ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/actions/update_markdown_code_blocks.py
RENAMED
|
@@ -39,6 +39,10 @@ def add_indentation(code_block, num_spaces):
|
|
|
39
39
|
|
|
40
40
|
def format_code_with_ruff(temp_dir):
|
|
41
41
|
"""Formats Python code files in the specified directory using ruff linter and docformatter tools."""
|
|
42
|
+
if not next(Path(temp_dir).rglob("*.py"), None):
|
|
43
|
+
print("No Python code blocks found to format")
|
|
44
|
+
return
|
|
45
|
+
|
|
42
46
|
try:
|
|
43
47
|
# Run ruff format
|
|
44
48
|
subprocess.run(
|
|
@@ -17,17 +17,19 @@ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
|
|
|
17
17
|
class Action:
|
|
18
18
|
"""Handles GitHub Actions API interactions and event processing."""
|
|
19
19
|
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
token: str = None,
|
|
23
|
-
event_name: str = None,
|
|
24
|
-
event_data: dict = None,
|
|
25
|
-
verbose: bool = True,
|
|
26
|
-
):
|
|
20
|
+
def __init__(self, token: str = None, event_name: str = None, event_data: dict = None, verbose: bool = True):
|
|
27
21
|
"""Initializes a GitHub Actions API handler with token and event data for processing events."""
|
|
28
22
|
self.token = token or os.getenv("GITHUB_TOKEN")
|
|
29
23
|
self.event_name = event_name or os.getenv("GITHUB_EVENT_NAME")
|
|
30
24
|
self.event_data = event_data or self._load_event_data(os.getenv("GITHUB_EVENT_PATH"))
|
|
25
|
+
self.pr = self.event_data.get("pull_request", {})
|
|
26
|
+
self.repository = self.event_data.get("repository", {}).get("full_name")
|
|
27
|
+
self.headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github+json"}
|
|
28
|
+
self.headers_diff = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github.v3.diff"}
|
|
29
|
+
self.verbose = verbose
|
|
30
|
+
self.eyes_reaction_id = None
|
|
31
|
+
self._pr_diff_cache = None
|
|
32
|
+
self._username_cache = None
|
|
31
33
|
self._default_status = {
|
|
32
34
|
"get": [200],
|
|
33
35
|
"post": [200, 201],
|
|
@@ -36,34 +38,22 @@ class Action:
|
|
|
36
38
|
"delete": [200, 204],
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
self.pr = self.event_data.get("pull_request", {})
|
|
40
|
-
self.repository = self.event_data.get("repository", {}).get("full_name")
|
|
41
|
-
self.headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github+json"}
|
|
42
|
-
self.headers_diff = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github.v3.diff"}
|
|
43
|
-
self.eyes_reaction_id = None
|
|
44
|
-
self.verbose = verbose
|
|
45
|
-
|
|
46
41
|
def _request(self, method: str, url: str, headers=None, expected_status=None, hard=False, **kwargs):
|
|
47
42
|
"""Unified request handler with error checking."""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
response = getattr(requests, method)(url, headers=headers, **kwargs)
|
|
52
|
-
status = response.status_code
|
|
53
|
-
success = status in expected_status
|
|
43
|
+
response = getattr(requests, method)(url, headers=headers or self.headers, **kwargs)
|
|
44
|
+
expected = expected_status or self._default_status[method]
|
|
45
|
+
success = response.status_code in expected
|
|
54
46
|
|
|
55
47
|
if self.verbose:
|
|
56
|
-
print(f"{'✓' if success else '✗'} {method.upper()} {url} → {
|
|
48
|
+
print(f"{'✓' if success else '✗'} {method.upper()} {url} → {response.status_code}")
|
|
57
49
|
if not success:
|
|
58
50
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
print(f" ❌ Error: {response.text[:100]}... {e}")
|
|
51
|
+
print(f" ❌ Error: {response.json().get('message', 'Unknown error')}")
|
|
52
|
+
except Exception:
|
|
53
|
+
print(f" ❌ Error: {response.text[:200]}")
|
|
63
54
|
|
|
64
55
|
if not success and hard:
|
|
65
56
|
response.raise_for_status()
|
|
66
|
-
|
|
67
57
|
return response
|
|
68
58
|
|
|
69
59
|
def get(self, url, **kwargs):
|
|
@@ -89,45 +79,47 @@ class Action:
|
|
|
89
79
|
@staticmethod
|
|
90
80
|
def _load_event_data(event_path: str) -> dict:
|
|
91
81
|
"""Load GitHub event data from path if it exists."""
|
|
92
|
-
if event_path and Path(event_path).exists()
|
|
93
|
-
return json.loads(Path(event_path).read_text())
|
|
94
|
-
return {}
|
|
82
|
+
return json.loads(Path(event_path).read_text()) if event_path and Path(event_path).exists() else {}
|
|
95
83
|
|
|
96
84
|
def is_repo_private(self) -> bool:
|
|
97
|
-
"""Checks if the repository is public using event data
|
|
98
|
-
return self.event_data.get("repository", {}).get("private")
|
|
85
|
+
"""Checks if the repository is public using event data."""
|
|
86
|
+
return self.event_data.get("repository", {}).get("private", False)
|
|
99
87
|
|
|
100
88
|
def get_username(self) -> str | None:
|
|
101
|
-
"""Gets username associated with the GitHub token."""
|
|
89
|
+
"""Gets username associated with the GitHub token with caching."""
|
|
90
|
+
if self._username_cache:
|
|
91
|
+
return self._username_cache
|
|
92
|
+
|
|
102
93
|
response = self.post(GITHUB_GRAPHQL_URL, json={"query": "query { viewer { login } }"})
|
|
103
94
|
if response.status_code == 200:
|
|
104
95
|
try:
|
|
105
|
-
|
|
96
|
+
self._username_cache = response.json()["data"]["viewer"]["login"]
|
|
106
97
|
except KeyError as e:
|
|
107
98
|
print(f"Error parsing authenticated user response: {e}")
|
|
108
|
-
return
|
|
99
|
+
return self._username_cache
|
|
109
100
|
|
|
110
101
|
def is_org_member(self, username: str) -> bool:
|
|
111
|
-
"""Checks if a user is a member of the organization
|
|
112
|
-
|
|
113
|
-
response = self.get(f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}")
|
|
114
|
-
return response.status_code == 204 # 204 means the user is a member
|
|
102
|
+
"""Checks if a user is a member of the organization."""
|
|
103
|
+
return self.get(f"{GITHUB_API_URL}/orgs/{self.repository.split('/')[0]}/members/{username}").status_code == 204
|
|
115
104
|
|
|
116
105
|
def get_pr_diff(self) -> str:
|
|
117
|
-
"""Retrieves the diff content for a specified pull request."""
|
|
106
|
+
"""Retrieves the diff content for a specified pull request with caching."""
|
|
107
|
+
if self._pr_diff_cache:
|
|
108
|
+
return self._pr_diff_cache
|
|
109
|
+
|
|
118
110
|
url = f"{GITHUB_API_URL}/repos/{self.repository}/pulls/{self.pr.get('number')}"
|
|
119
111
|
response = self.get(url, headers=self.headers_diff)
|
|
120
112
|
if response.status_code == 200:
|
|
121
|
-
|
|
113
|
+
self._pr_diff_cache = response.text
|
|
122
114
|
elif response.status_code == 406:
|
|
123
|
-
|
|
115
|
+
self._pr_diff_cache = "ERROR: PR diff exceeds GitHub's 20,000 line limit, unable to retrieve diff."
|
|
124
116
|
else:
|
|
125
|
-
|
|
117
|
+
self._pr_diff_cache = "ERROR: UNABLE TO RETRIEVE DIFF."
|
|
118
|
+
return self._pr_diff_cache
|
|
126
119
|
|
|
127
120
|
def get_repo_data(self, endpoint: str) -> dict:
|
|
128
121
|
"""Fetches repository data from a specified endpoint."""
|
|
129
|
-
|
|
130
|
-
return response.json()
|
|
122
|
+
return self.get(f"{GITHUB_API_URL}/repos/{self.repository}/{endpoint}").json()
|
|
131
123
|
|
|
132
124
|
def toggle_eyes_reaction(self, add: bool = True) -> None:
|
|
133
125
|
"""Adds or removes eyes emoji reaction."""
|
|
@@ -139,8 +131,8 @@ class Action:
|
|
|
139
131
|
id = self.event_data.get("issue", {}).get("number")
|
|
140
132
|
if not id:
|
|
141
133
|
return
|
|
142
|
-
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{id}/reactions"
|
|
143
134
|
|
|
135
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{id}/reactions"
|
|
144
136
|
if add:
|
|
145
137
|
response = self.post(url, json={"content": "eyes"})
|
|
146
138
|
if response.status_code == 201:
|
|
@@ -151,8 +143,7 @@ class Action:
|
|
|
151
143
|
|
|
152
144
|
def graphql_request(self, query: str, variables: dict = None) -> dict:
|
|
153
145
|
"""Executes a GraphQL query against the GitHub API."""
|
|
154
|
-
|
|
155
|
-
result = r.json()
|
|
146
|
+
result = self.post(GITHUB_GRAPHQL_URL, json={"query": query, "variables": variables}).json()
|
|
156
147
|
if "data" not in result or result.get("errors"):
|
|
157
148
|
print(result.get("errors"))
|
|
158
149
|
return result
|
|
@@ -166,11 +157,11 @@ class Action:
|
|
|
166
157
|
"github.repository.private": self.is_repo_private(),
|
|
167
158
|
"github.event.pull_request.number": self.pr.get("number"),
|
|
168
159
|
"github.event.pull_request.head.repo.full_name": self.pr.get("head", {}).get("repo", {}).get("full_name"),
|
|
169
|
-
"github.actor": os.
|
|
160
|
+
"github.actor": os.getenv("GITHUB_ACTOR"),
|
|
170
161
|
"github.event.pull_request.head.ref": self.pr.get("head", {}).get("ref"),
|
|
171
|
-
"github.ref": os.
|
|
172
|
-
"github.head_ref": os.
|
|
173
|
-
"github.base_ref": os.
|
|
162
|
+
"github.ref": os.getenv("GITHUB_REF"),
|
|
163
|
+
"github.head_ref": os.getenv("GITHUB_HEAD_REF"),
|
|
164
|
+
"github.base_ref": os.getenv("GITHUB_BASE_REF"),
|
|
174
165
|
"github.base_sha": self.pr.get("base", {}).get("sha"),
|
|
175
166
|
}
|
|
176
167
|
|
|
@@ -181,12 +172,9 @@ class Action:
|
|
|
181
172
|
"github.event.discussion.number": discussion.get("number"),
|
|
182
173
|
}
|
|
183
174
|
|
|
184
|
-
|
|
175
|
+
width = max(len(k) for k in info) + 5
|
|
185
176
|
header = f"Ultralytics Actions {__version__} Information " + "-" * 40
|
|
186
|
-
print(header)
|
|
187
|
-
for key, value in info.items():
|
|
188
|
-
print(f"{key:<{max_key_length + 5}}{value}")
|
|
189
|
-
print("-" * len(header))
|
|
177
|
+
print(f"{header}\n" + "\n".join(f"{k:<{width}}{v}" for k, v in info.items()) + f"\n{'-' * len(header)}")
|
|
190
178
|
|
|
191
179
|
|
|
192
180
|
def ultralytics_actions_info():
|
|
@@ -9,7 +9,7 @@ import requests
|
|
|
9
9
|
from actions.utils.common_utils import check_links_in_string
|
|
10
10
|
|
|
11
11
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
12
|
-
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-5-
|
|
12
|
+
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-5-2025-08-07")
|
|
13
13
|
SYSTEM_PROMPT_ADDITION = """Guidance:
|
|
14
14
|
- Ultralytics Branding: Use YOLO11, YOLO26, etc., not YOLOv11, YOLOv26 (only older versions like YOLOv10 have a v). Always capitalize "HUB" in "Ultralytics HUB"; use "Ultralytics HUB", not "The Ultralytics HUB".
|
|
15
15
|
- Avoid Equations: Do not include equations or mathematical notations.
|
|
@@ -57,6 +57,8 @@ def get_completion(
|
|
|
57
57
|
data["reasoning"] = {"effort": reasoning_effort or "low"} # Default to low for GPT-5
|
|
58
58
|
|
|
59
59
|
r = requests.post(url, json=data, headers=headers)
|
|
60
|
+
if r.status_code != 200:
|
|
61
|
+
print(f"❌ OpenAI error {r.status_code}:\n{r.text}\n")
|
|
60
62
|
r.raise_for_status()
|
|
61
63
|
response_data = r.json()
|
|
62
64
|
|
|
@@ -85,6 +85,7 @@ dev = [
|
|
|
85
85
|
|
|
86
86
|
[project.scripts]
|
|
87
87
|
ultralytics-actions-first-interaction = "actions.first_interaction:main"
|
|
88
|
+
ultralytics-actions-review-pr = "actions.review_pr:main"
|
|
88
89
|
ultralytics-actions-summarize-pr = "actions.summarize_pr:main"
|
|
89
90
|
ultralytics-actions-summarize-release = "actions.summarize_release:main"
|
|
90
91
|
ultralytics-actions-update-markdown-code-blocks = "actions.update_markdown_code_blocks:main"
|
{ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0/ultralytics_actions.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ultralytics-actions
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Ultralytics Actions for GitHub automation and PR management.
|
|
5
5
|
Author-email: Glenn Jocher <glenn.jocher@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
<a href="https://www.ultralytics.com/"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
|
|
40
40
|
|
|
41
|
-
# 🚀 Ultralytics Actions:
|
|
41
|
+
# 🚀 Ultralytics Actions: AI-powered formatting, labeling & PR summaries for Python and Markdown
|
|
42
42
|
|
|
43
43
|
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
|
44
44
|
|
|
@@ -46,6 +46,7 @@ Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) rep
|
|
|
46
46
|
|
|
47
47
|
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
|
48
48
|
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
|
49
|
+
[](https://github.com/ultralytics/actions/actions/workflows/open-prs.yml)
|
|
49
50
|
[](https://codecov.io/github/ultralytics/actions)
|
|
50
51
|
|
|
51
52
|
[](https://discord.com/invite/ultralytics)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
ultralytics-actions-first-interaction = actions.first_interaction:main
|
|
3
3
|
ultralytics-actions-headers = actions.update_file_headers:main
|
|
4
4
|
ultralytics-actions-info = actions.utils:ultralytics_actions_info
|
|
5
|
+
ultralytics-actions-review-pr = actions.review_pr:main
|
|
5
6
|
ultralytics-actions-summarize-pr = actions.summarize_pr:main
|
|
6
7
|
ultralytics-actions-summarize-release = actions.summarize_release:main
|
|
7
8
|
ultralytics-actions-update-markdown-code-blocks = actions.update_markdown_code_blocks:main
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/tests/test_update_markdown_codeblocks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/requires.txt
RENAMED
|
File without changes
|
{ultralytics_actions-0.0.99 → ultralytics_actions-0.1.0}/ultralytics_actions.egg-info/top_level.txt
RENAMED
|
File without changes
|