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.
Files changed (33) hide show
  1. {ultralytics_actions-0.0.72/ultralytics_actions.egg-info → ultralytics_actions-0.0.73}/PKG-INFO +32 -15
  2. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/README.md +30 -14
  3. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/__init__.py +1 -1
  4. ultralytics_actions-0.0.73/actions/update_file_headers.py +191 -0
  5. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/github_utils.py +5 -0
  6. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/pyproject.toml +2 -0
  7. ultralytics_actions-0.0.73/tests/test_cli_commands.py +19 -0
  8. ultralytics_actions-0.0.73/tests/test_common_utils.py +63 -0
  9. ultralytics_actions-0.0.73/tests/test_dispatch_actions.py +146 -0
  10. ultralytics_actions-0.0.73/tests/test_first_interaction.py +116 -0
  11. ultralytics_actions-0.0.73/tests/test_github_utils.py +64 -0
  12. ultralytics_actions-0.0.73/tests/test_init.py +84 -0
  13. ultralytics_actions-0.0.73/tests/test_openai_utils.py +65 -0
  14. ultralytics_actions-0.0.73/tests/test_summarize_pr.py +73 -0
  15. ultralytics_actions-0.0.73/tests/test_summarize_release.py +127 -0
  16. ultralytics_actions-0.0.73/tests/test_update_markdown_codeblocks.py +106 -0
  17. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73/ultralytics_actions.egg-info}/PKG-INFO +32 -15
  18. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/SOURCES.txt +11 -0
  19. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/entry_points.txt +1 -0
  20. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/requires.txt +1 -0
  21. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/LICENSE +0 -0
  22. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/dispatch_actions.py +0 -0
  23. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/first_interaction.py +0 -0
  24. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/summarize_pr.py +0 -0
  25. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/summarize_release.py +0 -0
  26. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/update_markdown_code_blocks.py +0 -0
  27. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/__init__.py +0 -0
  28. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/common_utils.py +0 -0
  29. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/actions/utils/openai_utils.py +0 -0
  30. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/setup.cfg +0 -0
  31. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/tests/test_urls.py +0 -0
  32. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
  33. {ultralytics_actions-0.0.72 → ultralytics_actions-0.0.73}/ultralytics_actions.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.0.72
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
  [![GitHub Actions Marketplace](https://img.shields.io/badge/Marketplace-Ultralytics_Actions-blue?style=flat&logo=github)](https://github.com/marketplace/actions/ultralytics-actions)
46
+
47
+ [![Actions CI](https://github.com/ultralytics/actions/actions/workflows/ci.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
45
48
  [![Ultralytics Actions](https://github.com/ultralytics/actions/actions/workflows/format.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/format.yml)
49
+ [![codecov](https://codecov.io/github/ultralytics/actions/graph/badge.svg?token=DoizJ1WS6j)](https://codecov.io/github/ultralytics/actions)
50
+ [![PyPI version](https://badge.fury.io/py/ultralytics-actions.svg)](https://badge.fury.io/py/ultralytics-actions)
51
+ [![Downloads](https://static.pepy.tech/badge/ultralytics-actions)](https://www.pepy.tech/projects/ultralytics-actions)
52
+
46
53
  [![Ultralytics Discord](https://img.shields.io/discord/1089800235347353640?logo=discord&logoColor=white&label=Discord&color=blue)](https://discord.com/invite/ultralytics)
47
54
  [![Ultralytics Forums](https://img.shields.io/discourse/users?server=https%3A%2F%2Fcommunity.ultralytics.com&logo=discourse&label=Forums&color=blue)](https://community.ultralytics.com/)
48
55
  [![Ultralytics Reddit](https://img.shields.io/reddit/subreddit-subscribers/ultralytics?style=flat&logo=reddit&logoColor=white&label=Reddit&color=blue)](https://reddit.com/r/ultralytics)
49
- [![PyPI version](https://badge.fury.io/py/ultralytics-actions.svg)](https://badge.fury.io/py/ultralytics-actions)
50
- [![Downloads](https://static.pepy.tech/badge/ultralytics-actions)](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
- format:
98
- runs-on: ubuntu-latest # Use 'macos-latest' if 'swift: true'
112
+ actions:
113
+ runs-on: ubuntu-latest
99
114
  steps:
100
- - name: Run Ultralytics Formatting
115
+ - name: Run Ultralytics Actions
101
116
  uses: ultralytics/actions@main
102
117
  with:
103
- token: ${{ secrets.GITHUB_TOKEN }} # Automatically generated, do not modify
104
- labels: true # Autolabel issues and PRs using GPT-4.1 (requires 'openai_api_key')
105
- python: true # Format Python code and docstrings with Ruff and docformatter
106
- prettier: true # Format YAML, JSON, Markdown, and CSS with Prettier
107
- swift: false # Format Swift code with swift-format (requires 'runs-on: macos-latest')
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 for broken links with Lychee
110
- summary: true # Generate PR summary with GPT-4.1 (requires 'openai_api_key')
111
- openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Add your OpenAI API key as a repository secret
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
  [![GitHub Actions Marketplace](https://img.shields.io/badge/Marketplace-Ultralytics_Actions-blue?style=flat&logo=github)](https://github.com/marketplace/actions/ultralytics-actions)
8
+
9
+ [![Actions CI](https://github.com/ultralytics/actions/actions/workflows/ci.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
8
10
  [![Ultralytics Actions](https://github.com/ultralytics/actions/actions/workflows/format.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/format.yml)
11
+ [![codecov](https://codecov.io/github/ultralytics/actions/graph/badge.svg?token=DoizJ1WS6j)](https://codecov.io/github/ultralytics/actions)
12
+ [![PyPI version](https://badge.fury.io/py/ultralytics-actions.svg)](https://badge.fury.io/py/ultralytics-actions)
13
+ [![Downloads](https://static.pepy.tech/badge/ultralytics-actions)](https://www.pepy.tech/projects/ultralytics-actions)
14
+
9
15
  [![Ultralytics Discord](https://img.shields.io/discord/1089800235347353640?logo=discord&logoColor=white&label=Discord&color=blue)](https://discord.com/invite/ultralytics)
10
16
  [![Ultralytics Forums](https://img.shields.io/discourse/users?server=https%3A%2F%2Fcommunity.ultralytics.com&logo=discourse&label=Forums&color=blue)](https://community.ultralytics.com/)
11
17
  [![Ultralytics Reddit](https://img.shields.io/reddit/subreddit-subscribers/ultralytics?style=flat&logo=reddit&logoColor=white&label=Reddit&color=blue)](https://reddit.com/r/ultralytics)
12
- [![PyPI version](https://badge.fury.io/py/ultralytics-actions.svg)](https://badge.fury.io/py/ultralytics-actions)
13
- [![Downloads](https://static.pepy.tech/badge/ultralytics-actions)](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
- format:
61
- runs-on: ubuntu-latest # Use 'macos-latest' if 'swift: true'
74
+ actions:
75
+ runs-on: ubuntu-latest
62
76
  steps:
63
- - name: Run Ultralytics Formatting
77
+ - name: Run Ultralytics Actions
64
78
  uses: ultralytics/actions@main
65
79
  with:
66
- token: ${{ secrets.GITHUB_TOKEN }} # Automatically generated, do not modify
67
- labels: true # Autolabel issues and PRs using GPT-4.1 (requires 'openai_api_key')
68
- python: true # Format Python code and docstrings with Ruff and docformatter
69
- prettier: true # Format YAML, JSON, Markdown, and CSS with Prettier
70
- swift: false # Format Swift code with swift-format (requires 'runs-on: macos-latest')
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 for broken links with Lychee
73
- summary: true # Generate PR summary with GPT-4.1 (requires 'openai_api_key')
74
- openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Add your OpenAI API key as a repository secret
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`.
@@ -22,4 +22,4 @@
22
22
  # ├── test_summarize_pr.py
23
23
  # └── ...
24
24
 
25
- __version__ = "0.0.72"
25
+ __version__ = "0.0.73"
@@ -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()