ultralytics-actions 0.1.7__tar.gz → 0.1.9__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.7 → ultralytics_actions-0.1.9}/PKG-INFO +1 -1
  2. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/__init__.py +1 -1
  3. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/review_pr.py +52 -59
  4. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/utils/github_utils.py +1 -1
  5. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/utils/openai_utils.py +3 -2
  6. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/ultralytics_actions.egg-info/PKG-INFO +1 -1
  7. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/LICENSE +0 -0
  8. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/README.md +0 -0
  9. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/dispatch_actions.py +0 -0
  10. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/first_interaction.py +0 -0
  11. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/summarize_pr.py +0 -0
  12. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/summarize_release.py +0 -0
  13. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/update_file_headers.py +0 -0
  14. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/update_markdown_code_blocks.py +0 -0
  15. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/utils/__init__.py +0 -0
  16. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/utils/common_utils.py +0 -0
  17. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/actions/utils/version_utils.py +0 -0
  18. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/pyproject.toml +0 -0
  19. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/setup.cfg +0 -0
  20. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_cli_commands.py +0 -0
  21. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_common_utils.py +0 -0
  22. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_dispatch_actions.py +0 -0
  23. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_file_headers.py +0 -0
  24. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_first_interaction.py +0 -0
  25. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_github_utils.py +0 -0
  26. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_init.py +0 -0
  27. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_openai_utils.py +0 -0
  28. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_summarize_pr.py +0 -0
  29. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_summarize_release.py +0 -0
  30. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_update_markdown_codeblocks.py +0 -0
  31. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/tests/test_urls.py +0 -0
  32. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/ultralytics_actions.egg-info/SOURCES.txt +0 -0
  33. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
  34. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/ultralytics_actions.egg-info/entry_points.txt +0 -0
  35. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/ultralytics_actions.egg-info/requires.txt +0 -0
  36. {ultralytics_actions-0.1.7 → ultralytics_actions-0.1.9}/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.7
3
+ Version: 0.1.9
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.7"
26
+ __version__ = "0.1.9"
@@ -30,9 +30,10 @@ SKIP_PATTERNS = [
30
30
  ]
31
31
 
32
32
 
33
- def parse_diff_files(diff_text: str) -> dict:
34
- """Parse diff to extract file paths, valid line numbers, and line content for comments (both sides)."""
33
+ def parse_diff_files(diff_text: str) -> tuple[dict, str]:
34
+ """Parse diff and return file mapping with line numbers AND augmented diff with explicit line numbers."""
35
35
  files, current_file, new_line, old_line = {}, None, 0, 0
36
+ augmented_lines = []
36
37
 
37
38
  for line in diff_text.split("\n"):
38
39
  if line.startswith("diff --git"):
@@ -41,23 +42,31 @@ def parse_diff_files(diff_text: str) -> dict:
41
42
  new_line, old_line = 0, 0
42
43
  if current_file:
43
44
  files[current_file] = {"RIGHT": {}, "LEFT": {}}
45
+ augmented_lines.append(line)
44
46
  elif line.startswith("@@") and current_file:
45
- # Extract both old and new line numbers
46
47
  match = re.search(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)?", line)
47
48
  if match:
48
49
  old_line, new_line = int(match.group(1)), int(match.group(2))
50
+ augmented_lines.append(line)
49
51
  elif current_file and (new_line > 0 or old_line > 0):
50
52
  if line.startswith("+") and not line.startswith("+++"):
51
- files[current_file]["RIGHT"][new_line] = line[1:] # Added line (right/new side)
53
+ files[current_file]["RIGHT"][new_line] = line[1:]
54
+ augmented_lines.append(f"R{new_line:>5} {line}") # Prefix with RIGHT line number
52
55
  new_line += 1
53
56
  elif line.startswith("-") and not line.startswith("---"):
54
- files[current_file]["LEFT"][old_line] = line[1:] # Removed line (left/old side)
57
+ files[current_file]["LEFT"][old_line] = line[1:]
58
+ augmented_lines.append(f"L{old_line:>5} {line}") # Prefix with LEFT line number
55
59
  old_line += 1
56
- elif not line.startswith("\\"): # Context line (ignore "No newline" markers)
60
+ elif not line.startswith("\\"):
61
+ augmented_lines.append(f" {line}") # Context line, no number
57
62
  new_line += 1
58
63
  old_line += 1
64
+ else:
65
+ augmented_lines.append(line)
66
+ else:
67
+ augmented_lines.append(line)
59
68
 
60
- return files
69
+ return files, "\n".join(augmented_lines)
61
70
 
62
71
 
63
72
  def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_description: str) -> dict:
@@ -65,7 +74,7 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
65
74
  if not diff_text:
66
75
  return {"comments": [], "summary": "No changes detected in diff"}
67
76
 
68
- diff_files = parse_diff_files(diff_text)
77
+ diff_files, augmented_diff = parse_diff_files(diff_text)
69
78
  if not diff_files:
70
79
  return {"comments": [], "summary": "No files with changes detected in diff"}
71
80
 
@@ -82,7 +91,7 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
82
91
  return {"comments": [], "summary": f"All {skipped_count} changed files are generated/vendored (skipped review)"}
83
92
 
84
93
  file_list = list(diff_files.keys())
85
- diff_truncated = len(diff_text) > MAX_PROMPT_CHARS
94
+ diff_truncated = len(augmented_diff) > MAX_PROMPT_CHARS
86
95
  lines_changed = sum(len(sides["RIGHT"]) + len(sides["LEFT"]) for sides in diff_files.values())
87
96
 
88
97
  content = (
@@ -108,17 +117,19 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
108
117
  "- Avoid triple backticks (```) in suggestions as they break markdown formatting\n"
109
118
  "- It's better to flag an issue without a suggestion than provide a wrong or uncertain fix\n\n"
110
119
  "LINE NUMBERS:\n"
111
- "- You MUST extract line numbers directly from the @@ hunk headers in the diff below\n"
112
- "- RIGHT (added +): Find @@ lines, use numbers after +N (e.g., @@ -10,5 +20,7 @@ means RIGHT starts at line 20)\n"
113
- "- LEFT (removed -): Find @@ lines, use numbers after -N (e.g., @@ -10,5 +20,7 @@ means LEFT starts at line 10)\n"
114
- "- Count forward from hunk start: + lines increment RIGHT, - lines increment LEFT, context lines increment both\n"
115
- "- CRITICAL: Using line numbers not in the diff will cause your comment to be rejected\n"
116
- "- Suggestions only work on RIGHT (added) lines, never on LEFT (removed) lines\n\n"
120
+ "- Each line in the diff is prefixed with its line number for clarity:\n"
121
+ " R 123 +added code <- RIGHT side (new file), line 123\n"
122
+ " L 45 -removed code <- LEFT side (old file), line 45\n"
123
+ " context line <- context (no number needed)\n"
124
+ "- Extract the number after R or L prefix to get the exact line number\n"
125
+ "- Use 'side': 'RIGHT' for R-prefixed lines, 'side': 'LEFT' for L-prefixed lines\n"
126
+ "- Suggestions only work on RIGHT lines, never on LEFT lines\n"
127
+ "- CRITICAL: Only use line numbers that you see explicitly prefixed in the diff\n\n"
117
128
  "Return JSON: "
118
129
  '{"comments": [{"file": "exact/path", "line": N, "side": "RIGHT", "severity": "HIGH", "message": "..."}], "summary": "..."}\n\n'
119
130
  "Rules:\n"
120
- "- Verify line numbers from @@ hunks: +N for RIGHT (added), -N for LEFT (removed)\n"
121
- "- Exact paths (no ./), 'side' field defaults to RIGHT if omitted\n"
131
+ "- Extract line numbers from R#### or L#### prefixes in the diff\n"
132
+ "- Exact paths (no ./), 'side' field must match R (RIGHT) or L (LEFT) prefix\n"
122
133
  "- Severity: CRITICAL, HIGH, MEDIUM, LOW, SUGGESTION\n"
123
134
  f"- Files changed: {len(file_list)} ({', '.join(file_list[:10])}{'...' if len(file_list) > 10 else ''})\n"
124
135
  f"- Lines changed: {lines_changed}\n"
@@ -132,19 +143,26 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
132
143
  f"Review this PR in https://github.com/{repository}:\n"
133
144
  f"Title: {pr_title}\n"
134
145
  f"Description: {remove_html_comments(pr_description or '')[:1000]}\n\n"
135
- f"Diff:\n{diff_text[:MAX_PROMPT_CHARS]}\n\n"
146
+ f"Diff:\n{augmented_diff[:MAX_PROMPT_CHARS]}\n\n"
136
147
  "Now review this diff according to the rules above. Return JSON with comments array and summary."
137
148
  ),
138
149
  },
139
150
  ]
140
151
 
152
+ # Debug: print prompts sent to AI
153
+ # print(f"\nSystem prompt (first 1000 chars):\n{messages[0]['content'][:2000]}...\n")
154
+ # print(f"\nUser prompt (first 1000 chars):\n{messages[1]['content'][:2000]}...\n")
155
+
141
156
  try:
142
- response = get_completion(messages, reasoning_effort="medium", model="gpt-5-codex")
157
+ response = get_completion(messages, reasoning_effort="low", model="gpt-5-codex")
143
158
 
144
159
  json_str = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", response, re.DOTALL)
145
160
  review_data = json.loads(json_str.group(1) if json_str else response)
146
161
  print(json.dumps(review_data, indent=2))
147
- print(f"AI generated {len(review_data.get('comments', []))} comments")
162
+
163
+ # Count comments BEFORE filtering (for COMMENT vs APPROVE decision)
164
+ comments_before_filtering = len(review_data.get("comments", []))
165
+ print(f"AI generated {comments_before_filtering} comments")
148
166
 
149
167
  # Validate, filter, and deduplicate comments
150
168
  unique_comments = {}
@@ -158,20 +176,14 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
158
176
  print(f"Filtered out {file_path}:{line_num} (file not in diff)")
159
177
  continue
160
178
  if line_num not in diff_files[file_path].get(side, {}):
161
- # Try other side if not found
162
- other_side = "LEFT" if side == "RIGHT" else "RIGHT"
163
- if line_num in diff_files[file_path].get(other_side, {}):
164
- print(f"Switching {file_path}:{line_num} from {side} to {other_side}")
165
- c["side"] = other_side
166
- side = other_side
167
- # GitHub rejects suggestions on removed lines
168
- if side == "LEFT" and c.get("suggestion"):
169
- print(f"Dropping suggestion for {file_path}:{line_num} - LEFT side doesn't support suggestions")
170
- c.pop("suggestion", None)
171
- else:
172
- available = {s: list(diff_files[file_path][s].keys())[:10] for s in ["RIGHT", "LEFT"]}
173
- print(f"Filtered out {file_path}:{line_num} (available: {available})")
174
- continue
179
+ available = {s: list(diff_files[file_path][s].keys())[:10] for s in ["RIGHT", "LEFT"]}
180
+ print(f"Filtered out {file_path}:{line_num} (side={side}, available: {available})")
181
+ continue
182
+
183
+ # GitHub rejects suggestions on removed lines
184
+ if side == "LEFT" and c.get("suggestion"):
185
+ print(f"Dropping suggestion for {file_path}:{line_num} - LEFT side doesn't support suggestions")
186
+ c.pop("suggestion", None)
175
187
 
176
188
  # Validate start_line if provided - drop start_line for suggestions (single-line only)
177
189
  if start_line:
@@ -195,6 +207,7 @@ def generate_pr_review(repository: str, diff_text: str, pr_title: str, pr_descri
195
207
  review_data.update(
196
208
  {
197
209
  "comments": list(unique_comments.values()),
210
+ "comments_before_filtering": comments_before_filtering,
198
211
  "diff_files": diff_files,
199
212
  "diff_truncated": diff_truncated,
200
213
  "skipped_files": skipped_count,
@@ -251,27 +264,11 @@ def post_review_summary(event: Action, review_data: dict, review_number: int) ->
251
264
  comments = review_data.get("comments", [])
252
265
  summary = review_data.get("summary") or ""
253
266
 
254
- # Don't approve if error occurred or if there are critical/high severity issues
267
+ # Don't approve if error occurred, inline comments exist, or critical/high severity issues
255
268
  has_error = not summary or ERROR_MARKER in summary
269
+ has_inline_comments = review_data.get("comments_before_filtering", 0) > 0
256
270
  has_issues = any(c.get("severity") not in ["LOW", "SUGGESTION", None] for c in comments)
257
- requests_changes = any(
258
- phrase in summary.lower()
259
- for phrase in [
260
- "please",
261
- "should",
262
- "must",
263
- "raise",
264
- "needs",
265
- "before merging",
266
- "fix",
267
- "error",
268
- "issue",
269
- "problem",
270
- "warning",
271
- "concern",
272
- ]
273
- )
274
- event_type = "COMMENT" if (has_error or has_issues or requests_changes) else "APPROVE"
271
+ event_type = "COMMENT" if (has_error or has_inline_comments or has_issues) else "APPROVE"
275
272
 
276
273
  body = (
277
274
  f"## {review_title}\n\n"
@@ -296,10 +293,8 @@ def post_review_summary(event: Action, review_data: dict, review_number: int) ->
296
293
  continue
297
294
 
298
295
  severity = comment.get("severity") or "SUGGESTION"
299
- comment_body = f"{EMOJI_MAP.get(severity, '💭')} **{severity}**: {(comment.get('message') or '')[:1000]}"
300
-
301
- # Get side (LEFT for removed lines, RIGHT for added lines)
302
296
  side = comment.get("side", "RIGHT")
297
+ comment_body = f"{EMOJI_MAP.get(severity, '💭')} **{severity}**: {(comment.get('message') or '')[:1000]}"
303
298
 
304
299
  if suggestion := comment.get("suggestion"):
305
300
  suggestion = suggestion[:1000] # Clip suggestion length
@@ -316,15 +311,13 @@ def post_review_summary(event: Action, review_data: dict, review_number: int) ->
316
311
  if start_line < line:
317
312
  review_comment["start_line"] = start_line
318
313
  review_comment["start_side"] = side
319
- print(f"Multi-line comment: {file_path}:{start_line}-{line} ({side})")
320
314
 
321
315
  review_comments.append(review_comment)
322
316
 
323
317
  # Submit review with inline comments
324
- payload = {"commit_id": commit_sha, "body": body, "event": event_type}
318
+ payload = {"commit_id": commit_sha, "body": body.strip(), "event": event_type}
325
319
  if review_comments:
326
320
  payload["comments"] = review_comments
327
- print(f"Posting review with {len(review_comments)} inline comments")
328
321
 
329
322
  event.post(
330
323
  f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}/reviews",
@@ -131,7 +131,7 @@ class Action:
131
131
 
132
132
  if self.verbose:
133
133
  elapsed = r.elapsed.total_seconds()
134
- print(f"{'✓' if success else '✗'} {method.upper()} {url} → {r.status_code} ({elapsed:.1f}s)")
134
+ print(f"{'✓' if success else '✗'} {method.upper()} {url} → {r.status_code} ({elapsed:.1f}s)", flush=True)
135
135
  if not success:
136
136
  try:
137
137
  print(f" ❌ Error: {r.json().get('message', 'Unknown error')}")
@@ -161,8 +161,9 @@ def get_completion(
161
161
  continue
162
162
  raise
163
163
  except requests.exceptions.HTTPError as e:
164
- if attempt < 2 and e.response and e.response.status_code >= 500:
165
- print(f"Server error {e.response.status_code}, retrying in {2**attempt}s")
164
+ status_code = getattr(e.response, "status_code", 0) if e.response else 0
165
+ if attempt < 2 and status_code >= 500:
166
+ print(f"Server error {status_code}, retrying in {2**attempt}s")
166
167
  time.sleep(2**attempt)
167
168
  continue
168
169
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.1.7
3
+ Version: 0.1.9
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>