ultralytics-actions 0.1.1__tar.gz → 0.1.3__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.

Files changed (36) hide show
  1. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/PKG-INFO +1 -1
  2. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/__init__.py +1 -1
  3. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/first_interaction.py +19 -1
  4. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/review_pr.py +23 -37
  5. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/summarize_pr.py +2 -1
  6. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/summarize_release.py +4 -4
  7. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/utils/github_utils.py +35 -3
  8. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/PKG-INFO +1 -1
  9. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/LICENSE +0 -0
  10. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/README.md +0 -0
  11. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/dispatch_actions.py +0 -0
  12. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/update_file_headers.py +0 -0
  13. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/update_markdown_code_blocks.py +0 -0
  14. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/utils/__init__.py +0 -0
  15. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/utils/common_utils.py +0 -0
  16. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/utils/openai_utils.py +0 -0
  17. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/actions/utils/version_utils.py +0 -0
  18. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/pyproject.toml +0 -0
  19. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/setup.cfg +0 -0
  20. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_cli_commands.py +0 -0
  21. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_common_utils.py +0 -0
  22. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_dispatch_actions.py +0 -0
  23. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_file_headers.py +0 -0
  24. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_first_interaction.py +0 -0
  25. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_github_utils.py +0 -0
  26. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_init.py +0 -0
  27. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_openai_utils.py +0 -0
  28. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_summarize_pr.py +0 -0
  29. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_summarize_release.py +0 -0
  30. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_update_markdown_codeblocks.py +0 -0
  31. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/tests/test_urls.py +0 -0
  32. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/SOURCES.txt +0 -0
  33. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
  34. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/entry_points.txt +0 -0
  35. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/requires.txt +0 -0
  36. {ultralytics_actions-0.1.1 → ultralytics_actions-0.1.3}/ultralytics_actions.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.1.1
3
+ Version: 0.1.3
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>
@@ -23,4 +23,4 @@
23
23
  # ├── test_summarize_pr.py
24
24
  # └── ...
25
25
 
26
- __version__ = "0.1.1"
26
+ __version__ = "0.1.3"
@@ -5,12 +5,14 @@ from __future__ import annotations
5
5
  import os
6
6
  import time
7
7
 
8
+ from . import review_pr
8
9
  from .utils import Action, filter_labels, get_completion, get_pr_open_response, remove_html_comments
9
10
 
10
11
  SUMMARY_START = (
11
12
  "## 🛠️ PR Summary\n\n<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)<sub>\n\n"
12
13
  )
13
14
  BLOCK_USER = os.getenv("BLOCK_USER", "false").lower() == "true"
15
+ AUTO_PR_REVIEW = os.getenv("REVIEW", "true").lower() == "true"
14
16
 
15
17
 
16
18
  def apply_and_check_labels(event, number, node_id, issue_type, username, labels, label_descriptions):
@@ -176,19 +178,27 @@ YOUR {issue_type.upper()} RESPONSE:
176
178
  def main(*args, **kwargs):
177
179
  """Executes auto-labeling and custom response generation for new GitHub issues, PRs, and discussions."""
178
180
  event = Action(*args, **kwargs)
181
+ if event.should_skip_openai():
182
+ return
183
+
179
184
  number, node_id, title, body, username, issue_type, action = get_event_content(event)
180
185
  available_labels = event.get_repo_data("labels")
181
186
  label_descriptions = {label["name"]: label.get("description", "") for label in available_labels}
182
187
 
183
188
  # Use unified PR open response for new PRs (summary + labels + first comment in 1 API call)
184
189
  if issue_type == "pull request" and action == "opened":
190
+ if event.should_skip_pr_author():
191
+ return
192
+
185
193
  print("Processing PR open with unified API call...")
186
194
  diff = event.get_pr_diff()
187
195
  response = get_pr_open_response(event.repository, diff, title, body, label_descriptions)
188
196
 
189
197
  if summary := response.get("summary"):
190
198
  print("Updating PR description with summary...")
191
- event.update_pr_description(number, SUMMARY_START + summary)
199
+ event.update_pr_description(number, SUMMARY_START + summary + "\n\n" + body)
200
+ else:
201
+ summary = body
192
202
 
193
203
  if relevant_labels := response.get("labels", []):
194
204
  apply_and_check_labels(event, number, node_id, issue_type, username, relevant_labels, label_descriptions)
@@ -197,6 +207,14 @@ def main(*args, **kwargs):
197
207
  print("Adding first interaction comment...")
198
208
  time.sleep(1) # sleep to ensure label added first
199
209
  event.add_comment(number, node_id, first_comment, issue_type)
210
+
211
+ # Automatic PR review after first interaction
212
+ if AUTO_PR_REVIEW:
213
+ print("Starting automatic PR review...")
214
+ review_number = review_pr.dismiss_previous_reviews(event)
215
+ review_data = review_pr.generate_pr_review(event.repository, diff, title, summary)
216
+ review_pr.post_review_summary(event, review_data, review_number)
217
+ print("PR review completed")
200
218
  return
201
219
 
202
220
  # Handle issues and discussions (NOT PRs)
@@ -79,49 +79,32 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
79
79
  diff_truncated = len(diff_text) > limit
80
80
  lines_changed = sum(len(lines) for lines in diff_files.values())
81
81
 
82
- comment_guidance = (
83
- "Provide up to 1-3 comments only if critical issues exist"
84
- if lines_changed < 50
85
- else "Provide up to 3-5 comments only if high-impact issues exist"
86
- if lines_changed < 200
87
- else "Provide up to 5-10 comments only for the most critical issues"
88
- )
89
-
90
82
  content = (
91
83
  "You are an expert code reviewer for Ultralytics. Provide detailed inline comments on specific code changes.\n\n"
92
- "Focus on: Code quality, style, best practices, bugs, edge cases, error handling, performance, security, documentation, test coverage\n\n"
93
- "FORMATTING: Use backticks for code, file names, branch names, function names, variable names, packages\n\n"
84
+ "Focus on: Bugs, security, performance, best practices, edge cases, error handling\n\n"
85
+ "FORMATTING: Use backticks for code: `x=3`, `file.py`, `function()`\n\n"
94
86
  "CRITICAL RULES:\n"
95
- "1. Quality over quantity: Zero comments is fine for clean code - only flag truly important issues\n"
96
- f"2. {comment_guidance} - these are maximums, not targets\n"
97
- "3. CRITICAL: Do not post separate comments on adjacent/nearby lines (within 10 lines). Combine all related issues into ONE comment\n"
98
- "4. When combining issues from multiple lines, use 'start_line' (first line) and 'line' (last line) to highlight the entire range\n"
99
- "5. Each comment must reference separate areas - no overlapping line ranges\n"
100
- "6. Prioritize: CRITICAL bugs/security > HIGH impact issues > code quality\n"
101
- "7. Keep comments concise, friendly, and easy to understand - avoid jargon when possible\n"
102
- "8. DO not comment on routine changes: adding imports, adding dependencies, updating version numbers, standard refactoring\n"
103
- "9. Trust the developer - only flag issues with clear evidence of problems, not hypothetical concerns\n\n"
104
- "SUMMARY GUIDELINES:\n"
105
- "- Keep summary brief, clear, and actionable - avoid overly detailed explanations\n"
106
- "- Highlight only the most important findings\n"
107
- "- Do NOT include file names or line numbers in the summary - inline comments already show exact locations\n"
108
- "- Focus on what needs to be fixed, not where\n\n"
109
- "CODE SUGGESTIONS:\n"
87
+ "1. Quality over quantity - zero comments is fine for clean code, only flag truly important issues\n"
88
+ "2. Combine issues that are directly related to the same problem\n"
89
+ "3. Use 'start_line' and 'line' to highlight multi-line ranges when issues span multiple lines\n"
90
+ "4. Prioritize: CRITICAL bugs/security > HIGH impact > code quality improvements\n"
91
+ "5. Keep comments concise and friendly - avoid jargon\n"
92
+ "6. Skip routine changes: imports, version updates, standard refactoring\n\n"
93
+ "SUMMARY:\n"
94
+ "- Brief and actionable - what needs fixing, not where (locations shown in inline comments)\n\n"
95
+ "SUGGESTIONS:\n"
110
96
  "- ONLY provide 'suggestion' field when you have high certainty the code is problematic AND sufficient context for a confident fix\n"
111
97
  "- If uncertain about the correct fix, omit 'suggestion' field and explain the concern in 'message' only\n"
112
98
  "- Suggestions must be ready-to-merge code with NO comments, placeholders, or explanations\n"
113
99
  "- Suggestions replace ONLY the single line at 'line' - for multi-line fixes, describe the change in 'message' instead\n"
114
100
  "- Do NOT provide 'start_line' when including a 'suggestion' - suggestions are always single-line only\n"
115
101
  "- Suggestion content must match the exact indentation of the original line\n"
116
- "- Never include triple backticks (```) in suggestions as they break markdown formatting\n"
102
+ "- Avoid triple backticks (```) in suggestions as they break markdown formatting\n"
117
103
  "- It's better to flag an issue without a suggestion than provide a wrong or uncertain fix\n\n"
118
104
  "Return JSON: "
119
105
  '{"comments": [{"file": "exact/path", "line": N, "severity": "HIGH", "message": "...", "suggestion": "..."}], "summary": "..."}\n\n'
120
106
  "Rules:\n"
121
- "- Only comment on NEW lines (starting with + in diff)\n"
122
- "- Use exact file paths from diff (no ./ prefix)\n"
123
- "- Line numbers must match NEW file line numbers from @@ hunks\n"
124
- "- When '- old' then '+ new', new line keeps SAME line number\n"
107
+ "- Only NEW lines (+ in diff), exact paths (no ./), correct line numbers from @@ hunks\n"
125
108
  "- Severity: CRITICAL, HIGH, MEDIUM, LOW, SUGGESTION\n"
126
109
  f"- Files changed: {len(file_list)} ({', '.join(file_list[:10])}{'...' if len(file_list) > 10 else ''})\n"
127
110
  f"- Lines changed: {lines_changed}\n"
@@ -236,19 +219,17 @@ def post_review_summary(event: Action, review_data: dict, review_number: int) ->
236
219
 
237
220
  review_title = f"{REVIEW_MARKER} {review_number}" if review_number > 1 else REVIEW_MARKER
238
221
  comments = review_data.get("comments", [])
239
- event_type = (
240
- "REQUEST_CHANGES" if any(c.get("severity") not in ["LOW", "SUGGESTION", None] for c in comments) else "APPROVE"
241
- )
222
+ event_type = "COMMENT" if any(c.get("severity") not in ["LOW", "SUGGESTION", None] for c in comments) else "APPROVE"
242
223
 
243
224
  body = (
244
225
  f"## {review_title}\n\n"
245
226
  "<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)</sub>\n\n"
246
- f"{review_data.get('summary', 'Review completed')}\n\n"
227
+ f"{review_data.get('summary', 'Review completed')[:1000]}\n\n" # Clip summary length
247
228
  )
248
229
 
249
230
  if comments:
250
231
  shown = min(len(comments), 10)
251
- body += f"💬 Posted {shown} inline comment{'s' if shown != 1 else ''}{' (10 shown, more available)' if len(comments) > 10 else ''}\n"
232
+ body += f"💬 Posted {shown} inline comment{'s' if shown != 1 else ''}\n"
252
233
 
253
234
  if review_data.get("diff_truncated"):
254
235
  body += "\n⚠️ **Large PR**: Review focused on critical issues. Some details may not be covered.\n"
@@ -258,14 +239,15 @@ def post_review_summary(event: Action, review_data: dict, review_number: int) ->
258
239
 
259
240
  # Build inline comments for the review
260
241
  review_comments = []
261
- for comment in comments[:10]:
242
+ for comment in comments[:10]: # Limit to 10 comments
262
243
  if not (file_path := comment.get("file")) or not (line := comment.get("line", 0)):
263
244
  continue
264
245
 
265
246
  severity = comment.get("severity", "SUGGESTION")
266
- comment_body = f"{EMOJI_MAP.get(severity, '💭')} **{severity}**: {comment.get('message', '')}"
247
+ comment_body = f"{EMOJI_MAP.get(severity, '💭')} **{severity}**: {comment.get('message', '')[:1000]}"
267
248
 
268
249
  if suggestion := comment.get("suggestion"):
250
+ suggestion = suggestion[:1000] # Clip suggestion length
269
251
  if "```" not in suggestion:
270
252
  # Extract original line indentation and apply to suggestion
271
253
  if original_line := review_data.get("diff_files", {}).get(file_path, {}).get(line):
@@ -309,6 +291,10 @@ def main(*args, **kwargs):
309
291
  print(f"Skipping: PR state is {event.pr.get('state') if event.pr else 'None'}")
310
292
  return
311
293
 
294
+ # Skip self-authored or bot PRs unless manually review_requested
295
+ if event.event_data.get("action") != "review_requested" and event.should_skip_pr_author():
296
+ return
297
+
312
298
  print(f"Starting PR review for #{event.pr['number']}")
313
299
  review_number = dismiss_previous_reviews(event)
314
300
 
@@ -96,10 +96,11 @@ def main(*args, **kwargs):
96
96
  """Summarize a pull request and update its description with a summary."""
97
97
  event = Action(*args, **kwargs)
98
98
  action = event.event_data.get("action")
99
-
100
99
  if action == "opened":
101
100
  print("Skipping PR open - handled by first_interaction.py with unified API call")
102
101
  return
102
+ if event.should_skip_openai():
103
+ return
103
104
 
104
105
  print(f"Retrieving diff for PR {event.pr['number']}")
105
106
  diff = event.get_pr_diff()
@@ -19,7 +19,7 @@ def get_release_diff(event, previous_tag: str, latest_tag: str) -> str:
19
19
  """Retrieves the differences between two specified Git tags in a GitHub repository."""
20
20
  url = f"{GITHUB_API_URL}/repos/{event.repository}/compare/{previous_tag}...{latest_tag}"
21
21
  r = event.get(url, headers=event.headers_diff)
22
- return r.text if r.status_code == 200 else f"Failed to get diff: {r.content}"
22
+ return r.text if r.status_code == 200 else f"Failed to get diff: {r.text}"
23
23
 
24
24
 
25
25
  def get_prs_between_tags(event, previous_tag: str, latest_tag: str) -> list:
@@ -42,8 +42,8 @@ def get_prs_between_tags(event, previous_tag: str, latest_tag: str) -> list:
42
42
  pr_numbers.update(pr_matches)
43
43
 
44
44
  prs = []
45
- time.sleep(10) # Allow final PR summary to update on merge
46
45
  for pr_number in sorted(pr_numbers): # earliest to latest
46
+ time.sleep(1) # Rate limit: GitHub search API has strict limits
47
47
  pr_url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}"
48
48
  pr_response = event.get(pr_url)
49
49
  if pr_response.status_code == 200:
@@ -52,7 +52,7 @@ def get_prs_between_tags(event, previous_tag: str, latest_tag: str) -> list:
52
52
  {
53
53
  "number": pr_data["number"],
54
54
  "title": pr_data["title"],
55
- "body": remove_html_comments(pr_data["body"]),
55
+ "body": remove_html_comments(pr_data.get("body", "")),
56
56
  "author": pr_data["user"]["login"],
57
57
  "html_url": pr_data["html_url"],
58
58
  "merged_at": pr_data["merged_at"],
@@ -140,7 +140,7 @@ def generate_release_summary(
140
140
  },
141
141
  {
142
142
  "role": "user",
143
- "content": f"Summarize the updates made in the '{latest_tag}' tag, focusing on major model or features changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Prioritize changes from the current PR (the first in the list), which is usually the most important in the release. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
143
+ "content": f"Summarize the updates made in the '{latest_tag}' tag, focusing on major model or features changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Prioritize changes from the current PR (the last in the list), which is usually the most important in the release. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
144
144
  f"## 🌟 Summary (single-line synopsis)\n"
145
145
  f"## 📊 Key Changes (bullet points highlighting any major changes)\n"
146
146
  f"## 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n\n\n"
@@ -114,6 +114,7 @@ class Action:
114
114
  self.verbose = verbose
115
115
  self.eyes_reaction_id = None
116
116
  self._pr_diff_cache = None
117
+ self._pr_summary_cache = None
117
118
  self._username_cache = None
118
119
  self._default_status = {
119
120
  "get": [200],
@@ -187,6 +188,36 @@ class Action:
187
188
  """Checks if a user is a member of the organization."""
188
189
  return self.get(f"{GITHUB_API_URL}/orgs/{self.repository.split('/')[0]}/members/{username}").status_code == 204
189
190
 
191
+ def should_skip_pr_author(self) -> bool:
192
+ """Checks if PR should be skipped based on author (self-authored or bot PRs)."""
193
+ if not self.pr:
194
+ return False
195
+ if pr_author := self.pr.get("user", {}).get("login"):
196
+ if pr_author == self.get_username():
197
+ print(f"Skipping: PR author ({pr_author}) is the same as bot")
198
+ return True
199
+ # Check both user.type and [bot] suffix for robust bot detection
200
+ if self.pr.get("user", {}).get("type") == "Bot" or pr_author.endswith("[bot]"):
201
+ print(f"Skipping: PR author ({pr_author}) is a bot")
202
+ return True
203
+ return False
204
+
205
+ def is_fork_pr(self) -> bool:
206
+ """Checks if PR is from a fork (different repo than base)."""
207
+ if not self.pr:
208
+ return False
209
+ head_repo = self.pr.get("head", {}).get("repo", {}).get("full_name")
210
+ return bool(head_repo) and head_repo != self.repository
211
+
212
+ def should_skip_openai(self) -> bool:
213
+ """Check if OpenAI operations should be skipped."""
214
+ from actions.utils.openai_utils import OPENAI_API_KEY
215
+
216
+ if not OPENAI_API_KEY:
217
+ print("⚠️ Skipping LLM operations (OPENAI_API_KEY not found)")
218
+ return True
219
+ return False
220
+
190
221
  def get_pr_diff(self) -> str:
191
222
  """Retrieves the diff content for a specified pull request with caching."""
192
223
  if self._pr_diff_cache:
@@ -256,6 +287,7 @@ class Action:
256
287
  updated_description = description + "\n\n" + new_summary
257
288
 
258
289
  self.patch(url, json={"body": updated_description})
290
+ self._pr_summary_cache = new_summary
259
291
 
260
292
  def get_label_ids(self, labels: list[str]) -> list[str]:
261
293
  """Retrieves GitHub label IDs for a list of label names using the GraphQL API."""
@@ -368,7 +400,7 @@ Thank you 🙏
368
400
  try:
369
401
  data = response.json()["data"]["repository"]["pullRequest"]
370
402
  comments = data["reviews"]["nodes"] + data["comments"]["nodes"]
371
- token_username = self.get_username()
403
+ username = self.get_username()
372
404
  author = data["author"]["login"] if data["author"]["__typename"] != "Bot" else None
373
405
 
374
406
  contributors = {x["author"]["login"] for x in comments if x["author"]["__typename"] != "Bot"}
@@ -381,10 +413,10 @@ Thank you 🙏
381
413
  contributors.add(login)
382
414
 
383
415
  contributors.discard(author)
384
- contributors.discard(token_username)
416
+ contributors.discard(username)
385
417
 
386
418
  pr_credit = ""
387
- if author and author != token_username:
419
+ if author and author != username:
388
420
  pr_credit += f"@{author}"
389
421
  if contributors:
390
422
  pr_credit += (" with contributions from " if pr_credit else "") + ", ".join(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.1.1
3
+ Version: 0.1.3
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>