ultralytics-actions 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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.

actions/__init__.py CHANGED
@@ -23,4 +23,4 @@
23
23
  # ├── test_summarize_pr.py
24
24
  # └── ...
25
25
 
26
- __version__ = "0.1.0"
26
+ __version__ = "0.1.1"
@@ -44,7 +44,7 @@ def trigger_and_get_workflow_info(event, branch: str) -> list[dict]:
44
44
  run_number = None
45
45
 
46
46
  runs_response = event.get(
47
- f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/runs?branch={branch}&event=workflow_dispatch&per_page=1",
47
+ f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/runs?branch={branch}&event=workflow_dispatch&per_page=1"
48
48
  )
49
49
 
50
50
  if runs_response.status_code == 200:
@@ -57,10 +57,10 @@ def trigger_and_get_workflow_info(event, branch: str) -> list[dict]:
57
57
  return results
58
58
 
59
59
 
60
- def update_comment(event, comment_body: str, triggered_actions: list[dict], branch: str) -> bool:
60
+ def update_comment(event, comment_body: str, triggered_actions: list[dict], branch: str):
61
61
  """Updates the comment with workflow information."""
62
62
  if not triggered_actions:
63
- return False
63
+ return
64
64
 
65
65
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
66
66
  summary = (
@@ -3,18 +3,37 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import time
6
7
 
7
- from .utils import GITHUB_API_URL, Action, get_completion, remove_html_comments
8
+ from .utils import Action, filter_labels, get_completion, get_pr_open_response, remove_html_comments
8
9
 
9
- # Environment variables
10
+ SUMMARY_START = (
11
+ "## 🛠️ PR Summary\n\n<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)<sub>\n\n"
12
+ )
10
13
  BLOCK_USER = os.getenv("BLOCK_USER", "false").lower() == "true"
11
14
 
12
15
 
16
+ def apply_and_check_labels(event, number, node_id, issue_type, username, labels, label_descriptions):
17
+ """Normalizes, applies labels, and handles Alert label if present."""
18
+ if not labels:
19
+ print("No relevant labels found or applied.")
20
+ return
21
+
22
+ available = {k.lower(): k for k in label_descriptions}
23
+ normalized = [available.get(label.lower(), label) for label in labels if label.lower() in available]
24
+
25
+ if normalized:
26
+ print(f"Applying labels: {normalized}")
27
+ event.apply_labels(number, node_id, normalized, issue_type)
28
+ if "Alert" in normalized and not event.is_org_member(username):
29
+ event.handle_alert(number, node_id, issue_type, username, block=BLOCK_USER)
30
+
31
+
13
32
  def get_event_content(event) -> tuple[int, str, str, str, str, str, str]:
14
33
  """Extracts key information from GitHub event data for issues, pull requests, or discussions."""
15
34
  data = event.event_data
16
35
  name = event.event_name
17
- action = data["action"] # 'opened', 'closed', 'created' (discussion), etc.
36
+ action = data["action"]
18
37
  if name == "issues":
19
38
  item = data["issue"]
20
39
  issue_type = "issue"
@@ -36,107 +55,12 @@ def get_event_content(event) -> tuple[int, str, str, str, str, str, str]:
36
55
  return number, node_id, title, body, username, issue_type, action
37
56
 
38
57
 
39
- def update_issue_pr_content(event, number: int, node_id: str, issue_type: str):
40
- """Updates the title and body of an issue, pull request, or discussion with predefined content."""
41
- new_title = "Content Under Review"
42
- new_body = """This post has been flagged for review by [Ultralytics Actions](https://ultralytics.com/actions) due to possible spam, abuse, or off-topic content. For more information please see our:
43
-
44
- - [Code of Conduct](https://docs.ultralytics.com/help/code-of-conduct/)
45
- - [Security Policy](https://docs.ultralytics.com/help/security/)
46
-
47
- For questions or bug reports related to this action please visit https://github.com/ultralytics/actions.
48
-
49
- Thank you 🙏
50
- """
51
- if issue_type == "discussion":
52
- mutation = """
53
- mutation($discussionId: ID!, $title: String!, $body: String!) {
54
- updateDiscussion(input: {discussionId: $discussionId, title: $title, body: $body}) {
55
- discussion {
56
- id
57
- }
58
- }
59
- }
60
- """
61
- event.graphql_request(mutation, variables={"discussionId": node_id, "title": new_title, "body": new_body})
62
- else:
63
- url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}"
64
- event.patch(url, json={"title": new_title, "body": new_body})
65
-
66
-
67
- def close_issue_pr(event, number: int, node_id: str, issue_type: str):
68
- """Closes the specified issue, pull request, or discussion using the GitHub API."""
69
- if issue_type == "discussion":
70
- mutation = """
71
- mutation($discussionId: ID!) {
72
- closeDiscussion(input: {discussionId: $discussionId}) {
73
- discussion {
74
- id
75
- }
76
- }
77
- }
78
- """
79
- event.graphql_request(mutation, variables={"discussionId": node_id})
80
- else:
81
- url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}"
82
- event.patch(url, json={"state": "closed"})
83
-
84
-
85
- def lock_issue_pr(event, number: int, node_id: str, issue_type: str):
86
- """Locks an issue, pull request, or discussion to prevent further interactions."""
87
- if issue_type == "discussion":
88
- mutation = """
89
- mutation($lockableId: ID!, $lockReason: LockReason) {
90
- lockLockable(input: {lockableId: $lockableId, lockReason: $lockReason}) {
91
- lockedRecord {
92
- ... on Discussion {
93
- id
94
- }
95
- }
96
- }
97
- }
98
- """
99
- event.graphql_request(mutation, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
100
- else:
101
- url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/lock"
102
- event.put(url, json={"lock_reason": "off-topic"})
103
-
104
-
105
- def block_user(event, username: str):
106
- """Blocks a user from the organization using the GitHub API."""
107
- url = f"{GITHUB_API_URL}/orgs/{event.repository.split('/')[0]}/blocks/{username}"
108
- event.put(url)
109
-
110
-
111
58
  def get_relevant_labels(
112
59
  issue_type: str, title: str, body: str, available_labels: dict, current_labels: list
113
60
  ) -> list[str]:
114
- """Determines relevant labels for GitHub issues/PRs using OpenAI, considering title, body, and existing labels."""
115
- # Remove mutually exclusive labels like both 'bug' and 'question' or inappropriate labels like 'help wanted'
116
- for label in {
117
- "help wanted",
118
- "TODO",
119
- "research",
120
- "non-reproducible",
121
- "popular",
122
- "invalid",
123
- "Stale",
124
- "wontfix",
125
- "duplicate",
126
- }: # normal case
127
- available_labels.pop(label, None) # remove as should only be manually added
128
- if "bug" in current_labels:
129
- available_labels.pop("question", None)
130
- elif "question" in current_labels:
131
- available_labels.pop("bug", None)
132
-
133
- # Add "Alert" to available labels if not present
134
- if "Alert" not in available_labels:
135
- available_labels["Alert"] = (
136
- "Potential spam, abuse, or illegal activity including advertising, unsolicited promotions, malware, phishing, crypto offers, pirated software or media, free movie downloads, cracks, keygens or any other content that violates terms of service or legal standards."
137
- )
138
-
139
- labels = "\n".join(f"- {name}: {description}" for name, description in available_labels.items())
61
+ """Determines relevant labels for GitHub issues/discussions using OpenAI."""
62
+ filtered_labels = filter_labels(available_labels, current_labels, is_pr=(issue_type == "pull request"))
63
+ labels_str = "\n".join(f"- {name}: {description}" for name, description in filtered_labels.items())
140
64
 
141
65
  prompt = f"""Select the top 1-3 most relevant labels for the following GitHub {issue_type}.
142
66
 
@@ -150,7 +74,7 @@ INSTRUCTIONS:
150
74
  {'7. Only use the "bug" label if the user provides a clear description of the bug, their environment with relevant package versions and a minimum reproducible example.' if issue_type == "issue" else ""}
151
75
 
152
76
  AVAILABLE LABELS:
153
- {labels}
77
+ {labels_str}
154
78
 
155
79
  {issue_type.upper()} TITLE:
156
80
  {title}
@@ -171,7 +95,7 @@ YOUR RESPONSE (label names only):
171
95
  if "none" in suggested_labels.lower():
172
96
  return []
173
97
 
174
- available_labels_lower = {name.lower(): name for name in available_labels}
98
+ available_labels_lower = {name.lower(): name for name in filtered_labels}
175
99
  return [
176
100
  available_labels_lower[label.lower().strip()]
177
101
  for label in suggested_labels.split(",")
@@ -179,83 +103,8 @@ YOUR RESPONSE (label names only):
179
103
  ]
180
104
 
181
105
 
182
- def get_label_ids(event, labels: list[str]) -> list[str]:
183
- """Retrieves GitHub label IDs for a list of label names using the GraphQL API."""
184
- query = """
185
- query($owner: String!, $name: String!) {
186
- repository(owner: $owner, name: $name) {
187
- labels(first: 100, query: "") {
188
- nodes {
189
- id
190
- name
191
- }
192
- }
193
- }
194
- }
195
- """
196
- owner, repo = event.repository.split("/")
197
- result = event.graphql_request(query, variables={"owner": owner, "name": repo})
198
- if "data" in result and "repository" in result["data"]:
199
- all_labels = result["data"]["repository"]["labels"]["nodes"]
200
- label_map = {label["name"].lower(): label["id"] for label in all_labels}
201
- return [label_map.get(label.lower()) for label in labels if label.lower() in label_map]
202
- else:
203
- return []
204
-
205
-
206
- def apply_labels(event, number: int, node_id: str, labels: list[str], issue_type: str):
207
- """Applies specified labels to a GitHub issue, pull request, or discussion using the appropriate API."""
208
- if "Alert" in labels:
209
- create_alert_label(event)
210
-
211
- if issue_type == "discussion":
212
- label_ids = get_label_ids(event, labels)
213
- if not label_ids:
214
- print("No valid labels to apply.")
215
- return
216
-
217
- mutation = """
218
- mutation($labelableId: ID!, $labelIds: [ID!]!) {
219
- addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
220
- labelable {
221
- ... on Discussion {
222
- id
223
- }
224
- }
225
- }
226
- }
227
- """
228
- event.graphql_request(mutation, {"labelableId": node_id, "labelIds": label_ids})
229
- else:
230
- url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/labels"
231
- event.post(url, json={"labels": labels})
232
-
233
-
234
- def create_alert_label(event):
235
- """Creates the 'Alert' label in the repository if it doesn't exist, with a red color and description."""
236
- alert_label = {"name": "Alert", "color": "FF0000", "description": "Potential spam, abuse, or off-topic."}
237
- event.post(f"{GITHUB_API_URL}/repos/{event.repository}/labels", json=alert_label)
238
-
239
-
240
- def add_comment(event, number: int, node_id: str, comment: str, issue_type: str):
241
- """Adds a comment to the specified issue, pull request, or discussion using the GitHub API."""
242
- if issue_type == "discussion":
243
- mutation = """
244
- mutation($discussionId: ID!, $body: String!) {
245
- addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
246
- comment {
247
- id
248
- }
249
- }
250
- }
251
- """
252
- event.graphql_request(mutation, variables={"discussionId": node_id, "body": comment})
253
- else:
254
- event.post(f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/comments", json={"body": comment})
255
-
256
-
257
106
  def get_first_interaction_response(event, issue_type: str, title: str, body: str, username: str) -> str:
258
- """Generates a custom LLM response for GitHub issues, PRs, or discussions based on content."""
107
+ """Generates a custom LLM response for GitHub issues or discussions (NOT PRs - PRs use unified call)."""
259
108
  issue_discussion_response = f"""
260
109
  👋 Hello @{username}, thank you for submitting a `{event.repository}` 🚀 {issue_type.capitalize()}. To help us address your concern efficiently, please ensure you've provided the following information:
261
110
 
@@ -281,35 +130,15 @@ Please make sure you've searched existing {issue_type}s to avoid duplicates. If
281
130
  Thank you for your contribution to improving our project!
282
131
  """
283
132
 
284
- pr_response = f"""
285
- 👋 Hello @{username}, thank you for submitting an `{event.repository}` 🚀 PR! To ensure a seamless integration of your work, please review the following checklist:
286
-
287
- - ✅ **Define a Purpose**: Clearly explain the purpose of your fix or feature in your PR description, and link to any [relevant issues](https://github.com/{event.repository}/issues). Ensure your commit messages are clear, concise, and adhere to the project's conventions.
288
- - ✅ **Synchronize with Source**: Confirm your PR is synchronized with the `{event.repository}` `main` branch. If it's behind, update it by clicking the 'Update branch' button or by running `git pull` and `git merge main` locally.
289
- - ✅ **Ensure CI Checks Pass**: Verify all Ultralytics [Continuous Integration (CI)](https://docs.ultralytics.com/help/CI/) checks are passing. If any checks fail, please address the issues.
290
- - ✅ **Update Documentation**: Update the relevant [documentation](https://docs.ultralytics.com/) for any new or modified features.
291
- - ✅ **Add Tests**: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
292
- - ✅ **Sign the CLA**: Please ensure you have signed our [Contributor License Agreement](https://docs.ultralytics.com/help/CLA/) if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
293
- - ✅ **Minimize Changes**: Limit your changes to the **minimum** necessary for your bug fix or feature addition. _"It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is."_ — Bruce Lee
294
-
295
- For more guidance, please refer to our [Contributing Guide](https://docs.ultralytics.com/help/contributing/). Don’t hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀
296
- """
297
-
298
- if issue_type == "pull request":
299
- example = os.getenv("FIRST_PR_RESPONSE") or pr_response
300
- else:
301
- example = os.getenv("FIRST_ISSUE_RESPONSE") or issue_discussion_response
302
-
133
+ example = os.getenv("FIRST_ISSUE_RESPONSE") or issue_discussion_response
303
134
  org_name, repo_name = event.repository.split("/")
304
- repo_url = f"https://github.com/{event.repository}"
305
- diff = event.get_pr_diff()[:32000] if issue_type == "pull request" else ""
306
135
 
307
136
  prompt = f"""Generate a customized response to the new GitHub {issue_type} below:
308
137
 
309
138
  CONTEXT:
310
139
  - Repository: {repo_name}
311
140
  - Organization: {org_name}
312
- - Repository URL: {repo_url}
141
+ - Repository URL: https://github.com/{event.repository}
313
142
  - User: {username}
314
143
 
315
144
  INSTRUCTIONS:
@@ -332,12 +161,8 @@ EXAMPLE {issue_type.upper()} RESPONSE:
332
161
  {issue_type.upper()} DESCRIPTION:
333
162
  {body[:16000]}
334
163
 
335
- {"PULL REQUEST DIFF:" if issue_type == "pull request" else ""}
336
- {diff if issue_type == "pull request" else ""}
337
-
338
164
  YOUR {issue_type.upper()} RESPONSE:
339
165
  """
340
- # print(f"\n\n{prompt}\n\n") # for debug
341
166
  messages = [
342
167
  {
343
168
  "role": "system",
@@ -354,25 +179,39 @@ def main(*args, **kwargs):
354
179
  number, node_id, title, body, username, issue_type, action = get_event_content(event)
355
180
  available_labels = event.get_repo_data("labels")
356
181
  label_descriptions = {label["name"]: label.get("description", "") for label in available_labels}
357
- if issue_type == "discussion":
358
- current_labels = [] # For discussions, labels may need to be fetched differently or adjusted
359
- else:
360
- current_labels = [label["name"].lower() for label in event.get_repo_data(f"issues/{number}/labels")]
361
- if relevant_labels := get_relevant_labels(issue_type, title, body, label_descriptions, current_labels):
362
- apply_labels(event, number, node_id, relevant_labels, issue_type)
363
- if "Alert" in relevant_labels and not event.is_org_member(username):
364
- update_issue_pr_content(event, number, node_id, issue_type)
365
- if issue_type != "pull request":
366
- close_issue_pr(event, number, node_id, issue_type)
367
- lock_issue_pr(event, number, node_id, issue_type)
368
- if BLOCK_USER:
369
- block_user(event, username=username)
370
- else:
371
- print("No relevant labels found or applied.")
182
+
183
+ # Use unified PR open response for new PRs (summary + labels + first comment in 1 API call)
184
+ if issue_type == "pull request" and action == "opened":
185
+ print("Processing PR open with unified API call...")
186
+ diff = event.get_pr_diff()
187
+ response = get_pr_open_response(event.repository, diff, title, body, label_descriptions)
188
+
189
+ if summary := response.get("summary"):
190
+ print("Updating PR description with summary...")
191
+ event.update_pr_description(number, SUMMARY_START + summary)
192
+
193
+ if relevant_labels := response.get("labels", []):
194
+ apply_and_check_labels(event, number, node_id, issue_type, username, relevant_labels, label_descriptions)
195
+
196
+ if first_comment := response.get("first_comment"):
197
+ print("Adding first interaction comment...")
198
+ time.sleep(1) # sleep to ensure label added first
199
+ event.add_comment(number, node_id, first_comment, issue_type)
200
+ return
201
+
202
+ # Handle issues and discussions (NOT PRs)
203
+ current_labels = (
204
+ []
205
+ if issue_type == "discussion"
206
+ else [label["name"].lower() for label in event.get_repo_data(f"issues/{number}/labels")]
207
+ )
208
+
209
+ relevant_labels = get_relevant_labels(issue_type, title, body, label_descriptions, current_labels)
210
+ apply_and_check_labels(event, number, node_id, issue_type, username, relevant_labels, label_descriptions)
372
211
 
373
212
  if action in {"opened", "created"}:
374
213
  custom_response = get_first_interaction_response(event, issue_type, title, body, username)
375
- add_comment(event, number, node_id, custom_response, issue_type)
214
+ event.add_comment(number, node_id, custom_response, issue_type)
376
215
 
377
216
 
378
217
  if __name__ == "__main__":