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 +1 -1
- actions/dispatch_actions.py +16 -27
- actions/first_interaction.py +10 -35
- actions/summarize_pr.py +30 -57
- actions/summarize_release.py +29 -39
- actions/utils/__init__.py +2 -1
- actions/utils/common_utils.py +1 -2
- actions/utils/github_utils.py +92 -31
- {ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/METADATA +1 -1
- ultralytics_actions-0.0.71.dist-info/RECORD +16 -0
- {ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/WHEEL +1 -1
- ultralytics_actions-0.0.70.dist-info/RECORD +0 -16
- {ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/entry_points.txt +0 -0
- {ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/licenses/LICENSE +0 -0
- {ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/top_level.txt +0 -0
actions/__init__.py
CHANGED
actions/dispatch_actions.py
CHANGED
@@ -4,13 +4,11 @@ import time
|
|
4
4
|
from datetime import datetime
|
5
5
|
from typing import Dict, List
|
6
6
|
|
7
|
-
import
|
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 = ["
|
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
|
-
|
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 =
|
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 =
|
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
|
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 =
|
95
|
+
comment_body = event.event_data["comment"].get("body", "")
|
110
96
|
username = event.event_data["comment"]["user"]["login"]
|
111
97
|
|
112
|
-
# Check for
|
113
|
-
|
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
|
-
|
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__":
|
actions/first_interaction.py
CHANGED
@@ -3,14 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
from typing import Dict, List, Tuple
|
5
5
|
|
6
|
-
import
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
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(
|
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 =
|
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(
|
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 =
|
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
|
-
|
126
|
-
return update_response.status_code
|
120
|
+
event.patch(url, json={"body": updated_description})
|
127
121
|
|
128
122
|
|
129
|
-
def label_fixed_issues(
|
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":
|
149
|
-
|
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
|
-
|
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 =
|
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
|
-
|
182
|
+
number = issue["number"]
|
192
183
|
# Add fixed label
|
193
|
-
|
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
|
-
|
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
|
195
|
+
def remove_pr_labels(event, labels=()):
|
215
196
|
"""Removes specified labels from PR."""
|
216
|
-
for label in
|
217
|
-
|
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
|
-
|
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 {
|
229
|
-
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
|
-
|
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
|
217
|
+
if event.pr.get("merged"):
|
245
218
|
print("PR is merged, labeling fixed issues...")
|
246
|
-
pr_credit = label_fixed_issues(
|
219
|
+
pr_credit = label_fixed_issues(event, summary)
|
247
220
|
print("Removing TODO label from PR...")
|
248
|
-
|
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(
|
224
|
+
post_merge_message(event, summary, pr_credit)
|
252
225
|
|
253
226
|
|
254
227
|
if __name__ == "__main__":
|
actions/summarize_release.py
CHANGED
@@ -6,36 +6,27 @@ import subprocess
|
|
6
6
|
import time
|
7
7
|
from datetime import datetime
|
8
8
|
|
9
|
-
import
|
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(
|
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/{
|
26
|
-
r =
|
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(
|
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/{
|
33
|
-
r =
|
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/{
|
47
|
-
pr_response =
|
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(
|
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:{
|
74
|
-
r =
|
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
|
-
|
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(
|
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/{
|
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(
|
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/{
|
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
|
-
|
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
|
-
|
154
|
+
event = Action(*args, **kwargs)
|
161
155
|
|
162
|
-
if not all([
|
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(
|
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(
|
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
|
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
|
-
|
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",
|
actions/utils/common_utils.py
CHANGED
@@ -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=
|
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")]
|
actions/utils/github_utils.py
CHANGED
@@ -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"
|
31
|
-
self.headers_diff = {"Authorization": f"
|
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
|
-
|
43
|
-
response
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
64
|
-
return
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
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.
|
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,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,,
|
{ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/entry_points.txt
RENAMED
File without changes
|
{ultralytics_actions-0.0.70.dist-info → ultralytics_actions-0.0.71.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|