ultralytics-actions 0.0.30__tar.gz → 0.0.36__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.
Files changed (24) hide show
  1. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/PKG-INFO +3 -4
  2. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/__init__.py +1 -1
  3. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/first_interaction.py +70 -73
  4. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/summarize_pr.py +72 -70
  5. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/summarize_release.py +23 -21
  6. ultralytics_actions-0.0.36/actions/utils/__init__.py +19 -0
  7. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/utils/common_utils.py +19 -14
  8. ultralytics_actions-0.0.36/actions/utils/github_utils.py +163 -0
  9. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/utils/openai_utils.py +2 -2
  10. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/pyproject.toml +15 -3
  11. ultralytics_actions-0.0.36/tests/test_urls.py +15 -0
  12. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/ultralytics_actions.egg-info/PKG-INFO +3 -4
  13. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/ultralytics_actions.egg-info/SOURCES.txt +1 -0
  14. ultralytics_actions-0.0.36/ultralytics_actions.egg-info/requires.txt +6 -0
  15. ultralytics_actions-0.0.30/actions/utils/__init__.py +0 -45
  16. ultralytics_actions-0.0.30/actions/utils/github_utils.py +0 -157
  17. ultralytics_actions-0.0.30/ultralytics_actions.egg-info/requires.txt +0 -7
  18. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/LICENSE +0 -0
  19. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/README.md +0 -0
  20. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/actions/update_markdown_code_blocks.py +0 -0
  21. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/setup.cfg +0 -0
  22. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
  23. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/ultralytics_actions.egg-info/entry_points.txt +0 -0
  24. {ultralytics_actions-0.0.30 → ultralytics_actions-0.0.36}/ultralytics_actions.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ultralytics-actions
3
- Version: 0.0.30
3
+ Version: 0.0.36
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>
@@ -28,12 +28,11 @@ Classifier: Operating System :: OS Independent
28
28
  Requires-Python: >=3.8
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
- Requires-Dist: requests>=2.26.0
32
- Requires-Dist: ruff>=0.1.6
31
+ Requires-Dist: requests>=2.32.3
32
+ Requires-Dist: ruff>=0.8.4
33
33
  Requires-Dist: docformatter>=1.7.5
34
34
  Provides-Extra: dev
35
35
  Requires-Dist: pytest; extra == "dev"
36
- Requires-Dist: black; extra == "dev"
37
36
 
38
37
  <a href="https://www.ultralytics.com/" target="_blank"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
39
38
 
@@ -22,4 +22,4 @@
22
22
  # ├── test_summarize_pr.py
23
23
  # └── ...
24
24
 
25
- __version__ = "0.0.30"
25
+ __version__ = "0.0.36"
@@ -6,15 +6,9 @@ from typing import Dict, List, Tuple
6
6
  import requests
7
7
 
8
8
  from .utils import (
9
- EVENT_DATA,
10
9
  GITHUB_API_URL,
11
- GITHUB_EVENT_NAME,
12
- GITHUB_HEADERS,
13
- GITHUB_REPOSITORY,
10
+ Action,
14
11
  get_completion,
15
- get_github_data,
16
- get_pr_diff,
17
- graphql_request,
18
12
  remove_html_comments,
19
13
  )
20
14
 
@@ -22,21 +16,23 @@ from .utils import (
22
16
  BLOCK_USER = os.getenv("BLOCK_USER", "false").lower() == "true"
23
17
 
24
18
 
25
- def get_event_content() -> Tuple[int, str, str, str, str, str, str]:
19
+ def get_event_content(event) -> Tuple[int, str, str, str, str, str, str]:
26
20
  """Extracts key information from GitHub event data for issues, pull requests, or discussions."""
27
- action = EVENT_DATA["action"] # 'opened', 'closed', 'created' (discussion), etc.
28
- if GITHUB_EVENT_NAME == "issues":
29
- item = EVENT_DATA["issue"]
21
+ data = event.event_data
22
+ name = event.event_name
23
+ action = data["action"] # 'opened', 'closed', 'created' (discussion), etc.
24
+ if name == "issues":
25
+ item = data["issue"]
30
26
  issue_type = "issue"
31
- elif GITHUB_EVENT_NAME in ["pull_request", "pull_request_target"]:
32
- pr_number = EVENT_DATA["pull_request"]["number"]
33
- item = get_github_data(f"pulls/{pr_number}")
27
+ elif name in ["pull_request", "pull_request_target"]:
28
+ pr_number = data["pull_request"]["number"]
29
+ item = event.get_repo_data(f"pulls/{pr_number}")
34
30
  issue_type = "pull request"
35
- elif GITHUB_EVENT_NAME == "discussion":
36
- item = EVENT_DATA["discussion"]
31
+ elif name == "discussion":
32
+ item = data["discussion"]
37
33
  issue_type = "discussion"
38
34
  else:
39
- raise ValueError(f"Unsupported event type: {GITHUB_EVENT_NAME}")
35
+ raise ValueError(f"Unsupported event type: {name}")
40
36
 
41
37
  number = item["number"]
42
38
  node_id = item.get("node_id") or item.get("id")
@@ -46,7 +42,7 @@ def get_event_content() -> Tuple[int, str, str, str, str, str, str]:
46
42
  return number, node_id, title, body, username, issue_type, action
47
43
 
48
44
 
49
- def update_issue_pr_content(number: int, node_id: str, issue_type: str):
45
+ def update_issue_pr_content(event, number: int, node_id: str, issue_type: str):
50
46
  """Updates the title and body of an issue, pull request, or discussion with predefined content."""
51
47
  new_title = "Content Under Review"
52
48
  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:
@@ -68,14 +64,14 @@ mutation($discussionId: ID!, $title: String!, $body: String!) {
68
64
  }
69
65
  }
70
66
  """
71
- graphql_request(mutation, variables={"discussionId": node_id, "title": new_title, "body": new_body})
67
+ event.graphql_request(mutation, variables={"discussionId": node_id, "title": new_title, "body": new_body})
72
68
  else:
73
- url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{number}"
74
- r = requests.patch(url, json={"title": new_title, "body": new_body}, headers=GITHUB_HEADERS)
69
+ 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)
75
71
  print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} update: {r.status_code}")
76
72
 
77
73
 
78
- def close_issue_pr(number: int, node_id: str, issue_type: str):
74
+ def close_issue_pr(event, number: int, node_id: str, issue_type: str):
79
75
  """Closes the specified issue, pull request, or discussion using the GitHub API."""
80
76
  if issue_type == "discussion":
81
77
  mutation = """
@@ -87,14 +83,14 @@ mutation($discussionId: ID!) {
87
83
  }
88
84
  }
89
85
  """
90
- graphql_request(mutation, variables={"discussionId": node_id})
86
+ event.graphql_request(mutation, variables={"discussionId": node_id})
91
87
  else:
92
- url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{number}"
93
- r = requests.patch(url, json={"state": "closed"}, headers=GITHUB_HEADERS)
88
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}"
89
+ r = requests.patch(url, json={"state": "closed"}, headers=event.headers)
94
90
  print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} close: {r.status_code}")
95
91
 
96
92
 
97
- def lock_issue_pr(number: int, node_id: str, issue_type: str):
93
+ def lock_issue_pr(event, number: int, node_id: str, issue_type: str):
98
94
  """Locks an issue, pull request, or discussion to prevent further interactions."""
99
95
  if issue_type == "discussion":
100
96
  mutation = """
@@ -108,17 +104,17 @@ mutation($lockableId: ID!, $lockReason: LockReason) {
108
104
  }
109
105
  }
110
106
  """
111
- graphql_request(mutation, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
107
+ event.graphql_request(mutation, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
112
108
  else:
113
- url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{number}/lock"
114
- r = requests.put(url, json={"lock_reason": "off-topic"}, headers=GITHUB_HEADERS)
109
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/lock"
110
+ r = requests.put(url, json={"lock_reason": "off-topic"}, headers=event.headers)
115
111
  print(f"{'Successful' if r.status_code in {200, 204} else 'Fail'} issue/PR #{number} lock: {r.status_code}")
116
112
 
117
113
 
118
- def block_user(username: str):
114
+ def block_user(event, username: str):
119
115
  """Blocks a user from the organization using the GitHub API."""
120
- url = f"{GITHUB_API_URL}/orgs/{GITHUB_REPOSITORY.split('/')[0]}/blocks/{username}"
121
- r = requests.put(url, headers=GITHUB_HEADERS)
116
+ url = f"{GITHUB_API_URL}/orgs/{event.repository.split('/')[0]}/blocks/{username}"
117
+ r = requests.put(url, headers=event.headers)
122
118
  print(f"{'Successful' if r.status_code == 204 else 'Fail'} user block for {username}: {r.status_code}")
123
119
 
124
120
 
@@ -167,7 +163,7 @@ YOUR RESPONSE (label names only):
167
163
  messages = [
168
164
  {
169
165
  "role": "system",
170
- "content": "You are a helpful assistant that labels GitHub issues, pull requests, and discussions.",
166
+ "content": "You are an Ultralytics AI assistant that labels GitHub issues, PRs, and discussions.",
171
167
  },
172
168
  {"role": "user", "content": prompt},
173
169
  ]
@@ -183,7 +179,7 @@ YOUR RESPONSE (label names only):
183
179
  ]
184
180
 
185
181
 
186
- def get_label_ids(labels: List[str]) -> List[str]:
182
+ def get_label_ids(event, labels: List[str]) -> List[str]:
187
183
  """Retrieves GitHub label IDs for a list of label names using the GraphQL API."""
188
184
  query = """
189
185
  query($owner: String!, $name: String!) {
@@ -197,8 +193,8 @@ query($owner: String!, $name: String!) {
197
193
  }
198
194
  }
199
195
  """
200
- owner, repo = GITHUB_REPOSITORY.split("/")
201
- result = graphql_request(query, variables={"owner": owner, "name": repo})
196
+ owner, repo = event.repository.split("/")
197
+ result = event.graphql_request(query, variables={"owner": owner, "name": repo})
202
198
  if "data" in result and "repository" in result["data"]:
203
199
  all_labels = result["data"]["repository"]["labels"]["nodes"]
204
200
  label_map = {label["name"].lower(): label["id"] for label in all_labels}
@@ -208,14 +204,14 @@ query($owner: String!, $name: String!) {
208
204
  return []
209
205
 
210
206
 
211
- def apply_labels(number: int, node_id: str, labels: List[str], issue_type: str):
207
+ def apply_labels(event, number: int, node_id: str, labels: List[str], issue_type: str):
212
208
  """Applies specified labels to a GitHub issue, pull request, or discussion using the appropriate API."""
213
209
  if "Alert" in labels:
214
- create_alert_label()
210
+ create_alert_label(event)
215
211
 
216
212
  if issue_type == "discussion":
217
213
  print(f"Using node_id: {node_id}") # Debug print
218
- label_ids = get_label_ids(labels)
214
+ label_ids = get_label_ids(event, labels)
219
215
  if not label_ids:
220
216
  print("No valid labels to apply.")
221
217
  return
@@ -231,29 +227,29 @@ mutation($labelableId: ID!, $labelIds: [ID!]!) {
231
227
  }
232
228
  }
233
229
  """
234
- graphql_request(mutation, {"labelableId": node_id, "labelIds": label_ids})
230
+ event.graphql_request(mutation, {"labelableId": node_id, "labelIds": label_ids})
235
231
  print(f"Successfully applied labels: {', '.join(labels)}")
236
232
  else:
237
- url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{number}/labels"
238
- r = requests.post(url, json={"labels": labels}, headers=GITHUB_HEADERS)
233
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/labels"
234
+ r = requests.post(url, json={"labels": labels}, headers=event.headers)
239
235
  print(f"{'Successful' if r.status_code == 200 else 'Fail'} apply labels {', '.join(labels)}: {r.status_code}")
240
236
 
241
237
 
242
- def create_alert_label():
238
+ def create_alert_label(event):
243
239
  """Creates the 'Alert' label in the repository if it doesn't exist, with a red color and description."""
244
240
  alert_label = {"name": "Alert", "color": "FF0000", "description": "Potential spam, abuse, or off-topic."}
245
- requests.post(f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/labels", json=alert_label, headers=GITHUB_HEADERS)
241
+ requests.post(f"{GITHUB_API_URL}/repos/{event.repository}/labels", json=alert_label, headers=event.headers)
246
242
 
247
243
 
248
- def is_org_member(username: str) -> bool:
244
+ def is_org_member(event, username: str) -> bool:
249
245
  """Checks if a user is a member of the organization using the GitHub API."""
250
- org_name = GITHUB_REPOSITORY.split("/")[0]
246
+ org_name = event.repository.split("/")[0]
251
247
  url = f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}"
252
- r = requests.get(url, headers=GITHUB_HEADERS)
248
+ r = requests.get(url, headers=event.headers)
253
249
  return r.status_code == 204 # 204 means the user is a member
254
250
 
255
251
 
256
- def add_comment(number: int, node_id: str, comment: str, issue_type: str):
252
+ def add_comment(event, number: int, node_id: str, comment: str, issue_type: str):
257
253
  """Adds a comment to the specified issue, pull request, or discussion using the GitHub API."""
258
254
  if issue_type == "discussion":
259
255
  mutation = """
@@ -265,17 +261,17 @@ mutation($discussionId: ID!, $body: String!) {
265
261
  }
266
262
  }
267
263
  """
268
- graphql_request(mutation, variables={"discussionId": node_id, "body": comment})
264
+ event.graphql_request(mutation, variables={"discussionId": node_id, "body": comment})
269
265
  else:
270
- url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{number}/comments"
271
- r = requests.post(url, json={"body": comment}, headers=GITHUB_HEADERS)
266
+ url = f"{GITHUB_API_URL}/repos/{event.repository}/issues/{number}/comments"
267
+ r = requests.post(url, json={"body": comment}, headers=event.headers)
272
268
  print(f"{'Successful' if r.status_code in {200, 201} else 'Fail'} issue/PR #{number} comment: {r.status_code}")
273
269
 
274
270
 
275
- def get_first_interaction_response(issue_type: str, title: str, body: str, username: str, number: int) -> str:
271
+ def get_first_interaction_response(event, issue_type: str, title: str, body: str, username: str) -> str:
276
272
  """Generates a custom LLM response for GitHub issues, PRs, or discussions based on content."""
277
273
  issue_discussion_response = f"""
278
- 👋 Hello @{username}, thank you for submitting a `{GITHUB_REPOSITORY}` 🚀 {issue_type.capitalize()}. To help us address your concern efficiently, please ensure you've provided the following information:
274
+ 👋 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:
279
275
 
280
276
  1. For bug reports:
281
277
  - A clear and concise description of the bug
@@ -300,10 +296,10 @@ Thank you for your contribution to improving our project!
300
296
  """
301
297
 
302
298
  pr_response = f"""
303
- 👋 Hello @{username}, thank you for submitting an `{GITHUB_REPOSITORY}` 🚀 PR! To ensure a seamless integration of your work, please review the following checklist:
299
+ 👋 Hello @{username}, thank you for submitting an `{event.repository}` 🚀 PR! To ensure a seamless integration of your work, please review the following checklist:
304
300
 
305
- - ✅ **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/{GITHUB_REPOSITORY}/issues). Ensure your commit messages are clear, concise, and adhere to the project's conventions.
306
- - ✅ **Synchronize with Source**: Confirm your PR is synchronized with the `{GITHUB_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.
301
+ - ✅ **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.
302
+ - ✅ **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.
307
303
  - ✅ **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.
308
304
  - ✅ **Update Documentation**: Update the relevant [documentation](https://docs.ultralytics.com) for any new or modified features.
309
305
  - ✅ **Add Tests**: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
@@ -318,9 +314,9 @@ For more guidance, please refer to our [Contributing Guide](https://docs.ultraly
318
314
  else:
319
315
  example = os.getenv("FIRST_ISSUE_RESPONSE") or issue_discussion_response
320
316
 
321
- org_name, repo_name = GITHUB_REPOSITORY.split("/")
322
- repo_url = f"https://github.com/{GITHUB_REPOSITORY}"
323
- diff = get_pr_diff(number)[:32000] if issue_type == "pull request" else ""
317
+ org_name, repo_name = event.repository.split("/")
318
+ repo_url = f"https://github.com/{event.repository}"
319
+ diff = event.get_pr_diff()[:32000] if issue_type == "pull request" else ""
324
320
 
325
321
  prompt = f"""Generate a customized response to the new GitHub {issue_type} below:
326
322
 
@@ -359,39 +355,40 @@ YOUR {issue_type.upper()} RESPONSE:
359
355
  messages = [
360
356
  {
361
357
  "role": "system",
362
- "content": f"You are a helpful assistant responding to GitHub {issue_type}s for {org_name}.",
358
+ "content": f"You are an Ultralytics AI assistant responding to GitHub {issue_type}s for {org_name}.",
363
359
  },
364
360
  {"role": "user", "content": prompt},
365
361
  ]
366
362
  return get_completion(messages)
367
363
 
368
364
 
369
- def main():
370
- """Executes autolabeling and custom response generation for new GitHub issues, PRs, and discussions."""
371
- number, node_id, title, body, username, issue_type, action = get_event_content()
372
- available_labels = get_github_data("labels")
365
+ def main(*args, **kwargs):
366
+ """Executes auto-labeling and custom response generation for new GitHub issues, PRs, and discussions."""
367
+ event = Action(*args, **kwargs)
368
+ number, node_id, title, body, username, issue_type, action = get_event_content(event)
369
+ available_labels = event.get_repo_data("labels")
373
370
  label_descriptions = {label["name"]: label.get("description", "") for label in available_labels}
374
371
  if issue_type == "discussion":
375
372
  current_labels = [] # For discussions, labels may need to be fetched differently or adjusted
376
373
  else:
377
- current_labels = [label["name"].lower() for label in get_github_data(f"issues/{number}/labels")]
374
+ current_labels = [label["name"].lower() for label in event.get_repo_data(f"issues/{number}/labels")]
378
375
  relevant_labels = get_relevant_labels(issue_type, title, body, label_descriptions, current_labels)
379
376
 
380
377
  if relevant_labels:
381
- apply_labels(number, node_id, relevant_labels, issue_type)
382
- if "Alert" in relevant_labels and not is_org_member(username):
383
- update_issue_pr_content(number, node_id, issue_type)
378
+ apply_labels(event, number, node_id, relevant_labels, issue_type)
379
+ if "Alert" in relevant_labels and not is_org_member(event, username):
380
+ update_issue_pr_content(event, number, node_id, issue_type)
384
381
  if issue_type != "pull request":
385
- close_issue_pr(number, node_id, issue_type)
386
- lock_issue_pr(number, node_id, issue_type)
382
+ close_issue_pr(event, number, node_id, issue_type)
383
+ lock_issue_pr(event, number, node_id, issue_type)
387
384
  if BLOCK_USER:
388
- block_user(username=username)
385
+ block_user(event, username=username)
389
386
  else:
390
387
  print("No relevant labels found or applied.")
391
388
 
392
389
  if action in {"opened", "created"}:
393
- custom_response = get_first_interaction_response(issue_type, title, body, username, number)
394
- add_comment(number, node_id, custom_response, issue_type)
390
+ custom_response = get_first_interaction_response(event, issue_type, title, body, username)
391
+ add_comment(event, number, node_id, custom_response, issue_type)
395
392
 
396
393
 
397
394
  if __name__ == "__main__":
@@ -6,12 +6,8 @@ import requests
6
6
 
7
7
  from .utils import (
8
8
  GITHUB_API_URL,
9
- GITHUB_HEADERS,
10
- GITHUB_REPOSITORY,
11
- PR,
9
+ Action,
12
10
  get_completion,
13
- get_github_username,
14
- get_pr_diff,
15
11
  )
16
12
 
17
13
  # Constants
@@ -20,13 +16,8 @@ SUMMARY_START = (
20
16
  )
21
17
 
22
18
 
23
- def generate_merge_message(pr_author, contributors, pr_summary=None):
19
+ def generate_merge_message(pr_summary=None, pr_credit=None):
24
20
  """Generates a thank-you message for merged PR contributors."""
25
- contributors_str = ", ".join(f"@{c}" for c in contributors if c != pr_author)
26
- mention_str = f"@{pr_author}"
27
- if contributors_str:
28
- mention_str += f" and {contributors_str}"
29
-
30
21
  messages = [
31
22
  {
32
23
  "role": "system",
@@ -34,10 +25,10 @@ def generate_merge_message(pr_author, contributors, pr_summary=None):
34
25
  },
35
26
  {
36
27
  "role": "user",
37
- "content": f"Write a friendly thank you for a merged PR by these GitHub contributors: {mention_str}. "
28
+ "content": f"Write a friendly thank you for a merged GitHub PR by {pr_credit}. "
38
29
  f"Context from PR:\n{pr_summary}\n\n"
39
- f"Start with the exciting message that this PR is now merged, and weave in an inspiring quote "
40
- f"from a famous figure in science, philosophy or stoicism. "
30
+ f"Start with the exciting message that this PR is now merged, and weave in an inspiring but obscure quote "
31
+ f"from a historical figure in science, art, stoicism and philosophy. "
41
32
  f"Keep the message concise yet relevant to the specific contributions in this PR. "
42
33
  f"We want the contributors to feel their effort is appreciated and will make a difference in the world.",
43
34
  },
@@ -45,15 +36,15 @@ def generate_merge_message(pr_author, contributors, pr_summary=None):
45
36
  return get_completion(messages)
46
37
 
47
38
 
48
- def post_merge_message(pr_number, pr_author, contributors, summary):
39
+ def post_merge_message(pr_number, repository, summary, pr_credit, headers):
49
40
  """Posts thank you message on PR after merge."""
50
- message = generate_merge_message(pr_author, contributors, summary)
51
- comment_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{pr_number}/comments"
52
- response = requests.post(comment_url, json={"body": message}, headers=GITHUB_HEADERS)
41
+ message = generate_merge_message(summary, pr_credit)
42
+ comment_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{pr_number}/comments"
43
+ response = requests.post(comment_url, json={"body": message}, headers=headers)
53
44
  return response.status_code == 201
54
45
 
55
46
 
56
- def generate_issue_comment(pr_url, pr_summary):
47
+ def generate_issue_comment(pr_url, pr_summary, pr_credit):
57
48
  """Generates a personalized issue comment using based on the PR context."""
58
49
  messages = [
59
50
  {
@@ -62,21 +53,22 @@ def generate_issue_comment(pr_url, pr_summary):
62
53
  },
63
54
  {
64
55
  "role": "user",
65
- "content": f"Write a GitHub issue comment announcing a potential fix has been merged in linked PR {pr_url}\n\n"
56
+ "content": f"Write a GitHub issue comment announcing a potential fix for this issue is now merged in linked PR {pr_url} by {pr_credit}\n\n"
66
57
  f"Context from PR:\n{pr_summary}\n\n"
67
58
  f"Include:\n"
68
59
  f"1. An explanation of key changes from the PR that may resolve this issue\n"
69
- f"2. Options for testing if PR changes have resolved this issue:\n"
60
+ f"2. Credit to the PR author and contributors\n"
61
+ f"3. Options for testing if PR changes have resolved this issue:\n"
70
62
  f" - pip install git+https://github.com/ultralytics/ultralytics.git@main # test latest changes\n"
71
63
  f" - or await next official PyPI release\n"
72
- f"3. Request feedback on whether the PR changes resolve the issue\n"
73
- f"4. Thank 🙏 for reporting the issue and welcome any further feedback if the issue persists\n\n",
64
+ f"4. Request feedback on whether the PR changes resolve the issue\n"
65
+ f"5. Thank 🙏 for reporting the issue and welcome any further feedback if the issue persists\n\n",
74
66
  },
75
67
  ]
76
68
  return get_completion(messages)
77
69
 
78
70
 
79
- def generate_pr_summary(repo_name, diff_text):
71
+ def generate_pr_summary(repository, diff_text):
80
72
  """Generates a concise, professional summary of a PR using OpenAI's API for Ultralytics repositories."""
81
73
  if not diff_text:
82
74
  diff_text = "**ERROR: DIFF IS EMPTY, THERE ARE ZERO CODE CHANGES IN THIS PR."
@@ -89,7 +81,7 @@ def generate_pr_summary(repo_name, diff_text):
89
81
  },
90
82
  {
91
83
  "role": "user",
92
- "content": f"Summarize this '{repo_name}' PR, focusing on major changes, their purpose, and potential impact. Keep the summary clear and concise, suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
84
+ "content": f"Summarize this '{repository}' PR, focusing on major changes, their purpose, and potential impact. Keep the summary clear and concise, suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
93
85
  f"### 🌟 Summary (single-line synopsis)\n"
94
86
  f"### 📊 Key Changes (bullet points highlighting any major changes)\n"
95
87
  f"### 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n"
@@ -102,12 +94,12 @@ def generate_pr_summary(repo_name, diff_text):
102
94
  return SUMMARY_START + reply
103
95
 
104
96
 
105
- def update_pr_description(repo_name, pr_number, new_summary, max_retries=2):
97
+ def update_pr_description(repository, pr_number, new_summary, headers, max_retries=2):
106
98
  """Updates PR description with new summary, retrying if description is None."""
107
- pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
99
+ pr_url = f"{GITHUB_API_URL}/repos/{repository}/pulls/{pr_number}"
108
100
  description = ""
109
101
  for i in range(max_retries + 1):
110
- description = requests.get(pr_url, headers=GITHUB_HEADERS).json().get("body") or ""
102
+ description = requests.get(pr_url, headers=headers).json().get("body") or ""
111
103
  if description:
112
104
  break
113
105
  if i < max_retries:
@@ -124,65 +116,76 @@ def update_pr_description(repo_name, pr_number, new_summary, max_retries=2):
124
116
  updated_description = description + "\n\n" + new_summary
125
117
 
126
118
  # Update the PR description
127
- update_response = requests.patch(pr_url, json={"body": updated_description}, headers=GITHUB_HEADERS)
119
+ update_response = requests.patch(pr_url, json={"body": updated_description}, headers=headers)
128
120
  return update_response.status_code
129
121
 
130
122
 
131
- def label_fixed_issues(pr_number, pr_summary):
132
- """Labels issues closed by this PR when merged, notifies users, and returns PR contributors."""
123
+ def label_fixed_issues(repository, pr_number, pr_summary, headers, action):
124
+ """Labels issues closed by PR when merged, notifies users, returns PR contributors."""
133
125
  query = """
134
126
  query($owner: String!, $repo: String!, $pr_number: Int!) {
135
127
  repository(owner: $owner, name: $repo) {
136
128
  pullRequest(number: $pr_number) {
137
- closingIssuesReferences(first: 50) {
138
- nodes {
139
- number
140
- }
141
- }
129
+ closingIssuesReferences(first: 50) { nodes { number } }
142
130
  url
143
131
  body
144
132
  author { login, __typename }
145
- reviews(first: 50) {
146
- nodes { author { login, __typename } }
147
- }
148
- comments(first: 50) {
149
- nodes { author { login, __typename } }
150
- }
133
+ reviews(first: 50) { nodes { author { login, __typename } } }
134
+ comments(first: 50) { nodes { author { login, __typename } } }
135
+ commits(first: 100) { nodes { commit { author { user { login } }, committer { user { login } } } } }
151
136
  }
152
137
  }
153
138
  }
154
139
  """
155
-
156
- owner, repo = GITHUB_REPOSITORY.split("/")
140
+ owner, repo = repository.split("/")
157
141
  variables = {"owner": owner, "repo": repo, "pr_number": pr_number}
158
142
  graphql_url = "https://api.github.com/graphql"
159
- response = requests.post(graphql_url, json={"query": query, "variables": variables}, headers=GITHUB_HEADERS)
143
+ response = requests.post(graphql_url, json={"query": query, "variables": variables}, headers=headers)
144
+
160
145
  if response.status_code != 200:
161
146
  print(f"Failed to fetch linked issues. Status code: {response.status_code}")
162
147
  return [], None
163
148
 
164
149
  try:
165
150
  data = response.json()["data"]["repository"]["pullRequest"]
166
- comments = data["reviews"]["nodes"] + data["comments"]["nodes"] # merge lists
167
- author = data["author"]["login"]
151
+ comments = data["reviews"]["nodes"] + data["comments"]["nodes"]
152
+ token_username = action.get_username() # get GITHUB_TOKEN username
153
+ author = data["author"]["login"] if data["author"]["__typename"] != "Bot" else None
168
154
 
169
155
  # Get unique contributors from reviews and comments
170
156
  contributors = {x["author"]["login"] for x in comments if x["author"]["__typename"] != "Bot"}
171
- contributors.discard(author) # Remove author from contributors list
157
+
158
+ # Add commit authors and committers that have GitHub accounts linked
159
+ for commit in data["commits"]["nodes"]:
160
+ commit_data = commit["commit"]
161
+ for user_type in ["author", "committer"]:
162
+ if user := commit_data[user_type].get("user"):
163
+ if login := user.get("login"):
164
+ contributors.add(login)
165
+
166
+ contributors.discard(author)
167
+ contributors.discard(token_username)
168
+
169
+ # Write credit string
170
+ pr_credit = "" # i.e. "@user1 with contributions from @user2, @user3"
171
+ if author and author != token_username:
172
+ pr_credit += f"@{author}"
173
+ if contributors:
174
+ pr_credit += (" with contributions from " if pr_credit else "") + ", ".join(f"@{c}" for c in contributors)
172
175
 
173
176
  # Generate personalized comment
174
- comment = generate_issue_comment(pr_url=data["url"], pr_summary=pr_summary)
177
+ comment = generate_issue_comment(pr_url=data["url"], pr_summary=pr_summary, pr_credit=pr_credit)
175
178
 
176
179
  # Update linked issues
177
180
  for issue in data["closingIssuesReferences"]["nodes"]:
178
181
  issue_number = issue["number"]
179
182
  # Add fixed label
180
- label_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{issue_number}/labels"
181
- label_response = requests.post(label_url, json={"labels": ["fixed"]}, headers=GITHUB_HEADERS)
183
+ label_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{issue_number}/labels"
184
+ label_response = requests.post(label_url, json={"labels": ["fixed"]}, headers=headers)
182
185
 
183
186
  # Add comment
184
- comment_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{issue_number}/comments"
185
- comment_response = requests.post(comment_url, json={"body": comment}, headers=GITHUB_HEADERS)
187
+ comment_url = f"{GITHUB_API_URL}/repos/{repository}/issues/{issue_number}/comments"
188
+ comment_response = requests.post(comment_url, json={"body": comment}, headers=headers)
186
189
 
187
190
  if label_response.status_code == 200 and comment_response.status_code == 201:
188
191
  print(f"Added 'fixed' label and comment to issue #{issue_number}")
@@ -192,50 +195,49 @@ query($owner: String!, $repo: String!, $pr_number: Int!) {
192
195
  f"Comment status: {comment_response.status_code}"
193
196
  )
194
197
 
195
- return contributors, author
198
+ return pr_credit
196
199
  except KeyError as e:
197
200
  print(f"Error parsing GraphQL response: {e}")
198
201
  return [], None
199
202
 
200
203
 
201
- def remove_todos_on_merge(pr_number):
204
+ def remove_todos_on_merge(pr_number, repository, headers):
202
205
  """Removes specified labels from PR."""
203
206
  for label in ["TODO"]: # Can be extended with more labels in the future
204
- requests.delete(
205
- f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{pr_number}/labels/{label}", headers=GITHUB_HEADERS
206
- )
207
+ requests.delete(f"{GITHUB_API_URL}/repos/{repository}/issues/{pr_number}/labels/{label}", headers=headers)
207
208
 
208
209
 
209
- def main():
210
+ def main(*args, **kwargs):
210
211
  """Summarize a pull request and update its description with a summary."""
211
- pr_number = PR["number"]
212
+ action = Action(*args, **kwargs)
213
+ pr_number = action.pr["number"]
214
+ headers = action.headers
215
+ repository = action.repository
212
216
 
213
217
  print(f"Retrieving diff for PR {pr_number}")
214
- diff = get_pr_diff(pr_number)
218
+ diff = action.get_pr_diff()
215
219
 
216
220
  # Generate PR summary
217
221
  print("Generating PR summary...")
218
- summary = generate_pr_summary(GITHUB_REPOSITORY, diff)
222
+ summary = generate_pr_summary(repository, diff)
219
223
 
220
224
  # Update PR description
221
225
  print("Updating PR description...")
222
- status_code = update_pr_description(GITHUB_REPOSITORY, pr_number, summary)
226
+ status_code = update_pr_description(repository, pr_number, summary, headers)
223
227
  if status_code == 200:
224
228
  print("PR description updated successfully.")
225
229
  else:
226
230
  print(f"Failed to update PR description. Status code: {status_code}")
227
231
 
228
232
  # Update linked issues and post thank you message if merged
229
- if PR.get("merged"):
233
+ if action.pr.get("merged"):
230
234
  print("PR is merged, labeling fixed issues...")
231
- contributors, author = label_fixed_issues(pr_number, summary)
235
+ pr_credit = label_fixed_issues(repository, pr_number, summary, headers, action)
232
236
  print("Removing TODO label from PR...")
233
- remove_todos_on_merge(pr_number)
234
- username = get_github_username() # get GITHUB_TOKEN username
235
- if author and author != username:
237
+ remove_todos_on_merge(pr_number, repository, headers)
238
+ if pr_credit:
236
239
  print("Posting PR author thank you message...")
237
- contributors.discard(username)
238
- post_merge_message(pr_number, author, contributors, summary)
240
+ post_merge_message(pr_number, repository, summary, pr_credit, headers)
239
241
 
240
242
 
241
243
  if __name__ == "__main__":