ultralytics-actions 0.0.72__tar.gz → 0.0.73__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.
- {ultralytics_actions-0.0.72/ultralytics_actions.egg-info → ultralytics_actions-0.0.73}/PKG-INFO +32 -15
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/README.md +30 -14
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/__init__.py +1 -1
- ultralytics_actions-0.0.73/actions/update_file_headers.py +191 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/github_utils.py +5 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/pyproject.toml +2 -0
- ultralytics_actions-0.0.73/tests/test_cli_commands.py +19 -0
- ultralytics_actions-0.0.73/tests/test_common_utils.py +63 -0
- ultralytics_actions-0.0.73/tests/test_dispatch_actions.py +146 -0
- ultralytics_actions-0.0.73/tests/test_first_interaction.py +116 -0
- ultralytics_actions-0.0.73/tests/test_github_utils.py +64 -0
- ultralytics_actions-0.0.73/tests/test_init.py +84 -0
- ultralytics_actions-0.0.73/tests/test_openai_utils.py +65 -0
- ultralytics_actions-0.0.73/tests/test_summarize_pr.py +73 -0
- ultralytics_actions-0.0.73/tests/test_summarize_release.py +127 -0
- ultralytics_actions-0.0.73/tests/test_update_markdown_codeblocks.py +106 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73/ultralytics_actions.egg-info}/PKG-INFO +32 -15
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/SOURCES.txt +11 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/entry_points.txt +1 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/requires.txt +1 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/LICENSE +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/dispatch_actions.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/first_interaction.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/summarize_pr.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/summarize_release.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/update_markdown_code_blocks.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/__init__.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/common_utils.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/openai_utils.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/setup.cfg +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/tests/test_urls.py +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
- {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/top_level.txt +0 -0
{ultralytics_actions-0.0.72/ultralytics_actions.egg-info → ultralytics_actions-0.0.73}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ultralytics-actions
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.73
|
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>
|
@@ -33,6 +33,7 @@ Requires-Dist: ruff>=0.9.1
|
|
33
33
|
Requires-Dist: docformatter>=1.7.5
|
34
34
|
Provides-Extra: dev
|
35
35
|
Requires-Dist: pytest; extra == "dev"
|
36
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
36
37
|
Dynamic: license-file
|
37
38
|
|
38
39
|
<a href="https://www.ultralytics.com/"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
|
@@ -42,12 +43,16 @@ Dynamic: license-file
|
|
42
43
|
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
43
44
|
|
44
45
|
[](https://github.com/marketplace/actions/ultralytics-actions)
|
46
|
+
|
47
|
+
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
45
48
|
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
49
|
+
[](https://codecov.io/github/ultralytics/actions)
|
50
|
+
[](https://badge.fury.io/py/ultralytics-actions)
|
51
|
+
[](https://www.pepy.tech/projects/ultralytics-actions)
|
52
|
+
|
46
53
|
[](https://discord.com/invite/ultralytics)
|
47
54
|
[](https://community.ultralytics.com/)
|
48
55
|
[](https://reddit.com/r/ultralytics)
|
49
|
-
[](https://badge.fury.io/py/ultralytics-actions)
|
50
|
-
[](https://www.pepy.tech/projects/ultralytics-actions)
|
51
56
|
|
52
57
|
## 📄 Actions Description
|
53
58
|
|
@@ -84,6 +89,11 @@ To integrate this action into your Ultralytics repository:
|
|
84
89
|
2. **Add the Action:** Configure the Ultralytics Actions in your workflow file as shown below:
|
85
90
|
|
86
91
|
```yaml
|
92
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
93
|
+
|
94
|
+
# Ultralytics Actions https://github.com/ultralytics/actions
|
95
|
+
# This workflow formats code and documentation in PRs to Ultralytics standards
|
96
|
+
|
87
97
|
name: Ultralytics Actions
|
88
98
|
|
89
99
|
on:
|
@@ -91,24 +101,31 @@ To integrate this action into your Ultralytics repository:
|
|
91
101
|
types: [opened]
|
92
102
|
pull_request:
|
93
103
|
branches: [main]
|
94
|
-
types: [opened, closed]
|
104
|
+
types: [opened, closed, synchronize, review_requested]
|
105
|
+
|
106
|
+
permissions:
|
107
|
+
contents: write # Modify code in PRs
|
108
|
+
pull-requests: write # Add comments and labels to PRs
|
109
|
+
issues: write # Add comments and labels to issues
|
95
110
|
|
96
111
|
jobs:
|
97
|
-
|
98
|
-
runs-on: ubuntu-latest
|
112
|
+
actions:
|
113
|
+
runs-on: ubuntu-latest
|
99
114
|
steps:
|
100
|
-
- name: Run Ultralytics
|
115
|
+
- name: Run Ultralytics Actions
|
101
116
|
uses: ultralytics/actions@main
|
102
117
|
with:
|
103
|
-
token: ${{ secrets.GITHUB_TOKEN }} #
|
104
|
-
labels: true #
|
105
|
-
python: true # Format Python
|
106
|
-
prettier: true # Format YAML, JSON, Markdown,
|
107
|
-
swift: false # Format Swift
|
118
|
+
token: ${{ secrets.GITHUB_TOKEN }} # Auto-generated token
|
119
|
+
labels: true # Auto-label issues/PRs using AI
|
120
|
+
python: true # Format Python with Ruff and docformatter
|
121
|
+
prettier: true # Format YAML, JSON, Markdown, CSS
|
122
|
+
swift: false # Format Swift (requires macos-latest)
|
123
|
+
dart: false # Format Dart/Flutter
|
108
124
|
spelling: true # Check spelling with codespell
|
109
|
-
links: true # Check
|
110
|
-
summary: true # Generate
|
111
|
-
openai_api_key: ${{ secrets.OPENAI_API_KEY }} #
|
125
|
+
links: true # Check broken links with Lychee
|
126
|
+
summary: true # Generate AI-powered PR summaries
|
127
|
+
openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Powers PR summaries, labels and comments
|
128
|
+
brave_api_key: ${{ secrets.BRAVE_API_KEY }} # Used for broken link resolution
|
112
129
|
```
|
113
130
|
|
114
131
|
3. **Customize:** Adjust the `runs-on` runner and the boolean flags (`labels`, `python`, `prettier`, `swift`, `spelling`, `links`, `summary`) based on your project's needs. Remember to add your `OPENAI_API_KEY` as a secret in your repository settings if you enable `labels` or `summary`.
|
@@ -5,12 +5,16 @@
|
|
5
5
|
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
6
6
|
|
7
7
|
[](https://github.com/marketplace/actions/ultralytics-actions)
|
8
|
+
|
9
|
+
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
8
10
|
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
11
|
+
[](https://codecov.io/github/ultralytics/actions)
|
12
|
+
[](https://badge.fury.io/py/ultralytics-actions)
|
13
|
+
[](https://www.pepy.tech/projects/ultralytics-actions)
|
14
|
+
|
9
15
|
[](https://discord.com/invite/ultralytics)
|
10
16
|
[](https://community.ultralytics.com/)
|
11
17
|
[](https://reddit.com/r/ultralytics)
|
12
|
-
[](https://badge.fury.io/py/ultralytics-actions)
|
13
|
-
[](https://www.pepy.tech/projects/ultralytics-actions)
|
14
18
|
|
15
19
|
## 📄 Actions Description
|
16
20
|
|
@@ -47,6 +51,11 @@ To integrate this action into your Ultralytics repository:
|
|
47
51
|
2. **Add the Action:** Configure the Ultralytics Actions in your workflow file as shown below:
|
48
52
|
|
49
53
|
```yaml
|
54
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
55
|
+
|
56
|
+
# Ultralytics Actions https://github.com/ultralytics/actions
|
57
|
+
# This workflow formats code and documentation in PRs to Ultralytics standards
|
58
|
+
|
50
59
|
name: Ultralytics Actions
|
51
60
|
|
52
61
|
on:
|
@@ -54,24 +63,31 @@ To integrate this action into your Ultralytics repository:
|
|
54
63
|
types: [opened]
|
55
64
|
pull_request:
|
56
65
|
branches: [main]
|
57
|
-
types: [opened, closed]
|
66
|
+
types: [opened, closed, synchronize, review_requested]
|
67
|
+
|
68
|
+
permissions:
|
69
|
+
contents: write # Modify code in PRs
|
70
|
+
pull-requests: write # Add comments and labels to PRs
|
71
|
+
issues: write # Add comments and labels to issues
|
58
72
|
|
59
73
|
jobs:
|
60
|
-
|
61
|
-
runs-on: ubuntu-latest
|
74
|
+
actions:
|
75
|
+
runs-on: ubuntu-latest
|
62
76
|
steps:
|
63
|
-
- name: Run Ultralytics
|
77
|
+
- name: Run Ultralytics Actions
|
64
78
|
uses: ultralytics/actions@main
|
65
79
|
with:
|
66
|
-
token: ${{ secrets.GITHUB_TOKEN }} #
|
67
|
-
labels: true #
|
68
|
-
python: true # Format Python
|
69
|
-
prettier: true # Format YAML, JSON, Markdown,
|
70
|
-
swift: false # Format Swift
|
80
|
+
token: ${{ secrets.GITHUB_TOKEN }} # Auto-generated token
|
81
|
+
labels: true # Auto-label issues/PRs using AI
|
82
|
+
python: true # Format Python with Ruff and docformatter
|
83
|
+
prettier: true # Format YAML, JSON, Markdown, CSS
|
84
|
+
swift: false # Format Swift (requires macos-latest)
|
85
|
+
dart: false # Format Dart/Flutter
|
71
86
|
spelling: true # Check spelling with codespell
|
72
|
-
links: true # Check
|
73
|
-
summary: true # Generate
|
74
|
-
openai_api_key: ${{ secrets.OPENAI_API_KEY }} #
|
87
|
+
links: true # Check broken links with Lychee
|
88
|
+
summary: true # Generate AI-powered PR summaries
|
89
|
+
openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Powers PR summaries, labels and comments
|
90
|
+
brave_api_key: ${{ secrets.BRAVE_API_KEY }} # Used for broken link resolution
|
75
91
|
```
|
76
92
|
|
77
93
|
3. **Customize:** Adjust the `runs-on` runner and the boolean flags (`labels`, `python`, `prettier`, `swift`, `spelling`, `links`, `summary`) based on your project's needs. Remember to add your `OPENAI_API_KEY` as a secret in your repository settings if you enable `labels` or `summary`.
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from actions.utils import Action
|
7
|
+
|
8
|
+
# Base header text
|
9
|
+
HEADER = os.getenv("HEADER")
|
10
|
+
|
11
|
+
# Map file extensions to comment styles
|
12
|
+
COMMENT_MAP = {
|
13
|
+
# Python style
|
14
|
+
".py": ("# ", None, None),
|
15
|
+
".yml": ("# ", None, None),
|
16
|
+
".yaml": ("# ", None, None),
|
17
|
+
".toml": ("# ", None, None),
|
18
|
+
".sh": ("# ", None, None), # Bash scripts
|
19
|
+
".bash": ("# ", None, None), # Bash scripts
|
20
|
+
# C/C++/Java/JS style
|
21
|
+
".c": ("// ", "/* ", " */"), # C files
|
22
|
+
".cpp": ("// ", "/* ", " */"), # C++ files
|
23
|
+
".h": ("// ", "/* ", " */"), # C/C++ header files
|
24
|
+
".hpp": ("// ", "/* ", " */"), # C++ header files
|
25
|
+
".swift": ("// ", "/* ", " */"),
|
26
|
+
".js": ("// ", "/* ", " */"),
|
27
|
+
".ts": ("// ", "/* ", " */"), # TypeScript files
|
28
|
+
".dart": ("// ", "/* ", " */"), # Dart/Flutter files
|
29
|
+
".rs": ("// ", "/* ", " */"), # Rust files
|
30
|
+
".java": ("// ", "/* ", " */"), # Android Java
|
31
|
+
".kt": ("// ", "/* ", " */"), # Android Kotlin
|
32
|
+
# CSS style
|
33
|
+
".css": (None, "/* ", " */"),
|
34
|
+
# HTML/XML style
|
35
|
+
".html": (None, "<!-- ", " -->"),
|
36
|
+
".xml": (None, "<!-- ", " -->"), # Android XML
|
37
|
+
# MATLAB style
|
38
|
+
".m": ("% ", None, None),
|
39
|
+
}
|
40
|
+
|
41
|
+
# Ignore these Paths (do not update their headers)
|
42
|
+
IGNORE_PATHS = [
|
43
|
+
".idea",
|
44
|
+
".venv",
|
45
|
+
"env",
|
46
|
+
"node_modules",
|
47
|
+
".git",
|
48
|
+
"__pycache__",
|
49
|
+
"mkdocs_github_authors.yaml",
|
50
|
+
# Build and distribution directories
|
51
|
+
"dist",
|
52
|
+
"build",
|
53
|
+
".eggs",
|
54
|
+
"site", # mkdocs build directory
|
55
|
+
# Generated code
|
56
|
+
"generated",
|
57
|
+
"auto_gen",
|
58
|
+
# Lock files
|
59
|
+
"lock",
|
60
|
+
# Minified files
|
61
|
+
".min.js",
|
62
|
+
".min.css",
|
63
|
+
]
|
64
|
+
|
65
|
+
|
66
|
+
def update_file(file_path, prefix, block_start, block_end, base_header):
|
67
|
+
"""Update file with the correct header and proper spacing."""
|
68
|
+
try:
|
69
|
+
with open(file_path, encoding="utf-8") as f:
|
70
|
+
lines = f.readlines()
|
71
|
+
except Exception as e:
|
72
|
+
print(f"Error reading {file_path}: {e}")
|
73
|
+
return False
|
74
|
+
|
75
|
+
if not lines:
|
76
|
+
return False
|
77
|
+
|
78
|
+
# Format the header based on comment style
|
79
|
+
if prefix:
|
80
|
+
formatted_header = f"{prefix}{base_header}\n"
|
81
|
+
elif block_start and block_end:
|
82
|
+
formatted_header = f"{block_start}{base_header}{block_end}\n"
|
83
|
+
else:
|
84
|
+
formatted_header = f"# {base_header}\n"
|
85
|
+
|
86
|
+
# Keep shebang line if it exists
|
87
|
+
start_idx = 0
|
88
|
+
if lines and lines[0].startswith("#!"):
|
89
|
+
start_idx = 1
|
90
|
+
|
91
|
+
modified = False
|
92
|
+
new_lines = lines[:start_idx]
|
93
|
+
remaining_lines = lines[start_idx:]
|
94
|
+
|
95
|
+
# If first line is already the exact header we want
|
96
|
+
if remaining_lines and remaining_lines[0] == formatted_header:
|
97
|
+
# Check if spacing is correct
|
98
|
+
new_lines.append(remaining_lines[0])
|
99
|
+
if len(remaining_lines) > 1:
|
100
|
+
second_line = remaining_lines[1].strip()
|
101
|
+
if second_line == "" or second_line in ["#", "//", "/*", "*", "<!--", "%"]:
|
102
|
+
# Spacing is correct, append the rest
|
103
|
+
new_lines.extend(remaining_lines[1:])
|
104
|
+
else:
|
105
|
+
# Add blank line
|
106
|
+
new_lines.append("\n")
|
107
|
+
new_lines.extend(remaining_lines[1:])
|
108
|
+
modified = True
|
109
|
+
else:
|
110
|
+
# Only header exists, no need for blank line
|
111
|
+
pass
|
112
|
+
# Check if first line has AGPL but is not the exact header
|
113
|
+
elif remaining_lines and "AGPL" in remaining_lines[0] and remaining_lines[0] != formatted_header:
|
114
|
+
# Replace with proper header
|
115
|
+
new_lines.append(formatted_header)
|
116
|
+
modified = True
|
117
|
+
|
118
|
+
# Check if second line is blank or commented
|
119
|
+
if len(remaining_lines) > 1:
|
120
|
+
second_line = remaining_lines[1].strip()
|
121
|
+
if second_line == "" or second_line in ["#", "//", "/*", "*", "<!--", "%"]:
|
122
|
+
# Keep existing blank/comment line
|
123
|
+
new_lines.append(remaining_lines[1])
|
124
|
+
new_lines.extend(remaining_lines[2:])
|
125
|
+
else:
|
126
|
+
# Add blank line
|
127
|
+
new_lines.append("\n")
|
128
|
+
new_lines.extend(remaining_lines[1:])
|
129
|
+
else:
|
130
|
+
# Only header line, no need for blank line after
|
131
|
+
pass
|
132
|
+
# No header found, add it
|
133
|
+
else:
|
134
|
+
# Add header at the beginning
|
135
|
+
new_lines.append(formatted_header)
|
136
|
+
# Add blank line if content follows
|
137
|
+
if remaining_lines and remaining_lines[0].strip():
|
138
|
+
new_lines.append("\n")
|
139
|
+
new_lines.extend(remaining_lines)
|
140
|
+
modified = True
|
141
|
+
|
142
|
+
if modified:
|
143
|
+
try:
|
144
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
145
|
+
f.writelines(new_lines)
|
146
|
+
return True
|
147
|
+
except Exception as e:
|
148
|
+
print(f"Error writing {file_path}: {e}")
|
149
|
+
return False
|
150
|
+
|
151
|
+
return False
|
152
|
+
|
153
|
+
|
154
|
+
def main(*args, **kwargs):
|
155
|
+
"""Automates file header updates for all files in the specified directory."""
|
156
|
+
event = Action(*args, **kwargs)
|
157
|
+
|
158
|
+
if "ultralytics" in event.repository.lower():
|
159
|
+
if event.is_repo_private() and event.repository.startswith("ultralytics/"):
|
160
|
+
from datetime import datetime
|
161
|
+
|
162
|
+
notice = f"Copyright © 2014-{datetime.now().year}"
|
163
|
+
header = f"Ultralytics Inc. 🚀 {notice} - CONFIDENTIAL - https://ultralytics.com - All Rights Reserved"
|
164
|
+
else:
|
165
|
+
header = "Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license"
|
166
|
+
elif HEADER and HEADER.lower() not in {"true", "false", "none"}:
|
167
|
+
header = HEADER
|
168
|
+
else:
|
169
|
+
return
|
170
|
+
|
171
|
+
directory = Path.cwd()
|
172
|
+
total = changed = unchanged = 0
|
173
|
+
for ext, comment_style in COMMENT_MAP.items():
|
174
|
+
prefix, block_start, block_end = comment_style
|
175
|
+
|
176
|
+
for file_path in directory.rglob(f"*{ext}"):
|
177
|
+
if any(part in str(file_path) for part in IGNORE_PATHS):
|
178
|
+
continue
|
179
|
+
|
180
|
+
total += 1
|
181
|
+
if update_file(file_path, prefix, block_start, block_end, header):
|
182
|
+
print(f"Updated: {file_path.relative_to(directory)}")
|
183
|
+
changed += 1
|
184
|
+
else:
|
185
|
+
unchanged += 1
|
186
|
+
|
187
|
+
print(f"Headers: {total}, Updated: {changed}, Unchanged: {unchanged}")
|
188
|
+
|
189
|
+
|
190
|
+
if __name__ == "__main__":
|
191
|
+
main()
|
@@ -91,6 +91,10 @@ class Action:
|
|
91
91
|
return json.loads(Path(event_path).read_text())
|
92
92
|
return {}
|
93
93
|
|
94
|
+
def is_repo_private(self) -> bool:
|
95
|
+
"""Checks if the repository is public using event data or GitHub API if needed."""
|
96
|
+
return self.event_data.get("repository", {}).get("private")
|
97
|
+
|
94
98
|
def get_username(self) -> str | None:
|
95
99
|
"""Gets username associated with the GitHub token."""
|
96
100
|
response = self.post(GITHUB_GRAPHQL_URL, json={"query": "query { viewer { login } }"})
|
@@ -152,6 +156,7 @@ class Action:
|
|
152
156
|
"github.event_name": self.event_name,
|
153
157
|
"github.event.action": self.event_data.get("action"),
|
154
158
|
"github.repository": self.repository,
|
159
|
+
"github.repository.private": self.is_repo_private(),
|
155
160
|
"github.event.pull_request.number": self.pr.get("number"),
|
156
161
|
"github.event.pull_request.head.repo.full_name": self.pr.get("head", {}).get("repo", {}).get("full_name"),
|
157
162
|
"github.actor": os.environ.get("GITHUB_ACTOR"),
|
@@ -73,6 +73,7 @@ dependencies = [
|
|
73
73
|
[project.optional-dependencies]
|
74
74
|
dev = [
|
75
75
|
"pytest",
|
76
|
+
"pytest-cov",
|
76
77
|
]
|
77
78
|
|
78
79
|
[project.urls]
|
@@ -87,6 +88,7 @@ ultralytics-actions-first-interaction = "actions.first_interaction:main"
|
|
87
88
|
ultralytics-actions-summarize-pr = "actions.summarize_pr:main"
|
88
89
|
ultralytics-actions-summarize-release = "actions.summarize_release:main"
|
89
90
|
ultralytics-actions-update-markdown-code-blocks = "actions.update_markdown_code_blocks:main"
|
91
|
+
ultralytics-actions-header = "actions.update_file_headers:main"
|
90
92
|
ultralytics-actions-info = "actions.utils:ultralytics_actions_info"
|
91
93
|
|
92
94
|
[tool.setuptools]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
2
|
+
|
3
|
+
|
4
|
+
# Import CLI command modules
|
5
|
+
from actions import (
|
6
|
+
first_interaction,
|
7
|
+
summarize_pr,
|
8
|
+
summarize_release,
|
9
|
+
update_markdown_code_blocks,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
def test_importable_modules():
|
14
|
+
"""Test that all modules can be imported without errors."""
|
15
|
+
# This is a simple test to ensure modules can be imported successfully
|
16
|
+
assert hasattr(first_interaction, "main")
|
17
|
+
assert hasattr(summarize_pr, "main")
|
18
|
+
assert hasattr(summarize_release, "main")
|
19
|
+
assert hasattr(update_markdown_code_blocks, "main")
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
2
|
+
|
3
|
+
from unittest.mock import MagicMock, patch
|
4
|
+
|
5
|
+
from actions.utils.common_utils import (
|
6
|
+
allow_redirect,
|
7
|
+
brave_search,
|
8
|
+
clean_url,
|
9
|
+
remove_html_comments,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
def test_remove_html_comments():
|
14
|
+
"""Test removing HTML comments from strings."""
|
15
|
+
test_str = "Before <!-- Comment --> After"
|
16
|
+
assert remove_html_comments(test_str) == "Before After"
|
17
|
+
|
18
|
+
# Multiline comment
|
19
|
+
test_str = "Before\n<!-- Comment\nline 2\nline 3 -->\nAfter"
|
20
|
+
assert remove_html_comments(test_str) == "Before\n\nAfter"
|
21
|
+
|
22
|
+
# No comments
|
23
|
+
test_str = "No comments here"
|
24
|
+
assert remove_html_comments(test_str) == "No comments here"
|
25
|
+
|
26
|
+
|
27
|
+
def test_clean_url():
|
28
|
+
"""Test cleaning URL strings."""
|
29
|
+
# Test removing quotes and trailing characters
|
30
|
+
assert clean_url('"https://example.com"') == "https://example.com"
|
31
|
+
assert clean_url("'https://example.com'") == "https://example.com"
|
32
|
+
assert clean_url("https://example.com.") == "https://example.com"
|
33
|
+
assert clean_url("https://example.com,") == "https://example.com"
|
34
|
+
|
35
|
+
# Test git URLs
|
36
|
+
assert clean_url("git+https://github.com/user/repo.git@main") == "https://github.com/user/repo"
|
37
|
+
|
38
|
+
|
39
|
+
def test_allow_redirect():
|
40
|
+
"""Test allowing URL redirects based on rules."""
|
41
|
+
# Should not allow - start ignores
|
42
|
+
assert not allow_redirect("https://youtu.be/xyz", "https://youtube.com")
|
43
|
+
|
44
|
+
# Should not allow - end ignores
|
45
|
+
assert not allow_redirect("https://example.com", "https://example.com/404")
|
46
|
+
|
47
|
+
# Empty end URL
|
48
|
+
assert not allow_redirect("https://example.com", "")
|
49
|
+
|
50
|
+
|
51
|
+
@patch("requests.get")
|
52
|
+
def test_brave_search(mock_get):
|
53
|
+
"""Test Brave search API integration."""
|
54
|
+
mock_response = MagicMock()
|
55
|
+
mock_response.status_code = 200
|
56
|
+
mock_response.json.return_value = {
|
57
|
+
"web": {"results": [{"url": "https://example.com"}, {"url": "https://example.org"}]}
|
58
|
+
}
|
59
|
+
mock_get.return_value = mock_response
|
60
|
+
|
61
|
+
results = brave_search("test query", "test-api-key", count=2)
|
62
|
+
assert results == ["https://example.com", "https://example.org"]
|
63
|
+
mock_get.assert_called_once()
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
from unittest.mock import MagicMock, patch
|
5
|
+
|
6
|
+
from actions.dispatch_actions import (
|
7
|
+
RUN_CI_KEYWORD,
|
8
|
+
get_pr_branch,
|
9
|
+
main,
|
10
|
+
trigger_and_get_workflow_info,
|
11
|
+
update_comment,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
def test_get_pr_branch():
|
16
|
+
"""Test getting PR branch name."""
|
17
|
+
mock_event = MagicMock()
|
18
|
+
mock_event.event_data = {"issue": {"number": 123}}
|
19
|
+
mock_event.get_repo_data.return_value = {"head": {"ref": "feature-branch"}}
|
20
|
+
|
21
|
+
branch = get_pr_branch(mock_event)
|
22
|
+
|
23
|
+
assert branch == "feature-branch"
|
24
|
+
mock_event.get_repo_data.assert_called_once_with("pulls/123")
|
25
|
+
|
26
|
+
|
27
|
+
def test_trigger_and_get_workflow_info():
|
28
|
+
"""Test triggering workflows and getting info."""
|
29
|
+
mock_event = MagicMock()
|
30
|
+
mock_event.repository = "test/repo"
|
31
|
+
|
32
|
+
# Mock the workflow and runs responses separately
|
33
|
+
workflow_response = MagicMock()
|
34
|
+
workflow_response.status_code = 200
|
35
|
+
workflow_response.json.return_value = {"name": "CI Workflow"}
|
36
|
+
|
37
|
+
runs_response = MagicMock()
|
38
|
+
runs_response.status_code = 200
|
39
|
+
runs_response.json.return_value = {
|
40
|
+
"workflow_runs": [{"html_url": "https://github.com/test/repo/actions/runs/123", "run_number": 42}]
|
41
|
+
}
|
42
|
+
|
43
|
+
# Set up get method to return different responses for different URLs
|
44
|
+
def get_side_effect(url):
|
45
|
+
if "workflows/ci.yml" in url and "runs" not in url:
|
46
|
+
return workflow_response
|
47
|
+
elif "workflows/ci.yml/runs" in url:
|
48
|
+
return runs_response
|
49
|
+
# Return default response for unexpected URLs
|
50
|
+
default = MagicMock()
|
51
|
+
default.status_code = 404
|
52
|
+
return default
|
53
|
+
|
54
|
+
mock_event.get.side_effect = get_side_effect
|
55
|
+
|
56
|
+
# Use patch to skip time.sleep and limit to one workflow
|
57
|
+
with patch("time.sleep"):
|
58
|
+
with patch("actions.dispatch_actions.WORKFLOW_FILES", ["ci.yml"]):
|
59
|
+
results = trigger_and_get_workflow_info(mock_event, "feature-branch")
|
60
|
+
|
61
|
+
# Check results
|
62
|
+
assert len(results) == 1
|
63
|
+
assert results[0]["name"] == "CI Workflow"
|
64
|
+
assert results[0]["run_number"] == 42
|
65
|
+
|
66
|
+
|
67
|
+
def test_update_comment_function():
|
68
|
+
"""Test updating comment with workflow info."""
|
69
|
+
mock_event = MagicMock()
|
70
|
+
mock_event.repository = "test/repo"
|
71
|
+
mock_event.event_data = {"comment": {"id": 456}}
|
72
|
+
|
73
|
+
comment_body = f"Run tests please {RUN_CI_KEYWORD}"
|
74
|
+
triggered_actions = [
|
75
|
+
{
|
76
|
+
"name": "CI Workflow",
|
77
|
+
"file": "ci.yml",
|
78
|
+
"url": "https://github.com/test/repo/actions/workflows/ci.yml",
|
79
|
+
"run_number": 42,
|
80
|
+
}
|
81
|
+
]
|
82
|
+
|
83
|
+
# Mock datetime to have a consistent timestamp
|
84
|
+
with patch("actions.dispatch_actions.datetime") as mock_datetime:
|
85
|
+
mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0)
|
86
|
+
# Call without capturing return value
|
87
|
+
update_comment(mock_event, comment_body, triggered_actions, "feature-branch")
|
88
|
+
|
89
|
+
# Check that patch was called with expected content
|
90
|
+
mock_event.patch.assert_called_once()
|
91
|
+
|
92
|
+
# Verify key content in the comment body
|
93
|
+
args, kwargs = mock_event.patch.call_args
|
94
|
+
assert "https://api.github.com/repos/test/repo/issues/comments/456" in args[0]
|
95
|
+
assert "Actions Trigger" in kwargs["json"]["body"]
|
96
|
+
assert "CI Workflow" in kwargs["json"]["body"]
|
97
|
+
assert "2023-01-01 12:00:00 UTC" in kwargs["json"]["body"]
|
98
|
+
|
99
|
+
|
100
|
+
def test_main_triggers_workflows():
|
101
|
+
"""Test main function when comment contains trigger keyword."""
|
102
|
+
with patch("actions.dispatch_actions.Action") as MockAction:
|
103
|
+
# Configure mock
|
104
|
+
mock_event = MockAction.return_value
|
105
|
+
mock_event.event_name = "issue_comment"
|
106
|
+
mock_event.repository = "test/repo"
|
107
|
+
mock_event.event_data = {
|
108
|
+
"action": "created",
|
109
|
+
"issue": {"pull_request": {}},
|
110
|
+
"comment": {"body": f"Please run CI {RUN_CI_KEYWORD}", "user": {"login": "testuser"}, "id": 789},
|
111
|
+
}
|
112
|
+
mock_event.is_org_member.return_value = True
|
113
|
+
|
114
|
+
# Create minimal patches for the functions called by main
|
115
|
+
with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch:
|
116
|
+
with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger:
|
117
|
+
with patch("actions.dispatch_actions.update_comment"):
|
118
|
+
# Set return values
|
119
|
+
mock_get_branch.return_value = "feature-branch"
|
120
|
+
mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}]
|
121
|
+
|
122
|
+
# Call the function
|
123
|
+
main()
|
124
|
+
|
125
|
+
# Verify main component calls were made
|
126
|
+
mock_event.is_org_member.assert_called_once_with("testuser")
|
127
|
+
mock_get_branch.assert_called_once()
|
128
|
+
mock_trigger.assert_called_once()
|
129
|
+
|
130
|
+
|
131
|
+
def test_main_skips_non_pr_comments():
|
132
|
+
"""Test main function skips non-PR comments."""
|
133
|
+
with patch("actions.dispatch_actions.Action") as MockAction:
|
134
|
+
# Configure mock
|
135
|
+
mock_event = MockAction.return_value
|
136
|
+
mock_event.event_name = "issue_comment"
|
137
|
+
mock_event.event_data = {
|
138
|
+
"action": "created",
|
139
|
+
"issue": {}, # No pull_request key
|
140
|
+
"comment": {"body": f"Please run CI {RUN_CI_KEYWORD}", "user": {"login": "testuser"}},
|
141
|
+
}
|
142
|
+
|
143
|
+
main()
|
144
|
+
|
145
|
+
# Verify toggle_eyes_reaction was not called
|
146
|
+
mock_event.toggle_eyes_reaction.assert_not_called()
|