ultralytics-actions 0.0.70__py3-none-any.whl → 0.0.71__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.
actions/__init__.py CHANGED
@@ -22,4 +22,4 @@
22
22
  # ├── test_summarize_pr.py
23
23
  # └── ...
24
24
 
25
- __version__ = "0.0.70"
25
+ __version__ = "0.0.71"
@@ -4,13 +4,11 @@ import time
4
4
  from datetime import datetime
5
5
  from typing import Dict, List
6
6
 
7
- import requests
8
-
9
- from .utils import GITHUB_API_URL, Action, remove_html_comments
7
+ from .utils import GITHUB_API_URL, Action
10
8
 
11
9
  # Configuration
12
10
  RUN_CI_KEYWORD = "@ultralytics/run-ci" # and then to merge "@ultralytics/run-ci-and-merge"
13
- WORKFLOW_FILES = ["format.yml", "ci.yml", "docker.yml"]
11
+ WORKFLOW_FILES = ["ci.yml", "docker.yml"]
14
12
 
15
13
 
16
14
  def get_pr_branch(event) -> str:
@@ -27,11 +25,7 @@ def trigger_and_get_workflow_info(event, branch: str) -> List[Dict]:
27
25
 
28
26
  # Trigger all workflows
29
27
  for file in WORKFLOW_FILES:
30
- requests.post(
31
- f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/dispatches",
32
- json={"ref": branch},
33
- headers=event.headers,
34
- )
28
+ event.post(f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/dispatches", json={"ref": branch})
35
29
 
36
30
  # Wait for workflows to be created
37
31
  time.sleep(10)
@@ -39,7 +33,7 @@ def trigger_and_get_workflow_info(event, branch: str) -> List[Dict]:
39
33
  # Collect information about all workflows
40
34
  for file in WORKFLOW_FILES:
41
35
  # Get workflow name
42
- response = requests.get(f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}", headers=event.headers)
36
+ response = event.get(f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}")
43
37
  name = file.replace(".yml", "").title()
44
38
  if response.status_code == 200:
45
39
  name = response.json().get("name", name)
@@ -48,9 +42,8 @@ def trigger_and_get_workflow_info(event, branch: str) -> List[Dict]:
48
42
  run_url = f"https://github.com/{repo}/actions/workflows/{file}"
49
43
  run_number = None
50
44
 
51
- runs_response = requests.get(
45
+ runs_response = event.get(
52
46
  f"{GITHUB_API_URL}/repos/{repo}/actions/workflows/{file}/runs?branch={branch}&event=workflow_dispatch&per_page=1",
53
- headers=event.headers,
54
47
  )
55
48
 
56
49
  if runs_response.status_code == 200:
@@ -74,7 +67,7 @@ def update_comment(event, comment_body: str, triggered_actions: List[Dict], bran
74
67
  f"\n\n## ⚡ Actions Trigger\n\n"
75
68
  f"<sub>Made with ❤️ by [Ultralytics Actions](https://www.ultralytics.com/actions)<sub>\n\n"
76
69
  f"GitHub Actions below triggered via workflow dispatch on this "
77
- f"PR branch `{branch}` at {timestamp} with `@ultralytics/dispatch-actions command`:\n\n"
70
+ f"PR branch `{branch}` at {timestamp} with `{RUN_CI_KEYWORD}` command:\n\n"
78
71
  )
79
72
 
80
73
  for action in triggered_actions:
@@ -83,14 +76,7 @@ def update_comment(event, comment_body: str, triggered_actions: List[Dict], bran
83
76
 
84
77
  new_body = comment_body.replace(RUN_CI_KEYWORD, summary).strip()
85
78
  comment_id = event.event_data["comment"]["id"]
86
-
87
- response = requests.patch(
88
- f"{GITHUB_API_URL}/repos/{event.repository}/issues/comments/{comment_id}",
89
- json={"body": new_body},
90
- headers=event.headers,
91
- )
92
-
93
- return response.status_code == 200
79
+ event.patch(f"{GITHUB_API_URL}/repos/{event.repository}/issues/comments/{comment_id}", json={"body": new_body})
94
80
 
95
81
 
96
82
  def main(*args, **kwargs):
@@ -106,21 +92,24 @@ def main(*args, **kwargs):
106
92
  return
107
93
 
108
94
  # Get comment info
109
- comment_body = remove_html_comments(event.event_data["comment"].get("body", ""))
95
+ comment_body = event.event_data["comment"].get("body", "")
110
96
  username = event.event_data["comment"]["user"]["login"]
111
97
 
112
- # Check for trigger keyword and permissions
113
- if RUN_CI_KEYWORD not in comment_body or not event.is_org_member(username):
98
+ # Check for keyword without surrounding backticks to avoid triggering on replies
99
+ has_keyword = RUN_CI_KEYWORD in comment_body and comment_body.count(RUN_CI_KEYWORD) > comment_body.count(
100
+ f"`{RUN_CI_KEYWORD}`"
101
+ )
102
+ if not has_keyword or not event.is_org_member(username):
114
103
  return
115
104
 
116
105
  # Get branch, trigger workflows, and update comment
106
+ event.toggle_eyes_reaction(True)
117
107
  branch = get_pr_branch(event)
118
108
  print(f"Triggering workflows on branch: {branch}")
119
109
 
120
110
  triggered_actions = trigger_and_get_workflow_info(event, branch)
121
- success = update_comment(event, comment_body, triggered_actions, branch)
122
-
123
- print(f"Comment update {'succeeded' if success else 'failed'}.")
111
+ update_comment(event, comment_body, triggered_actions, branch)
112
+ event.toggle_eyes_reaction(False)
124
113
 
125
114
 
126
115
  if __name__ == "__main__":
@@ -3,14 +3,7 @@
3
3
  import os
4
4
  from typing import Dict, List, Tuple
5
5
 
6
- import requests
7
-
8
- from .utils import (
9
- GITHUB_API_URL,
10
- Action,
11
- get_completion,
12
- remove_html_comments,
13
- )
6
+ from .utils import GITHUB_API_URL, Action, get_completion, remove_html_comments
14
7
 
15
8
  # Environment variables
16
9
  BLOCK_USER = os.getenv("BLOCK_USER", "false").lower() == "true"
@@ -67,8 +60,7 @@ mutation($discussionId: ID!, $title: String!, $body: String!) {
67
60
  event.graphql_request(mutation, variables={"discussionId": node_id, "title": new_title, "body": new_body})
68
61
  else:
69
62
  url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}"
70
- r = requests.patch(url, json={"title": new_title, "body": new_body}, headers=event.headers)
71
- print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} update: {r.status_code}")
63
+ event.patch(url, json={"title": new_title, "body": new_body})
72
64
 
73
65
 
74
66
  def close_issue_pr(event, number: int, node_id: str, issue_type: str):
@@ -86,8 +78,7 @@ mutation($discussionId: ID!) {
86
78
  event.graphql_request(mutation, variables={"discussionId": node_id})
87
79
  else:
88
80
  url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}"
89
- r = requests.patch(url, json={"state": "closed"}, headers=event.headers)
90
- print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} close: {r.status_code}")
81
+ event.patch(url, json={"state": "closed"})
91
82
 
92
83
 
93
84
  def lock_issue_pr(event, number: int, node_id: str, issue_type: str):
@@ -107,15 +98,13 @@ mutation($lockableId: ID!, $lockReason: LockReason) {
107
98
  event.graphql_request(mutation, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
108
99
  else:
109
100
  url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/lock"
110
- r = requests.put(url, json={"lock_reason": "off-topic"}, headers=event.headers)
111
- print(f"{'Successful' if r.status_code in {200, 204} else 'Fail'} issue/PR #{number} lock: {r.status_code}")
101
+ event.put(url, json={"lock_reason": "off-topic"})
112
102
 
113
103
 
114
104
  def block_user(event, username: str):
115
105
  """Blocks a user from the organization using the GitHub API."""
116
106
  url = f"{GITHUB_API_URL}/orgs/{event.repository.split('/')[0]}/blocks/{username}"
117
- r = requests.put(url, headers=event.headers)
118
- print(f"{'Successful' if r.status_code == 204 else 'Fail'} user block for {username}: {r.status_code}")
107
+ event.put(url)
119
108
 
120
109
 
121
110
  def get_relevant_labels(
@@ -169,7 +158,6 @@ AVAILABLE LABELS:
169
158
 
170
159
  YOUR RESPONSE (label names only):
171
160
  """
172
- print(prompt) # for short-term debugging
173
161
  messages = [
174
162
  {
175
163
  "role": "system",
@@ -210,7 +198,6 @@ query($owner: String!, $name: String!) {
210
198
  label_map = {label["name"].lower(): label["id"] for label in all_labels}
211
199
  return [label_map.get(label.lower()) for label in labels if label.lower() in label_map]
212
200
  else:
213
- print(f"Failed to fetch labels: {result.get('errors', 'Unknown error')}")
214
201
  return []
215
202
 
216
203
 
@@ -220,7 +207,6 @@ def apply_labels(event, number: int, node_id: str, labels: List[str], issue_type
220
207
  create_alert_label(event)
221
208
 
222
209
  if issue_type == "discussion":
223
- print(f"Using node_id: {node_id}") # Debug print
224
210
  label_ids = get_label_ids(event, labels)
225
211
  if not label_ids:
226
212
  print("No valid labels to apply.")
@@ -238,25 +224,15 @@ mutation($labelableId: ID!, $labelIds: [ID!]!) {
238
224
  }
239
225
  """
240
226
  event.graphql_request(mutation, {"labelableId": node_id, "labelIds": label_ids})
241
- print(f"Successfully applied labels: {', '.join(labels)}")
242
227
  else:
243
228
  url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/labels"
244
- r = requests.post(url, json={"labels": labels}, headers=event.headers)
245
- print(f"{'Successful' if r.status_code == 200 else 'Fail'} apply labels {', '.join(labels)}: {r.status_code}")
229
+ event.post(url, json={"labels": labels})
246
230
 
247
231
 
248
232
  def create_alert_label(event):
249
233
  """Creates the 'Alert' label in the repository if it doesn't exist, with a red color and description."""
250
234
  alert_label = {"name": "Alert", "color": "FF0000", "description": "Potential spam, abuse, or off-topic."}
251
- requests.post(f"{GITHUB_API_URL}/repos/{event.repository}/labels", json=alert_label, headers=event.headers)
252
-
253
-
254
- def is_org_member(event, username: str) -> bool:
255
- """Checks if a user is a member of the organization using the GitHub API."""
256
- org_name = event.repository.split("/")[0]
257
- url = f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}"
258
- r = requests.get(url, headers=event.headers)
259
- return r.status_code == 204 # 204 means the user is a member
235
+ event.post(f"{GITHUB_API_URL}/repos/{event.repository}/labels", json=alert_label)
260
236
 
261
237
 
262
238
  def add_comment(event, number: int, node_id: str, comment: str, issue_type: str):
@@ -274,8 +250,7 @@ mutation($discussionId: ID!, $body: String!) {
274
250
  event.graphql_request(mutation, variables={"discussionId": node_id, "body": comment})
275
251
  else:
276
252
  url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/comments"
277
- r = requests.post(url, json={"body": comment}, headers=event.headers)
278
- print(f"{'Successful' if r.status_code in {200, 201} else 'Fail'} issue/PR #{number} comment: {r.status_code}")
253
+ event.post(url, json={"body": comment}, headers=event.headers)
279
254
 
280
255
 
281
256
  def get_first_interaction_response(event, issue_type: str, title: str, body: str, username: str) -> str:
@@ -361,7 +336,7 @@ EXAMPLE {issue_type.upper()} RESPONSE:
361
336
 
362
337
  YOUR {issue_type.upper()} RESPONSE:
363
338
  """
364
- print(f"\n\n{prompt}\n\n") # for debug
339
+ # print(f"\n\n{prompt}\n\n") # for debug
365
340
  messages = [
366
341
  {
367
342
  "role": "system",
@@ -384,7 +359,7 @@ def main(*args, **kwargs):
384
359
  current_labels = [label["name"].lower() for label in event.get_repo_data(f"issues/{number}/labels")]
385
360
  if relevant_labels := get_relevant_labels(issue_type, title, body, label_descriptions, current_labels):
386
361
  apply_labels(event, number, node_id, relevant_labels, issue_type)
387
- if "Alert" in relevant_labels and not is_org_member(event, username):
362
+ if "Alert" in relevant_labels and not event.is_org_member(username):
388
363
  update_issue_pr_content(event, number, node_id, issue_type)
389
364
  if issue_type != "pull request":
390
365
  close_issue_pr(event, number, node_id, issue_type)
actions/summarize_pr.py CHANGED
@@ -2,13 +2,7 @@
2
2
 
3
3
  import time
4
4
 
5
- import requests
6
-
7
- from .utils import (
8
- GITHUB_API_URL,
9
- Action,
10
- get_completion,
11
- )
5
+ from .utils import GITHUB_API_URL, GITHUB_GRAPHQL_URL, Action, get_completion
12
6
 
13
7
  # Constants
14
8
  SUMMARY_START = (
@@ -36,12 +30,12 @@ def generate_merge_message(pr_summary=None, pr_credit=None, pr_url=None):
36
30
  return get_completion(messages)
37
31
 
38
32
 
39
- def post_merge_message(pr_number, pr_url, repository, summary, pr_credit, headers):
33
+ def post_merge_message(event, summary, pr_credit):
40
34
  """Posts thank you message on PR after merge."""
35
+ pr_url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{event.pr['number']}"
36
+ comment_url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{event.pr['number']}/comments"
41
37
  message = generate_merge_message(summary, pr_credit, pr_url)
42
- comment_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{pr_number}/comments"
43
- response = requests.post(comment_url, json={"body": message}, headers=headers)
44
- return response.status_code == 201
38
+ event.post(comment_url, json={"body": message})
45
39
 
46
40
 
47
41
  def generate_issue_comment(pr_url, pr_summary, pr_credit, pr_title=""):
@@ -101,11 +95,12 @@ def generate_pr_summary(repository, diff_text):
101
95
  return SUMMARY_START + reply
102
96
 
103
97
 
104
- def update_pr_description(pr_url, new_summary, headers, max_retries=2):
98
+ def update_pr_description(event, new_summary, max_retries=2):
105
99
  """Updates PR description with new summary, retrying if description is None."""
106
100
  description = ""
101
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{event.pr['number']}"
107
102
  for i in range(max_retries + 1):
108
- description = requests.get(pr_url, headers=headers).json().get("body") or ""
103
+ description = event.get(url).json().get("body") or ""
109
104
  if description:
110
105
  break
111
106
  if i < max_retries:
@@ -122,11 +117,10 @@ def update_pr_description(pr_url, new_summary, headers, max_retries=2):
122
117
  updated_description = description + "\n\n" + new_summary
123
118
 
124
119
  # Update the PR description
125
- update_response = requests.patch(pr_url, json={"body": updated_description}, headers=headers)
126
- return update_response.status_code
120
+ event.patch(url, json={"body": updated_description})
127
121
 
128
122
 
129
- def label_fixed_issues(repository, pr_number, pr_summary, headers, action):
123
+ def label_fixed_issues(event, pr_summary):
130
124
  """Labels issues closed by PR when merged, notifies users, and returns PR contributors."""
131
125
  query = """
132
126
  query($owner: String!, $repo: String!, $pr_number: Int!) {
@@ -144,19 +138,16 @@ query($owner: String!, $repo: String!, $pr_number: Int!) {
144
138
  }
145
139
  }
146
140
  """
147
- owner, repo = repository.split("/")
148
- variables = {"owner": owner, "repo": repo, "pr_number": pr_number}
149
- graphql_url = "https://api.github.com/graphql"
150
- response = requests.post(graphql_url, json={"query": query, "variables": variables}, headers=headers)
151
-
141
+ owner, repo = event.repository.split("/")
142
+ variables = {"owner": owner, "repo": repo, "pr_number": event.pr["number"]}
143
+ response = event.post(GITHUB_GRAPHQL_URL, json={"query": query, "variables": variables})
152
144
  if response.status_code != 200:
153
- print(f"Failed to fetch linked issues. Status code: {response.status_code}")
154
- return None
145
+ return None # no linked issues
155
146
 
156
147
  try:
157
148
  data = response.json()["data"]["repository"]["pullRequest"]
158
149
  comments = data["reviews"]["nodes"] + data["comments"]["nodes"]
159
- token_username = action.get_username() # get GITHUB_TOKEN username
150
+ token_username = event.get_username() # get GITHUB_TOKEN username
160
151
  author = data["author"]["login"] if data["author"]["__typename"] != "Bot" else None
161
152
  pr_title = data.get("title", "")
162
153
 
@@ -188,22 +179,12 @@ query($owner: String!, $repo: String!, $pr_number: Int!) {
188
179
 
189
180
  # Update linked issues
190
181
  for issue in data["closingIssuesReferences"]["nodes"]:
191
- issue_number = issue["number"]
182
+ number = issue["number"]
192
183
  # Add fixed label
193
- label_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{issue_number}/labels"
194
- label_response = requests.post(label_url, json={"labels": ["fixed"]}, headers=headers)
184
+ event.post(f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/labels", json={"labels": ["fixed"]})
195
185
 
196
186
  # Add comment
197
- comment_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{issue_number}/comments"
198
- comment_response = requests.post(comment_url, json={"body": comment}, headers=headers)
199
-
200
- if label_response.status_code == 200 and comment_response.status_code == 201:
201
- print(f"Added 'fixed' label and comment to issue #{issue_number}")
202
- else:
203
- print(
204
- f"Failed to update issue #{issue_number}. Label status: {label_response.status_code}, "
205
- f"Comment status: {comment_response.status_code}"
206
- )
187
+ event.post(f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/comments", json={"body": comment})
207
188
 
208
189
  return pr_credit
209
190
  except KeyError as e:
@@ -211,44 +192,36 @@ query($owner: String!, $repo: String!, $pr_number: Int!) {
211
192
  return None
212
193
 
213
194
 
214
- def remove_todos_on_merge(pr_number, repository, headers):
195
+ def remove_pr_labels(event, labels=()):
215
196
  """Removes specified labels from PR."""
216
- for label in ["TODO"]: # Can be extended with more labels in the future
217
- requests.delete(f"{GITHUB_API_URL}/repos/{repository}/issues/{pr_number}/labels/{label}", headers=headers)
197
+ for label in labels: # Can be extended with more labels in the future
198
+ event.delete(f"{GITHUB_API_URL}/repos/{event.repository}/issues/{event.pr['number']}/labels/{label}")
218
199
 
219
200
 
220
201
  def main(*args, **kwargs):
221
202
  """Summarize a pull request and update its description with a summary."""
222
- action = Action(*args, **kwargs)
223
- headers = action.headers
224
- repository = action.repository
225
- pr_number = action.pr["number"]
226
- pr_url = f"{GITHUB_API_URL}/repos/{repository}/pulls/{pr_number}"
203
+ event = Action(*args, **kwargs)
227
204
 
228
- print(f"Retrieving diff for PR {pr_number}")
229
- diff = action.get_pr_diff()
205
+ print(f"Retrieving diff for PR {event.pr['number']}")
206
+ diff = event.get_pr_diff()
230
207
 
231
208
  # Generate PR summary
232
209
  print("Generating PR summary...")
233
- summary = generate_pr_summary(repository, diff)
210
+ summary = generate_pr_summary(event.repository, diff)
234
211
 
235
212
  # Update PR description
236
213
  print("Updating PR description...")
237
- status_code = update_pr_description(pr_url, summary, headers)
238
- if status_code == 200:
239
- print("PR description updated successfully.")
240
- else:
241
- print(f"Failed to update PR description. Status code: {status_code}")
214
+ update_pr_description(event, summary)
242
215
 
243
216
  # Update linked issues and post thank you message if merged
244
- if action.pr.get("merged"):
217
+ if event.pr.get("merged"):
245
218
  print("PR is merged, labeling fixed issues...")
246
- pr_credit = label_fixed_issues(repository, pr_number, summary, headers, action)
219
+ pr_credit = label_fixed_issues(event, summary)
247
220
  print("Removing TODO label from PR...")
248
- remove_todos_on_merge(pr_number, repository, headers)
221
+ remove_pr_labels(event, labels=["TODO"])
249
222
  if pr_credit:
250
223
  print("Posting PR author thank you message...")
251
- post_merge_message(pr_number, pr_url, repository, summary, pr_credit, headers)
224
+ post_merge_message(event, summary, pr_credit)
252
225
 
253
226
 
254
227
  if __name__ == "__main__":
@@ -6,36 +6,27 @@ import subprocess
6
6
  import time
7
7
  from datetime import datetime
8
8
 
9
- import requests
10
-
11
- from .utils import (
12
- GITHUB_API_URL,
13
- Action,
14
- get_completion,
15
- remove_html_comments,
16
- )
9
+ from .utils import GITHUB_API_URL, Action, get_completion, remove_html_comments
17
10
 
18
11
  # Environment variables
19
12
  CURRENT_TAG = os.getenv("CURRENT_TAG")
20
13
  PREVIOUS_TAG = os.getenv("PREVIOUS_TAG")
21
14
 
22
15
 
23
- def get_release_diff(repo_name: str, previous_tag: str, latest_tag: str, headers: dict) -> str:
16
+ def get_release_diff(event, previous_tag: str, latest_tag: str) -> str:
24
17
  """Retrieves the differences between two specified Git tags in a GitHub repository."""
25
- url = f"{GITHUB_API_URL}/repos/{repo_name}/compare/{previous_tag}...{latest_tag}"
26
- r = requests.get(url, headers=headers)
18
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/compare/{previous_tag}...{latest_tag}"
19
+ r = event.get(url, headers=event.headers_diff)
27
20
  return r.text if r.status_code == 200 else f"Failed to get diff: {r.content}"
28
21
 
29
22
 
30
- def get_prs_between_tags(repo_name: str, previous_tag: str, latest_tag: str, headers: dict) -> list:
23
+ def get_prs_between_tags(event, previous_tag: str, latest_tag: str) -> list:
31
24
  """Retrieves and processes pull requests merged between two specified tags in a GitHub repository."""
32
- url = f"{GITHUB_API_URL}/repos/{repo_name}/compare/{previous_tag}...{latest_tag}"
33
- r = requests.get(url, headers=headers)
34
- r.raise_for_status()
25
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/compare/{previous_tag}...{latest_tag}"
26
+ r = event.get(url)
35
27
 
36
28
  data = r.json()
37
29
  pr_numbers = set()
38
-
39
30
  for commit in data["commits"]:
40
31
  pr_matches = re.findall(r"#(\d+)", commit["commit"]["message"])
41
32
  pr_numbers.update(pr_matches)
@@ -43,8 +34,8 @@ def get_prs_between_tags(repo_name: str, previous_tag: str, latest_tag: str, hea
43
34
  prs = []
44
35
  time.sleep(10) # sleep 10 seconds to allow final PR summary to update on merge
45
36
  for pr_number in sorted(pr_numbers): # earliest to latest
46
- pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
47
- pr_response = requests.get(pr_url, headers=headers)
37
+ pr_url = f"{GITHUB_API_URL}/repos/{event.repository}/pulls/{pr_number}"
38
+ pr_response = event.get(pr_url)
48
39
  if pr_response.status_code == 200:
49
40
  pr_data = pr_response.json()
50
41
  prs.append(
@@ -64,14 +55,14 @@ def get_prs_between_tags(repo_name: str, previous_tag: str, latest_tag: str, hea
64
55
  return prs
65
56
 
66
57
 
67
- def get_new_contributors(repo: str, prs: list, headers: dict) -> set:
58
+ def get_new_contributors(event, prs: list) -> set:
68
59
  """Identify new contributors who made their first merged PR in the current release."""
69
60
  new_contributors = set()
70
61
  for pr in prs:
71
62
  author = pr["author"]
72
63
  # Check if this is the author's first contribution
73
- url = f"{GITHUB_API_URL}/search/issues?q=repo:{repo}+author:{author}+is:pr+is:merged&sort=created&order=asc"
74
- r = requests.get(url, headers=headers)
64
+ url = f"{GITHUB_API_URL}/search/issues?q=repo:{event.repository}+author:{author}+is:pr+is:merged&sort=created&order=asc"
65
+ r = event.get(url)
75
66
  if r.status_code == 200:
76
67
  data = r.json()
77
68
  if data["total_count"] > 0:
@@ -82,7 +73,11 @@ def get_new_contributors(repo: str, prs: list, headers: dict) -> set:
82
73
 
83
74
 
84
75
  def generate_release_summary(
85
- diff: str, prs: list, latest_tag: str, previous_tag: str, repo_name: str, headers: dict
76
+ event,
77
+ diff: str,
78
+ prs: list,
79
+ latest_tag: str,
80
+ previous_tag: str,
86
81
  ) -> str:
87
82
  """Generate a concise release summary with key changes, purpose, and impact for a new Ultralytics version."""
88
83
  pr_summaries = "\n\n".join(
@@ -99,7 +94,7 @@ def generate_release_summary(
99
94
  whats_changed = "\n".join([f"* {pr['title']} by @{pr['author']} in {pr['html_url']}" for pr in prs])
100
95
 
101
96
  # Generate New Contributors section
102
- new_contributors = get_new_contributors(repo_name, prs, headers)
97
+ new_contributors = get_new_contributors(event, prs)
103
98
  new_contributors_section = (
104
99
  "\n## New Contributors\n"
105
100
  + "\n".join(
@@ -112,7 +107,7 @@ def generate_release_summary(
112
107
  else ""
113
108
  )
114
109
 
115
- full_changelog = f"https://github.com/{repo_name}/compare/{previous_tag}...{latest_tag}"
110
+ full_changelog = f"https://github.com/{event.repository}/compare/{previous_tag}...{latest_tag}"
116
111
  release_suffix = (
117
112
  f"\n\n## What's Changed\n{whats_changed}\n{new_contributors_section}\n\n**Full Changelog**: {full_changelog}\n"
118
113
  )
@@ -133,16 +128,15 @@ def generate_release_summary(
133
128
  f"Here's the release diff:\n\n{diff[:300000]}",
134
129
  },
135
130
  ]
136
- print(messages[-1]["content"]) # for debug
131
+ # print(messages[-1]["content"]) # for debug
137
132
  return get_completion(messages, temperature=0.2) + release_suffix
138
133
 
139
134
 
140
- def create_github_release(repo_name: str, tag_name: str, name: str, body: str, headers: dict) -> int:
135
+ def create_github_release(event, tag_name: str, name: str, body: str):
141
136
  """Creates a GitHub release with specified tag, name, and body content for the given repository."""
142
- url = f"{GITHUB_API_URL}/repos/{repo_name}/releases"
137
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/releases"
143
138
  data = {"tag_name": tag_name, "name": name, "body": body, "draft": False, "prerelease": False}
144
- r = requests.post(url, headers=headers, json=data)
145
- return r.status_code
139
+ event.post(url, json=data)
146
140
 
147
141
 
148
142
  def get_previous_tag() -> str:
@@ -157,22 +151,22 @@ def get_previous_tag() -> str:
157
151
 
158
152
  def main(*args, **kwargs):
159
153
  """Automates generating and publishing a GitHub release summary from PRs and commit differences."""
160
- action = Action(*args, **kwargs)
154
+ event = Action(*args, **kwargs)
161
155
 
162
- if not all([action.token, CURRENT_TAG]):
156
+ if not all([event.token, CURRENT_TAG]):
163
157
  raise ValueError("One or more required environment variables are missing.")
164
158
 
165
159
  previous_tag = PREVIOUS_TAG or get_previous_tag()
166
160
 
167
161
  # Get the diff between the tags
168
- diff = get_release_diff(action.repository, previous_tag, CURRENT_TAG, action.headers_diff)
162
+ diff = get_release_diff(event, previous_tag, CURRENT_TAG)
169
163
 
170
164
  # Get PRs merged between the tags
171
- prs = get_prs_between_tags(action.repository, previous_tag, CURRENT_TAG, action.headers)
165
+ prs = get_prs_between_tags(event, previous_tag, CURRENT_TAG)
172
166
 
173
167
  # Generate release summary
174
168
  try:
175
- summary = generate_release_summary(diff, prs, CURRENT_TAG, previous_tag, action.repository, action.headers)
169
+ summary = generate_release_summary(event, diff, prs, CURRENT_TAG, previous_tag)
176
170
  except Exception as e:
177
171
  print(f"Failed to generate summary: {str(e)}")
178
172
  summary = "Failed to generate summary."
@@ -183,11 +177,7 @@ def main(*args, **kwargs):
183
177
 
184
178
  # Create the release on GitHub
185
179
  msg = f"{CURRENT_TAG} - {commit_message}"
186
- status_code = create_github_release(action.repository, CURRENT_TAG, msg, summary, action.headers)
187
- if status_code == 201:
188
- print(f"Successfully created release {CURRENT_TAG}")
189
- else:
190
- print(f"Failed to create release {CURRENT_TAG}. Status code: {status_code}")
180
+ create_github_release(event, CURRENT_TAG, msg, summary)
191
181
 
192
182
 
193
183
  if __name__ == "__main__":
actions/utils/__init__.py CHANGED
@@ -8,11 +8,12 @@ from .common_utils import (
8
8
  allow_redirect,
9
9
  remove_html_comments,
10
10
  )
11
- from .github_utils import GITHUB_API_URL, Action, check_pypi_version, ultralytics_actions_info
11
+ from .github_utils import GITHUB_API_URL, GITHUB_GRAPHQL_URL, Action, check_pypi_version, ultralytics_actions_info
12
12
  from .openai_utils import get_completion
13
13
 
14
14
  __all__ = (
15
15
  "GITHUB_API_URL",
16
+ "GITHUB_GRAPHQL_URL",
16
17
  "REQUESTS_HEADERS",
17
18
  "URL_IGNORE_LIST",
18
19
  "REDIRECT_START_IGNORE_LIST",
@@ -156,11 +156,10 @@ def brave_search(query, api_key, count=5):
156
156
  """Search for alternative URLs using Brave Search API."""
157
157
  if not api_key:
158
158
  return
159
- headers = {"X-Subscription-Token": api_key, "Accept": "application/json"}
160
159
  if len(query) > 400:
161
160
  print(f"WARNING ⚠️ Brave search query length {len(query)} exceed limit of 400 characters, truncating.")
162
161
  url = f"https://api.search.brave.com/res/v1/web/search?q={parse.quote(query.strip()[:400])}&count={count}"
163
- response = requests.get(url, headers=headers)
162
+ response = requests.get(url, headers={"X-Subscription-Token": api_key, "Accept": "application/json"})
164
163
  data = response.json() if response.status_code == 200 else {}
165
164
  results = data.get("web", {}).get("results", []) if data else []
166
165
  return [result.get("url") for result in results if result.get("url")]
@@ -9,6 +9,7 @@ import requests
9
9
  from actions import __version__
10
10
 
11
11
  GITHUB_API_URL = "https://api.github.com"
12
+ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
12
13
 
13
14
 
14
15
  class Action:
@@ -19,16 +20,69 @@ class Action:
19
20
  token: str = None,
20
21
  event_name: str = None,
21
22
  event_data: dict = None,
23
+ verbose: bool = True,
22
24
  ):
23
25
  """Initializes a GitHub Actions API handler with token and event data for processing events."""
24
26
  self.token = token or os.getenv("GITHUB_TOKEN")
25
27
  self.event_name = event_name or os.getenv("GITHUB_EVENT_NAME")
26
28
  self.event_data = event_data or self._load_event_data(os.getenv("GITHUB_EVENT_PATH"))
29
+ self._default_status = {
30
+ "get": [200],
31
+ "post": [200, 201],
32
+ "put": [200, 201, 204],
33
+ "patch": [200],
34
+ "delete": [200, 204],
35
+ }
27
36
 
28
37
  self.pr = self.event_data.get("pull_request", {})
29
38
  self.repository = self.event_data.get("repository", {}).get("full_name")
30
- self.headers = {"Authorization": f"token {self.token}", "Accept": "application/vnd.github.v3+json"}
31
- self.headers_diff = {"Authorization": f"token {self.token}", "Accept": "application/vnd.github.v3.diff"}
39
+ self.headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github+json"}
40
+ self.headers_diff = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github.v3.diff"}
41
+ self.eyes_reaction_id = None
42
+ self.verbose = verbose
43
+
44
+ def _request(self, method: str, url: str, headers=None, expected_status=None, **kwargs):
45
+ """Unified request handler with error checking."""
46
+ headers = headers or self.headers
47
+ expected_status = expected_status or self._default_status[method.lower()]
48
+
49
+ response = getattr(requests, method)(url, headers=headers, **kwargs)
50
+ status = response.status_code
51
+ success = status in expected_status
52
+
53
+ if self.verbose:
54
+ print(f"{'✓' if success else '✗'} {method.upper()} {url} → {status}")
55
+ if not success:
56
+ try:
57
+ error_detail = response.json()
58
+ print(f" Error: {error_detail.get('message', 'Unknown error')}")
59
+ except:
60
+ print(f" Error: {response.text[:100]}...")
61
+
62
+ if not success:
63
+ response.raise_for_status()
64
+
65
+ return response
66
+
67
+ def get(self, url, **kwargs):
68
+ """Performs GET request with error handling."""
69
+ return self._request("get", url, **kwargs)
70
+
71
+ def post(self, url, **kwargs):
72
+ """Performs POST request with error handling."""
73
+ return self._request("post", url, **kwargs)
74
+
75
+ def put(self, url, **kwargs):
76
+ """Performs PUT request with error handling."""
77
+ return self._request("put", url, **kwargs)
78
+
79
+ def patch(self, url, **kwargs):
80
+ """Performs PATCH request with error handling."""
81
+ return self._request("patch", url, **kwargs)
82
+
83
+ def delete(self, url, **kwargs):
84
+ """Performs DELETE request with error handling."""
85
+ return self._request("delete", url, **kwargs)
32
86
 
33
87
  @staticmethod
34
88
  def _load_event_data(event_path: str) -> dict:
@@ -39,50 +93,57 @@ class Action:
39
93
 
40
94
  def get_username(self) -> str | None:
41
95
  """Gets username associated with the GitHub token."""
42
- query = "query { viewer { login } }"
43
- response = requests.post(f"{GITHUB_API_URL}/graphql", json={"query": query}, headers=self.headers)
44
- if response.status_code != 200:
45
- print(f"Failed to fetch authenticated user. Status code: {response.status_code}")
46
- return None
47
- try:
48
- return response.json()["data"]["viewer"]["login"]
49
- except KeyError as e:
50
- print(f"Error parsing authenticated user response: {e}")
51
- return None
96
+ response = self.post(GITHUB_GRAPHQL_URL, json={"query": "query { viewer { login } }"})
97
+ if response.status_code == 200:
98
+ try:
99
+ return response.json()["data"]["viewer"]["login"]
100
+ except KeyError as e:
101
+ print(f"Error parsing authenticated user response: {e}")
102
+ return None
52
103
 
53
104
  def is_org_member(self, username: str) -> bool:
54
105
  """Checks if a user is a member of the organization using the GitHub API."""
55
106
  org_name = self.repository.split("/")[0]
56
- url = f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}"
57
- r = requests.get(url, headers=self.headers)
58
- return r.status_code == 204 # 204 means the user is a member
107
+ response = self.get(f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}")
108
+ return response.status_code == 204 # 204 means the user is a member
59
109
 
60
110
  def get_pr_diff(self) -> str:
61
111
  """Retrieves the diff content for a specified pull request."""
62
112
  url = f"{GITHUB_API_URL}/repos/{self.repository}/pulls/{self.pr.get('number')}"
63
- r = requests.get(url, headers=self.headers_diff)
64
- return r.text if r.status_code == 200 else ""
113
+ response = self.get(url, headers=self.headers_diff)
114
+ return response.text if response.status_code == 200 else ""
65
115
 
66
116
  def get_repo_data(self, endpoint: str) -> dict:
67
117
  """Fetches repository data from a specified endpoint."""
68
- r = requests.get(f"{GITHUB_API_URL}/repos/{self.repository}/{endpoint}", headers=self.headers)
69
- r.raise_for_status()
70
- return r.json()
118
+ response = self.get(f"{GITHUB_API_URL}/repos/{self.repository}/{endpoint}")
119
+ return response.json()
120
+
121
+ def toggle_eyes_reaction(self, add: bool = True) -> None:
122
+ """Adds or removes eyes emoji reaction."""
123
+ if self.event_name in ["pull_request", "pull_request_target"]:
124
+ id = self.pr.get("number")
125
+ elif self.event_name == "issue_comment":
126
+ id = f"comments/{self.event_data.get('comment', {}).get('id')}"
127
+ else:
128
+ id = self.event_data.get("issue", {}).get("number")
129
+ if not id:
130
+ return
131
+ url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{id}/reactions"
132
+
133
+ if add:
134
+ response = self.post(url, json={"content": "eyes"})
135
+ if response.status_code == 201:
136
+ self.eyes_reaction_id = response.json().get("id")
137
+ elif self.eyes_reaction_id:
138
+ self.delete(f"{url}/{self.eyes_reaction_id}")
139
+ self.eyes_reaction_id = None
71
140
 
72
141
  def graphql_request(self, query: str, variables: dict = None) -> dict:
73
142
  """Executes a GraphQL query against the GitHub API."""
74
- headers = {
75
- "Authorization": f"Bearer {self.token}",
76
- "Content-Type": "application/json",
77
- "Accept": "application/vnd.github.v4+json",
78
- }
79
- r = requests.post(f"{GITHUB_API_URL}/graphql", json={"query": query, "variables": variables}, headers=headers)
80
- r.raise_for_status()
143
+ r = self.post(GITHUB_GRAPHQL_URL, json={"query": query, "variables": variables})
81
144
  result = r.json()
82
- success = "data" in result and not result.get("errors")
83
- print(
84
- f"{'Successful' if success else 'Failed'} discussion GraphQL request: {result.get('errors', 'No errors')}"
85
- )
145
+ if "data" not in result or result.get("errors"):
146
+ print(result.get("errors"))
86
147
  return result
87
148
 
88
149
  def print_info(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.0.70
3
+ Version: 0.0.71
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>
@@ -0,0 +1,16 @@
1
+ actions/__init__.py,sha256=KuXt92GxOFYtPimGijr5CdEXsLmxIG4MQWAaAgFd3ew,742
2
+ actions/dispatch_actions.py,sha256=vbA4w_B8vMXMen__ck2WoDsUFCELjXOQbpLzZCmqTXg,4240
3
+ actions/first_interaction.py,sha256=Yagh38abX638DNYr18HoiEEfCZOJfrqObhJIff54Sx0,16350
4
+ actions/summarize_pr.py,sha256=NCaDSbw4PVoRbPJzji_Ua2HadI2pn7QOE_dy3VK9_cc,10463
5
+ actions/summarize_release.py,sha256=OncODHx7XsmB-nPf-B1tnxUTcaJx6hM4JAMa9frypzM,7922
6
+ actions/update_markdown_code_blocks.py,sha256=9PL7YIQfApRNAa0que2hYHv7umGZTZoHlblesB0xFj4,8587
7
+ actions/utils/__init__.py,sha256=TXYvhFgDeAnosePM4jfOrEd6PlC7tWC-WMOgCB_T6Tw,728
8
+ actions/utils/common_utils.py,sha256=2eNwGJFigl9bBXcyWzdr8mr97Lrx7zFKWIFYugZcUJw,11736
9
+ actions/utils/github_utils.py,sha256=bpFMbpzPvtBiqtNvzISFkCp8_7EboMiD3aMA8RG_tTs,9785
10
+ actions/utils/openai_utils.py,sha256=txbsEPQnIOieejatBuE6Yk7xR1fQ0erWOEs6cYgUQX4,2943
11
+ ultralytics_actions-0.0.71.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
12
+ ultralytics_actions-0.0.71.dist-info/METADATA,sha256=Q0n2vt5uFAB2167ciNanCwJk86uLv_6sry9l8mKgvNU,10930
13
+ ultralytics_actions-0.0.71.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
14
+ ultralytics_actions-0.0.71.dist-info/entry_points.txt,sha256=GowvOFplj0C7JmsjbKcbpgLpdf2r921pcaOQkAHWZRA,378
15
+ ultralytics_actions-0.0.71.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
16
+ ultralytics_actions-0.0.71.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.1)
2
+ Generator: setuptools (80.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- actions/__init__.py,sha256=jkNfCMkrM58RMv6ijJXC4tASCnP7q1VvKsh1yKtLQn4,742
2
- actions/dispatch_actions.py,sha256=plfDaFdfBPldqI1uUQPfgSeIDaRTKbkzkXnsYbGkmwI,4415
3
- actions/first_interaction.py,sha256=1_WvQHCi5RWaSfyi49ClF2Zk_3CKGjFnZqz6FlxPRAc,17868
4
- actions/summarize_pr.py,sha256=BKttOq-MGaanVaChLU5B1ewKUA8K6S05Cy3FQtyRmxU,11681
5
- actions/summarize_release.py,sha256=tov6qsYGC68lfobvkwVyoWZBGtJ598G0m097n4Ydzvo,8472
6
- actions/update_markdown_code_blocks.py,sha256=9PL7YIQfApRNAa0que2hYHv7umGZTZoHlblesB0xFj4,8587
7
- actions/utils/__init__.py,sha256=ZE0RmC9qOCt9TUhvORd6uVhbxOKVFWJDobR454v55_M,682
8
- actions/utils/common_utils.py,sha256=BCqB2noJRXxXLAH8rmZ7c4ss1WjFXDC9OPPPFX5OuRU,11758
9
- actions/utils/github_utils.py,sha256=EZIATz5OSrNgdBrLFxL-9KRxn0i_Ph5-ZXHhhcIrtPE,7495
10
- actions/utils/openai_utils.py,sha256=txbsEPQnIOieejatBuE6Yk7xR1fQ0erWOEs6cYgUQX4,2943
11
- ultralytics_actions-0.0.70.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
12
- ultralytics_actions-0.0.70.dist-info/METADATA,sha256=c9aZUeDQntfNp4gptnUlEVAxDVoEieK1h2qcGQ88jDs,10930
13
- ultralytics_actions-0.0.70.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
14
- ultralytics_actions-0.0.70.dist-info/entry_points.txt,sha256=GowvOFplj0C7JmsjbKcbpgLpdf2r921pcaOQkAHWZRA,378
15
- ultralytics_actions-0.0.70.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
16
- ultralytics_actions-0.0.70.dist-info/RECORD,,