Ultralytics-Actions Python package (#243)

Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
Glenn Jocher
2024-10-16 01:51:19 +02:00
committed by GitHub
co-authored by UltralyticsAssistant
parent 776d3ac485
commit 5f84281dad
13 changed files with 1239 additions and 84 deletions
+148
View File
@@ -0,0 +1,148 @@
# Ultralytics Actions 🚀, AGPL-3.0 license
# Publish pip package to PyPI https://pypi.org/project/ultralytics-actions/
name: Publish to PyPI
on:
push:
branches: [main]
workflow_dispatch:
inputs:
pypi:
type: boolean
description: Publish to PyPI
jobs:
publish:
if: github.repository == 'ultralytics/actions' && github.actor == 'glenn-jocher'
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write # for PyPI trusted publishing
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets._GITHUB_TOKEN }} # use your PAT here
- name: Git config
run: |
git config --global user.name "UltralyticsAssistant"
git config --global user.email "[email protected]"
- name: Set up Python environment
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: "pip" # caching pip dependencies
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install requests build twine toml
- name: Check PyPI version
shell: python
run: |
import os
import requests
import toml
# Load version and package name from pyproject.toml
pyproject = toml.load('pyproject.toml')
package_name = pyproject['project']['name']
local_version = pyproject['project'].get('version', 'dynamic')
# If version is dynamic, extract it from the specified file
if local_version == 'dynamic':
version_attr = pyproject['tool']['setuptools']['dynamic']['version']['attr']
module_path, attr_name = version_attr.rsplit('.', 1)
with open(f"{module_path.replace('.', '/')}/__init__.py") as f:
local_version = next(line.split('=')[1].strip().strip("'\"") for line in f if line.startswith(attr_name))
print(f"Local Version: {local_version}")
# Get online version from PyPI
response = requests.get(f"https://pypi.org/pypi/{package_name}/json")
online_version = response.json()['info']['version'] if response.status_code == 200 else None
print(f"Online Version: {online_version or 'Not Found'}")
# Determine if a new version should be published
publish = False
if online_version:
local_ver = tuple(map(int, local_version.split('.')))
online_ver = tuple(map(int, online_version.split('.')))
major_diff = local_ver[0] - online_ver[0]
minor_diff = local_ver[1] - online_ver[1]
patch_diff = local_ver[2] - online_ver[2]
publish = (
(major_diff == 0 and minor_diff == 0 and 0 < patch_diff <= 2) or
(major_diff == 0 and minor_diff == 1 and local_ver[2] == 0) or
(major_diff == 1 and local_ver[1] == 0 and local_ver[2] == 0)
)
else:
publish = True # First release
os.system(f'echo "increment={publish}" >> $GITHUB_OUTPUT')
os.system(f'echo "current_tag=v{local_version}" >> $GITHUB_OUTPUT')
os.system(f'echo "previous_tag=v{online_version}" >> $GITHUB_OUTPUT')
if publish:
print('Ready to publish new version to PyPI ✅.')
id: check_pypi
- name: Build package
if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True'
run: python -m build
- name: Publish to PyPI
continue-on-error: true
if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
- name: Publish new tag
continue-on-error: true
if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True'
run: |
git tag -a "${{ steps.check_pypi.outputs.current_tag }}" -m "$(git log -1 --pretty=%B)" # i.e. "v0.1.2 commit message"
git push origin "${{ steps.check_pypi.outputs.current_tag }}"
- name: Publish new release
continue-on-error: true
if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
CURRENT_TAG: ${{ steps.check_pypi.outputs.current_tag }}
PREVIOUS_TAG: ${{ steps.check_pypi.outputs.previous_tag }}
run: |
curl -s "https://raw.githubusercontent.com/ultralytics/actions/main/utils/summarize_release.py" | python -
shell: bash
- name: Extract PR Details
env:
GH_TOKEN: ${{ secrets._GITHUB_TOKEN }}
run: |
# Check if the event is a pull request or pull_request_target
if [ "${{ github.event_name }}" = "pull_request" ] || [ "${{ github.event_name }}" = "pull_request_target" ]; then
PR_NUMBER=${{ github.event.pull_request.number }}
PR_TITLE=$(gh pr view $PR_NUMBER --json title --jq '.title')
else
# Use gh to find the PR associated with the commit
COMMIT_SHA=${{ github.event.after }}
PR_JSON=$(gh pr list --search "${COMMIT_SHA}" --state merged --json number,title --jq '.[0]')
PR_NUMBER=$(echo $PR_JSON | jq -r '.number')
PR_TITLE=$(echo $PR_JSON | jq -r '.title')
fi
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
echo "PR_TITLE=$PR_TITLE" >> $GITHUB_ENV
- name: Notify on Slack (Success)
if: success() && github.event_name == 'push' && steps.check_pypi.outputs.increment == 'True'
uses: slackapi/[email protected]
with:
payload: |
{"text": "<!channel> GitHub Actions success for ${{ github.workflow }} ✅\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* NEW '${{ github.repository }} ${{ steps.check_pypi.outputs.current_tag }}' pip package published 😃\n*Job Status:* ${{ job.status }}\n*Pull Request:* <https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}> ${{ env.PR_TITLE }}\n"}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
- name: Notify on Slack (Failure)
if: failure()
uses: slackapi/[email protected]
with:
payload: |
{"text": "<!channel> GitHub Actions error for ${{ github.workflow }} ❌\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* ${{ github.event_name }}\n*Job Status:* ${{ job.status }}\n*Pull Request:* <https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}> ${{ env.PR_TITLE }}\n"}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
-76
View File
@@ -1,76 +0,0 @@
# Ultralytics 🚀 - AGPL-3.0 License https://ultralytics.com/license
# Ultralytics Actions https://github.com/ultralytics/actions
# This workflow automatically publishes a new repository tag and release
name: Tag and Release
on:
workflow_dispatch:
inputs:
tag_name:
description: "Tag name (e.g., v0.0.0)"
required: true
type: string
publish_tag:
description: "Publish new tag"
required: true
type: boolean
default: true
publish_release:
description: "Publish new release"
required: true
type: boolean
default: true
jobs:
tag-and-release:
if: github.repository == 'ultralytics/actions' && github.actor == 'glenn-jocher'
name: Tag and Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets._GITHUB_TOKEN }}
- name: Git config
run: |
git config --global user.name "UltralyticsAssistant"
git config --global user.email "[email protected]"
- name: Check if tag exists
id: check_tag
run: |
if git rev-parse ${{ github.event.inputs.tag_name }} >/dev/null 2>&1; then
echo "Tag ${{ github.event.inputs.tag_name }} already exists"
echo "tag_exists=true" >> $GITHUB_OUTPUT
else
echo "Tag ${{ github.event.inputs.tag_name }} does not exist"
echo "tag_exists=false" >> $GITHUB_OUTPUT
fi
- name: Publish new tag
if: steps.check_tag.outputs.tag_exists == 'false'
run: |
git tag -a "${{ github.event.inputs.tag_name }}" -m "$(git log -1 --pretty=%B)"
git push origin "${{ github.event.inputs.tag_name }}"
- name: Set up Python environment
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install requests
- name: Publish new release
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
CURRENT_TAG: ${{ github.event.inputs.tag_name }}
run: |
curl -s "https://raw.githubusercontent.com/ultralytics/actions/main/utils/summarize_release.py" | python -
shell: bash
+6 -8
View File
@@ -1,4 +1,4 @@
# Ultralytics YOLO 🚀, AGPL-3.0 License https://ultralytics.com/license
# Ultralytics Actions 🚀, AGPL-3.0 License https://ultralytics.com/license
name: "Ultralytics Actions"
author: "Ultralytics"
@@ -93,8 +93,7 @@ runs:
# Note tomli required for codespell with pyproject.toml
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.action != 'closed'
run: |
pip install -q \
ruff docformatter codespell tomli
pip install ultralytics-actions codespell tomli
shell: bash
# PR Summary -------------------------------------------------------------------------------------------------------
@@ -106,8 +105,7 @@ runs:
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
OPENAI_MODEL: ${{ inputs.openai_model }}
run: |
pip install requests
curl -s "https://raw.githubusercontent.com/ultralytics/actions/main/utils/summarize_pr.py" | python -
ultralytics-actions-summarize-pr
shell: bash
continue-on-error: true
@@ -152,7 +150,8 @@ runs:
- name: Run Prettier
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && (inputs.prettier == 'true' || inputs.markdown == 'true') && github.event.action != 'closed'
run: |
curl -s "https://raw.githubusercontent.com/ultralytics/actions/main/utils/update_markdown_code_blocks.py" | python -
ultralytics-actions-update-markdown-code-blocks
npm install --global prettier
# npx prettier --write "**/*.{js,jsx,ts,tsx,css,less,scss,json,md,mdx,yml,yaml,html,vue,svelte}"
npx prettier --write "**/*.{css,json,yml,yaml}"
@@ -221,8 +220,7 @@ runs:
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
OPENAI_MODEL: ${{ inputs.openai_model }}
run: |
pip install requests
curl -s "https://raw.githubusercontent.com/ultralytics/actions/main/utils/first_interaction.py" | python -
ultralytics-actions-first-interaction
shell: bash
continue-on-error: true
+31
View File
@@ -0,0 +1,31 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
# project_root/
# ├── pyproject.toml
# ├── README.md
# ├── LICENSE
# ├── .gitignore
# ├── actions/
# │ ├── __init__.py
# │ ├── utils/
# │ │ ├── __init__.py
# │ │ ├── github_utils.py
# │ │ ├── openai_utils.py
# │ │ └── common_utils.py
# │ ├── first_interaction.py
# │ ├── summarize_pr.py
# │ ├── summarize_release.py
# │ └── update_markdown_code_blocks.py
# └── tests/
# ├── __init__.py
# ├── test_first_interaction.py
# ├── test_summarize_pr.py
# └── ...
from .first_interaction import main as first_interaction_main
from .summarize_pr import main as summarize_pr_main
from .summarize_release import main as summarize_release_main
from .update_markdown_code_blocks import main as update_markdown_code_blocks_main
__all__ = ["first_interaction_main", "summarize_pr_main", "summarize_release_main", "update_markdown_code_blocks_main"]
__version__ = "0.0.5"
+401
View File
@@ -0,0 +1,401 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import json
import os
from typing import Dict, List, Tuple
import requests
from .utils import (
GITHUB_API_URL,
GITHUB_EVENT_NAME,
GITHUB_EVENT_PATH,
GITHUB_HEADERS,
REPO_NAME,
get_completion,
get_github_data,
get_pr_diff,
graphql_request,
remove_html_comments,
)
# Environment variables
BLOCK_USER = os.getenv("BLOCK_USER", "false").lower() == "true"
def get_event_content() -> Tuple[int, str, str, str, str, str, str]:
"""Extracts the number, node_id, title, body, username, and issue_type."""
with open(GITHUB_EVENT_PATH) as f:
data = json.load(f)
action = data["action"] # 'opened', 'closed', 'created' (discussion), etc.
if GITHUB_EVENT_NAME == "issues":
item = data["issue"]
issue_type = "issue"
elif GITHUB_EVENT_NAME in ["pull_request", "pull_request_target"]:
pr_number = data["pull_request"]["number"]
item = get_github_data(f"pulls/{pr_number}")
issue_type = "pull request"
elif GITHUB_EVENT_NAME == "discussion":
item = data["discussion"]
issue_type = "discussion"
else:
raise ValueError(f"Unsupported event type: {GITHUB_EVENT_NAME}")
number = item["number"]
node_id = item.get("node_id") or item.get("id")
title = item["title"]
body = remove_html_comments(item.get("body", ""))
username = item["user"]["login"]
return number, node_id, title, body, username, issue_type, action
def update_issue_pr_content(number: int, node_id: str, issue_type: str):
"""Updates the title and body of the issue, pull request, or discussion."""
new_title = "Content Under Review"
new_body = """This post has been flagged for review by [Ultralytics Actions](https://ultralytics.com/actions) due to possible spam, abuse, or off-topic content. For more information please see our:
- [Code of Conduct](https://docs.ultralytics.com/help/code_of_conduct)
- [Security Policy](https://docs.ultralytics.com/help/security)
For questions or bug reports related to this action please visit https://github.com/ultralytics/actions.
Thank you 🙏
"""
if issue_type == "discussion":
mutation = """
mutation($discussionId: ID!, $title: String!, $body: String!) {
updateDiscussion(input: {discussionId: $discussionId, title: $title, body: $body}) {
discussion {
id
}
}
}
"""
graphql_request(mutation, variables={"discussionId": node_id, "title": new_title, "body": new_body})
else:
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/issues/{number}"
r = requests.patch(url, json={"title": new_title, "body": new_body}, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} update: {r.status_code}")
def close_issue_pr(number: int, node_id: str, issue_type: str):
"""Closes the issue, pull request, or discussion."""
if issue_type == "discussion":
mutation = """
mutation($discussionId: ID!) {
closeDiscussion(input: {discussionId: $discussionId}) {
discussion {
id
}
}
}
"""
graphql_request(mutation, variables={"discussionId": node_id})
else:
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/issues/{number}"
r = requests.patch(url, json={"state": "closed"}, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code == 200 else 'Fail'} issue/PR #{number} close: {r.status_code}")
def lock_issue_pr(number: int, node_id: str, issue_type: str):
"""Locks the issue, pull request, or discussion."""
if issue_type == "discussion":
mutation = """
mutation($lockableId: ID!, $lockReason: LockReason) {
lockLockable(input: {lockableId: $lockableId, lockReason: $lockReason}) {
lockedRecord {
... on Discussion {
id
}
}
}
}
"""
graphql_request(mutation, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
else:
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/issues/{number}/lock"
r = requests.put(url, json={"lock_reason": "off-topic"}, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code in {200, 204} else 'Fail'} issue/PR #{number} lock: {r.status_code}")
def block_user(username: str):
"""Blocks a user from the organization."""
url = f"{GITHUB_API_URL}/orgs/{REPO_NAME.split('/')[0]}/blocks/{username}"
r = requests.put(url, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code == 204 else 'Fail'} user block for {username}: {r.status_code}")
def get_relevant_labels(
issue_type: str, title: str, body: str, available_labels: Dict, current_labels: List
) -> List[str]:
"""Uses OpenAI to determine the most relevant labels."""
# Remove mutually exclusive labels like both 'bug' and 'question' or inappropriate labels like 'help wanted'
for label in ["help wanted", "TODO"]: # normal case
available_labels.pop(label, None) # remove as should only be manually added
if "bug" in current_labels:
available_labels.pop("question", None)
elif "question" in current_labels:
available_labels.pop("bug", None)
# Add "Alert" to available labels if not present
if "Alert" not in available_labels:
available_labels["Alert"] = (
"Potential spam, abuse, or illegal activity including advertising, unsolicited promotions, malware, phishing, crypto offers, pirated software or media, free movie downloads, cracks, keygens or any other content that violates terms of service or legal standards."
)
labels = "\n".join(f"- {name}: {description}" for name, description in available_labels.items())
prompt = f"""Select the top 1-3 most relevant labels for the following GitHub {issue_type}.
INSTRUCTIONS:
1. Review the {issue_type} title and description.
2. Consider the available labels and their descriptions.
3. Choose 1-3 labels that best match the {issue_type} content.
4. Only use the "Alert" label when you have high confidence that this is an inappropriate {issue_type}.
5. Respond ONLY with the chosen label names (no descriptions), separated by commas.
6. If no labels are relevant, respond with 'None'.
AVAILABLE LABELS:
{labels}
{issue_type.upper()} TITLE:
{title}
{issue_type.upper()} DESCRIPTION:
{body[:16000]}
YOUR RESPONSE (label names only):
"""
print(prompt) # for short-term debugging
messages = [
{
"role": "system",
"content": "You are a helpful assistant that labels GitHub issues, pull requests, and discussions.",
},
{"role": "user", "content": prompt},
]
suggested_labels = get_completion(messages)
if "none" in suggested_labels.lower():
return []
available_labels_lower = {name.lower(): name for name in available_labels}
return [
available_labels_lower[label.lower().strip()]
for label in suggested_labels.split(",")
if label.lower().strip() in available_labels_lower
]
def get_label_ids(labels: List[str]) -> List[str]:
"""Retrieves GitHub label IDs for a list of label names using the GraphQL API."""
query = """
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
labels(first: 100, query: "") {
nodes {
id
name
}
}
}
}
"""
owner, repo = REPO_NAME.split("/")
result = graphql_request(query, variables={"owner": owner, "name": repo})
if "data" in result and "repository" in result["data"]:
all_labels = result["data"]["repository"]["labels"]["nodes"]
label_map = {label["name"].lower(): label["id"] for label in all_labels}
return [label_map.get(label.lower()) for label in labels if label.lower() in label_map]
else:
print(f"Failed to fetch labels: {result.get('errors', 'Unknown error')}")
return []
def apply_labels(number: int, node_id: str, labels: List[str], issue_type: str):
"""Applies the given labels to the issue, pull request, or discussion."""
if "Alert" in labels:
create_alert_label()
if issue_type == "discussion":
print(f"Using node_id: {node_id}") # Debug print
label_ids = get_label_ids(labels)
if not label_ids:
print("No valid labels to apply.")
return
mutation = """
mutation($labelableId: ID!, $labelIds: [ID!]!) {
addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
labelable {
... on Discussion {
id
}
}
}
}
"""
graphql_request(mutation, {"labelableId": node_id, "labelIds": label_ids})
print(f"Successfully applied labels: {', '.join(labels)}")
else:
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/issues/{number}/labels"
r = requests.post(url, json={"labels": labels}, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code == 200 else 'Fail'} apply labels {', '.join(labels)}: {r.status_code}")
def create_alert_label():
"""Creates the 'Alert' label in the repository if it doesn't exist."""
alert_label = {"name": "Alert", "color": "FF0000", "description": "Potential spam, abuse, or off-topic."}
requests.post(f"{GITHUB_API_URL}/repos/{REPO_NAME}/labels", json=alert_label, headers=GITHUB_HEADERS)
def is_org_member(username: str) -> bool:
"""Checks if a user is a member of the organization."""
org_name = REPO_NAME.split("/")[0]
url = f"{GITHUB_API_URL}/orgs/{org_name}/members/{username}"
r = requests.get(url, headers=GITHUB_HEADERS)
return r.status_code == 204 # 204 means the user is a member
def add_comment(number: int, node_id: str, comment: str, issue_type: str):
"""Adds a comment to the issue, pull request, or discussion."""
if issue_type == "discussion":
mutation = """
mutation($discussionId: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
comment {
id
}
}
}
"""
graphql_request(mutation, variables={"discussionId": node_id, "body": comment})
else:
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/issues/{number}/comments"
r = requests.post(url, json={"body": comment}, headers=GITHUB_HEADERS)
print(f"{'Successful' if r.status_code in {200, 201} else 'Fail'} issue/PR #{number} comment: {r.status_code}")
def get_first_interaction_response(issue_type: str, title: str, body: str, username: str, number: int) -> str:
"""Generates a custom response using LLM based on the issue/PR content and instructions."""
issue_discussion_response = f"""
👋 Hello @{username}, thank you for submitting a `{REPO_NAME}` 🚀 {issue_type.capitalize()}. To help us address your concern efficiently, please ensure you've provided the following information:
1. For bug reports:
- A clear and concise description of the bug
- A minimum reproducible example (MRE)[https://docs.ultralytics.com/help/minimum_reproducible_example/] that demonstrates the issue
- Your environment details (OS, Python version, package versions)
- Expected behavior vs. actual behavior
- Any error messages or logs related to the issue
2. For feature requests:
- A clear and concise description of the proposed feature
- The problem this feature would solve
- Any alternative solutions you've considered
3. For questions:
- Provide as much context as possible about your question
- Include any research you've already done on the topic
- Specify which parts of the [documentation](https://docs.ultralytics.com), if any, you've already consulted
Please make sure you've searched existing {issue_type}s to avoid duplicates. If you need to add any additional information, please comment on this {issue_type}.
Thank you for your contribution to improving our project!
"""
pr_response = f"""
👋 Hello @{username}, thank you for submitting an `{REPO_NAME}` 🚀 PR! To ensure a seamless integration of your work, please review the following checklist:
- ✅ **Define a Purpose**: Clearly explain the purpose of your fix or feature in your PR description, and link to any [relevant issues](https://github.com/{REPO_NAME}/issues). Ensure your commit messages are clear, concise, and adhere to the project's conventions.
- ✅ **Synchronize with Source**: Confirm your PR is synchronized with the `{REPO_NAME}` `main` branch. If it's behind, update it by clicking the 'Update branch' button or by running `git pull` and `git merge main` locally.
- ✅ **Ensure CI Checks Pass**: Verify all Ultralytics [Continuous Integration (CI)](https://docs.ultralytics.com/help/CI/) checks are passing. If any checks fail, please address the issues.
- ✅ **Update Documentation**: Update the relevant [documentation](https://docs.ultralytics.com) for any new or modified features.
- ✅ **Add Tests**: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
- ✅ **Sign the CLA**: Please ensure you have signed our [Contributor License Agreement](https://docs.ultralytics.com/help/CLA/) if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
- ✅ **Minimize Changes**: Limit your changes to the **minimum** necessary for your bug fix or feature addition. _"It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is."_ — Bruce Lee
For more guidance, please refer to our [Contributing Guide](https://docs.ultralytics.com/help/contributing). Don’t hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀
"""
if issue_type == "pull request":
example = os.getenv("FIRST_PR_RESPONSE") or pr_response
else:
example = os.getenv("FIRST_ISSUE_RESPONSE") or issue_discussion_response
org_name, repo_name = REPO_NAME.split("/")
repo_url = f"https://github.com/{REPO_NAME}"
diff = get_pr_diff(number)[:32000] if issue_type == "pull request" else ""
prompt = f"""Generate a customized response to the new GitHub {issue_type} below:
CONTEXT:
- Repository: {repo_name}
- Organization: {org_name}
- Repository URL: {repo_url}
- User: {username}
INSTRUCTIONS:
- Do not answer the question or resolve the issue directly
- Adapt the example {issue_type} response below as appropriate, keeping all badges, links and references provided
- For bug reports, specifically request a minimum reproducible example (MRE) if not provided
- INCLUDE ALL LINKS AND INSTRUCTIONS IN THE EXAMPLE BELOW, customized as appropriate
- In your response, mention to the user that this is an automated response and that an Ultralytics engineer will also assist soon
- Do not add a sign-off or valediction like "best regards" at the end of your response
- Do not add spaces between bullet points or numbered lists
- Only link to files or URLs in the example below, do not add external links
- Use a few emojis to enliven your response
EXAMPLE {issue_type.upper()} RESPONSE:
{example}
{issue_type.upper()} TITLE:
{title}
{issue_type.upper()} DESCRIPTION:
{body[:16000]}
{"PULL REQUEST DIFF:" if issue_type == "pull request" else ""}
{diff if issue_type == "pull request" else ""}
YOUR {issue_type.upper()} RESPONSE:
"""
print(f"\n\n{prompt}\n\n") # for debug
messages = [
{
"role": "system",
"content": f"You are a helpful assistant responding to GitHub {issue_type}s for the {org_name} organization.",
},
{"role": "user", "content": prompt},
]
return get_completion(messages)
def main():
"""Runs autolabel action and adds custom response for new issues/PRs/Discussions."""
number, node_id, title, body, username, issue_type, action = get_event_content()
available_labels = get_github_data("labels")
label_descriptions = {label["name"]: label.get("description", "") for label in available_labels}
if issue_type == "discussion":
current_labels = [] # For discussions, labels may need to be fetched differently or adjusted
else:
current_labels = [label["name"].lower() for label in get_github_data(f"issues/{number}/labels")]
relevant_labels = get_relevant_labels(issue_type, title, body, label_descriptions, current_labels)
if relevant_labels:
apply_labels(number, node_id, relevant_labels, issue_type)
if "Alert" in relevant_labels and not is_org_member(username):
update_issue_pr_content(number, node_id, issue_type)
if issue_type != "pull request":
close_issue_pr(number, node_id, issue_type)
lock_issue_pr(number, node_id, issue_type)
if BLOCK_USER:
block_user(username=username)
else:
print("No relevant labels found or applied.")
if action in {"opened", "created"}:
custom_response = get_first_interaction_response(issue_type, title, body, username, number)
add_comment(number, node_id, custom_response, issue_type)
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import requests
from .utils import (
GITHUB_API_URL,
GITHUB_HEADERS,
PR_NUMBER,
REPO_NAME,
get_completion,
get_pr_diff,
)
# Action settings
SUMMARY_START = (
"## 🛠️ PR Summary\n\n<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)<sub>\n\n"
)
def generate_pr_summary(repo_name, diff_text):
"""Generates a professionally written yet accessible summary of a PR using OpenAI's API."""
if not diff_text:
diff_text = "**ERROR: DIFF IS EMPTY, THERE ARE ZERO CODE CHANGES IN THIS PR."
ratio = 3.3 # about 3.3 characters per token
limit = round(128000 * ratio * 0.5) # use up to 50% of the 128k context window for prompt
messages = [
{
"role": "system",
"content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub PRs from Ultralytics in a way that is accurate, concise, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple, concise terms.",
},
{
"role": "user",
"content": f"Summarize this '{repo_name}' PR, focusing on major changes, their purpose, and potential impact. Keep the summary clear and concise, suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
f"### 🌟 Summary (single-line synopsis)\n"
f"### 📊 Key Changes (bullet points highlighting any major changes)\n"
f"### 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n"
f"\n\nHere's the PR diff:\n\n{diff_text[:limit]}",
},
]
reply = get_completion(messages)
if len(diff_text) > limit:
return SUMMARY_START + "**WARNING ⚠️** this PR is very large, summary may not cover all changes.\n\n" + reply
else:
return SUMMARY_START + reply
def update_pr_description(repo_name, pr_number, new_summary):
"""Updates the original PR description with a new summary, replacing an existing summary if found."""
# Fetch the current PR description
pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
pr_response = requests.get(pr_url, headers=GITHUB_HEADERS)
pr_data = pr_response.json()
current_description = pr_data.get("body") or "" # warning, can be None
# Check if existing summary is present and update accordingly
if SUMMARY_START in current_description:
updated_description = current_description.split(SUMMARY_START)[0] + new_summary
else:
updated_description = current_description + "\n\n" + new_summary
# Update the PR description
update_response = requests.patch(pr_url, json={"body": updated_description}, headers=GITHUB_HEADERS)
return update_response.status_code
def main():
"""Summarize PR."""
diff = get_pr_diff(PR_NUMBER)
# Generate PR summary
summary = generate_pr_summary(REPO_NAME, diff)
# Update PR description
status_code = update_pr_description(REPO_NAME, PR_NUMBER, summary)
if status_code == 200:
print("PR description updated successfully.")
else:
print(f"Failed to update PR description. Status code: {status_code}")
if __name__ == "__main__":
main()
+192
View File
@@ -0,0 +1,192 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import os
import re
import subprocess
import time
from datetime import datetime
import requests
from .utils import (
GITHUB_API_URL,
GITHUB_HEADERS,
GITHUB_HEADERS_DIFF,
GITHUB_TOKEN,
REPO_NAME,
get_completion,
remove_html_comments,
)
# Environment variables
CURRENT_TAG = os.getenv("CURRENT_TAG")
PREVIOUS_TAG = os.getenv("PREVIOUS_TAG")
def get_release_diff(repo_name: str, previous_tag: str, latest_tag: str) -> str:
"""Get the diff between two tags."""
url = f"{GITHUB_API_URL}/repos/{repo_name}/compare/{previous_tag}...{latest_tag}"
r = requests.get(url, headers=GITHUB_HEADERS_DIFF)
return r.text if r.status_code == 200 else f"Failed to get diff: {r.content}"
def get_prs_between_tags(repo_name: str, previous_tag: str, latest_tag: str) -> list:
"""Get PRs merged between two tags using the compare API."""
url = f"{GITHUB_API_URL}/repos/{repo_name}/compare/{previous_tag}...{latest_tag}"
r = requests.get(url, headers=GITHUB_HEADERS)
r.raise_for_status()
data = r.json()
pr_numbers = set()
for commit in data["commits"]:
pr_matches = re.findall(r"#(\d+)", commit["commit"]["message"])
pr_numbers.update(pr_matches)
prs = []
time.sleep(10) # sleep 10 seconds to allow final PR summary to update on merge
for pr_number in sorted(pr_numbers): # earliest to latest
pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
pr_response = requests.get(pr_url, headers=GITHUB_HEADERS)
if pr_response.status_code == 200:
pr_data = pr_response.json()
prs.append(
{
"number": pr_data["number"],
"title": pr_data["title"],
"body": remove_html_comments(pr_data["body"]),
"author": pr_data["user"]["login"],
"html_url": pr_data["html_url"],
"merged_at": pr_data["merged_at"],
}
)
# Sort PRs by merge date
prs.sort(key=lambda x: datetime.strptime(x["merged_at"], "%Y-%m-%dT%H:%M:%SZ"))
return prs
def get_new_contributors(repo: str, prs: list) -> set:
"""Identify genuinely new contributors in the current release."""
new_contributors = set()
for pr in prs:
author = pr["author"]
# Check if this is the author's first contribution
url = f"{GITHUB_API_URL}/search/issues?q=repo:{repo}+author:{author}+is:pr+is:merged&sort=created&order=asc"
r = requests.get(url, headers=GITHUB_HEADERS)
if r.status_code == 200:
data = r.json()
if data["total_count"] > 0:
first_pr = data["items"][0]
if first_pr["number"] == pr["number"]:
new_contributors.add(author)
return new_contributors
def generate_release_summary(diff: str, prs: list, latest_tag: str, previous_tag: str, repo_name: str) -> str:
"""Generate a summary for the release."""
pr_summaries = "\n\n".join(
[f"PR #{pr['number']}: {pr['title']} by @{pr['author']}\n{pr['body'][:1000]}" for pr in prs]
)
current_pr = prs[-1] if prs else None
current_pr_summary = (
f"Current PR #{current_pr['number']}: {current_pr['title']} by @{current_pr['author']}\n{current_pr['body'][:1000]}"
if current_pr
else "No current PR found."
)
whats_changed = "\n".join([f"* {pr['title']} by @{pr['author']} in {pr['html_url']}" for pr in prs])
# Generate New Contributors section
new_contributors = get_new_contributors(repo_name, prs)
new_contributors_section = (
"\n## New Contributors\n"
+ "\n".join(
[
f"* @{contributor} made their first contribution in {next(pr['html_url'] for pr in prs if pr['author'] == contributor)}"
for contributor in new_contributors
]
)
if new_contributors
else ""
)
full_changelog = f"https://github.com/{repo_name}/compare/{previous_tag}...{latest_tag}"
release_suffix = (
f"\n\n## What's Changed\n{whats_changed}\n{new_contributors_section}\n\n**Full Changelog**: {full_changelog}\n"
)
messages = [
{
"role": "system",
"content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub releases in a way that is detailed, accurate, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple and intuitive terms.",
},
{
"role": "user",
"content": f"Summarize the updates made in the '{latest_tag}' tag, focusing on major model or features changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Prioritize changes from the current PR (the first in the list), which is usually the most important in the release. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
f"## 🌟 Summary (single-line synopsis)\n"
f"## 📊 Key Changes (bullet points highlighting any major changes)\n"
f"## 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n\n\n"
f"Here's the information about the current PR:\n\n{current_pr_summary}\n\n"
f"Here's the information about PRs merged between the previous release and this one:\n\n{pr_summaries[:30000]}\n\n"
f"Here's the release diff:\n\n{diff[:300000]}",
},
]
print(messages[-1]["content"]) # for debug
return get_completion(messages) + release_suffix
def create_github_release(repo_name: str, tag_name: str, name: str, body: str) -> int:
"""Create a release on GitHub."""
url = f"{GITHUB_API_URL}/repos/{repo_name}/releases"
data = {"tag_name": tag_name, "name": name, "body": body, "draft": False, "prerelease": False}
r = requests.post(url, headers=GITHUB_HEADERS, json=data)
return r.status_code
def get_previous_tag() -> str:
"""Get the previous tag from git tags."""
cmd = ["git", "describe", "--tags", "--abbrev=0", "--exclude", CURRENT_TAG]
try:
return subprocess.run(cmd, check=True, text=True, capture_output=True).stdout.strip()
except subprocess.CalledProcessError:
print("Failed to get previous tag from git. Using previous commit.")
return "HEAD~1"
def main():
"""Automates generating and publishing a GitHub release summary from PRs and commit differences."""
if not all([GITHUB_TOKEN, CURRENT_TAG]):
raise ValueError("One or more required environment variables are missing.")
previous_tag = PREVIOUS_TAG or get_previous_tag()
# Get the diff between the tags
diff = get_release_diff(REPO_NAME, previous_tag, CURRENT_TAG)
# Get PRs merged between the tags
prs = get_prs_between_tags(REPO_NAME, previous_tag, CURRENT_TAG)
# Generate release summary
try:
summary = generate_release_summary(diff, prs, CURRENT_TAG, previous_tag, REPO_NAME)
except Exception as e:
print(f"Failed to generate summary: {str(e)}")
summary = "Failed to generate summary."
# Get the latest commit message
cmd = ["git", "log", "-1", "--pretty=%B"]
commit_message = subprocess.run(cmd, check=True, text=True, capture_output=True).stdout.split("\n")[0].strip()
# Create the release on GitHub
status_code = create_github_release(REPO_NAME, CURRENT_TAG, f"{CURRENT_TAG} - {commit_message}", summary)
if status_code == 201:
print(f"Successfully created release {CURRENT_TAG}")
else:
print(f"Failed to create release {CURRENT_TAG}. Status code: {status_code}")
if __name__ == "__main__":
main()
+173
View File
@@ -0,0 +1,173 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import hashlib
import re
import shutil
import subprocess
from pathlib import Path
def extract_code_blocks(markdown_content):
"""Extract Python code blocks with ``` followed by "python", "py", or "{ .py .annotate }"."""
pattern = r"^( *)```(?:python|py|\{[ ]*\.py[ ]*\.annotate[ ]*\})\n(.*?)\n\1```"
code_block_pattern = re.compile(pattern, re.DOTALL | re.MULTILINE)
return code_block_pattern.findall(markdown_content)
def remove_indentation(code_block, num_spaces):
"""Removes `num_spaces` leading spaces from each line in `code_block` and returns the modified string."""
lines = code_block.split("\n")
stripped_lines = [line[num_spaces:] if len(line) >= num_spaces else line for line in lines]
return "\n".join(stripped_lines)
def add_indentation(code_block, num_spaces):
"""Adds `num_spaces` leading spaces to each non-empty line in `code_block`."""
indent = " " * num_spaces
lines = code_block.split("\n")
indented_lines = [indent + line if line.strip() != "" else line for line in lines]
return "\n".join(indented_lines)
def format_code_with_ruff(temp_dir):
"""Formats all Python code files in the `temp_dir` directory using the 'ruff' linter tool."""
try:
# Run ruff format
subprocess.run(
[
"ruff",
"format",
"--line-length=120",
str(temp_dir),
],
check=True,
)
print("Completed ruff format ✅")
except Exception as e:
print(f"ERROR running ruff format ❌ {e}")
try:
# Run ruff check with ignored rules:
# F821: Undefined name
# F841: Local variable is assigned to but never used
subprocess.run(
[
"ruff",
"check",
"--fix",
"--extend-select=I",
"--ignore=F821,F841",
str(temp_dir),
],
check=True,
)
print("Completed ruff check ✅")
except Exception as e:
print(f"ERROR running ruff check ❌ {e}")
try:
# Run docformatter
subprocess.run(
[
"docformatter",
"--wrap-summaries=120",
"--wrap-descriptions=120",
"--pre-summary-newline",
"--close-quotes-on-newline",
"--in-place",
"--recursive",
str(temp_dir),
],
check=True,
)
print("Completed docformatter ✅")
except Exception as e:
print(f"ERROR running docformatter ❌ {e}")
def generate_temp_filename(file_path, index):
"""Generates a unique temporary filename based on the file path and index."""
unique_string = f"{file_path.parent}_{file_path.stem}_{index}"
unique_hash = hashlib.md5(unique_string.encode()).hexdigest()
return f"temp_{unique_hash}.py"
def process_markdown_file(file_path, temp_dir, verbose=False):
"""Reads a markdown file, extracts Python code blocks, saves them to temp files, and updates the file."""
try:
markdown_content = Path(file_path).read_text()
code_blocks = extract_code_blocks(markdown_content)
temp_files = []
for i, (num_spaces, code_block) in enumerate(code_blocks):
if verbose:
print(f"Extracting code block {i} from {file_path}")
num_spaces = len(num_spaces)
code_without_indentation = remove_indentation(code_block, num_spaces)
# Generate a unique temp file path
temp_file_path = temp_dir / generate_temp_filename(file_path, i)
with open(temp_file_path, "w") as temp_file:
temp_file.write(code_without_indentation)
temp_files.append((num_spaces, code_block, temp_file_path))
return markdown_content, temp_files
except Exception as e:
print(f"Error processing file {file_path}: {e}")
return None, None
def update_markdown_file(file_path, markdown_content, temp_files):
"""Updates the markdown file with formatted code blocks."""
for num_spaces, original_code_block, temp_file_path in temp_files:
try:
with open(temp_file_path) as temp_file:
formatted_code = temp_file.read().rstrip("\n") # Strip trailing newlines
formatted_code_with_indentation = add_indentation(formatted_code, num_spaces)
# Replace both `python` and `py` code blocks
for lang in ["python", "py", "{ .py .annotate }"]:
markdown_content = markdown_content.replace(
f"{' ' * num_spaces}```{lang}\n{original_code_block}\n{' ' * num_spaces}```",
f"{' ' * num_spaces}```{lang}\n{formatted_code_with_indentation}\n{' ' * num_spaces}```",
)
except Exception as e:
print(f"Error updating code block in file {file_path}: {e}")
try:
with open(file_path, "w") as file:
file.write(markdown_content)
except Exception as e:
print(f"Error writing file {file_path}: {e}")
def main(root_dir=Path.cwd(), verbose=False):
"""Processes all markdown files in a specified directory and its subdirectories."""
root_path = Path(root_dir)
markdown_files = list(root_path.rglob("*.md"))
temp_dir = Path("temp_code_blocks")
temp_dir.mkdir(exist_ok=True)
# Extract code blocks and save to temp files
all_temp_files = []
for markdown_file in markdown_files:
if verbose:
print(f"Processing {markdown_file}")
markdown_content, temp_files = process_markdown_file(markdown_file, temp_dir)
if markdown_content and temp_files:
all_temp_files.append((markdown_file, markdown_content, temp_files))
# Format all code blocks with ruff
format_code_with_ruff(temp_dir)
# Update markdown files with formatted code blocks
for markdown_file, markdown_content, temp_files in all_temp_files:
update_markdown_file(markdown_file, markdown_content, temp_files)
# Clean up temp directory
shutil.rmtree(temp_dir)
if __name__ == "__main__":
main()
+35
View File
@@ -0,0 +1,35 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
from .common_utils import remove_html_comments
from .github_utils import (
GITHUB_API_URL,
GITHUB_EVENT_NAME,
GITHUB_EVENT_PATH,
GITHUB_HEADERS,
GITHUB_HEADERS_DIFF,
GITHUB_TOKEN,
PR_NUMBER,
REPO_NAME,
get_github_data,
get_pr_diff,
graphql_request,
)
from .openai_utils import OPENAI_API_KEY, OPENAI_MODEL, get_completion
__all__ = (
"remove_html_comments",
"GITHUB_API_URL",
"GITHUB_HEADERS",
"GITHUB_HEADERS_DIFF",
"GITHUB_TOKEN",
"REPO_NAME",
"PR_NUMBER",
"GITHUB_EVENT_NAME",
"GITHUB_EVENT_PATH",
"get_github_data",
"get_pr_diff",
"graphql_request",
"OPENAI_API_KEY",
"OPENAI_MODEL",
"get_completion",
)
+8
View File
@@ -0,0 +1,8 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import re
def remove_html_comments(body: str) -> str:
"""Removes HTML comments from a string using regex pattern matching."""
return re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL).strip()
+44
View File
@@ -0,0 +1,44 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import os
import requests
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
GITHUB_API_URL = "https://api.github.com"
GITHUB_HEADERS = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"}
GITHUB_HEADERS_DIFF = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3.diff"}
PR_NUMBER = os.getenv("PR_NUMBER")
REPO_NAME = os.getenv("GITHUB_REPOSITORY")
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME")
GITHUB_EVENT_PATH = os.getenv("GITHUB_EVENT_PATH")
def get_pr_diff(pr_number: int) -> str:
"""Retrieves the diff content for a specified pull request in a GitHub repository."""
url = f"{GITHUB_API_URL}/repos/{REPO_NAME}/pulls/{pr_number}"
r = requests.get(url, headers=GITHUB_HEADERS_DIFF)
return r.text if r.status_code == 200 else ""
def get_github_data(endpoint: str) -> dict:
"""Fetches GitHub repository data from a specified endpoint using the GitHub API."""
r = requests.get(f"{GITHUB_API_URL}/repos/{REPO_NAME}/{endpoint}", headers=GITHUB_HEADERS)
r.raise_for_status()
return r.json()
def graphql_request(query: str, variables: dict = None) -> dict:
"""Executes a GraphQL query against the GitHub API and returns the response as a dictionary."""
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/vnd.github.v4+json",
}
r = requests.post(f"{GITHUB_API_URL}/graphql", json={"query": query, "variables": variables}, headers=headers)
r.raise_for_status()
result = r.json()
success = "data" in result and not result.get("errors")
print(f"{'Successful' if success else 'Fail'} discussion GraphQL request: {result.get('errors', 'No errors')}")
return result
+24
View File
@@ -0,0 +1,24 @@
# Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
import os
import requests
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
def get_completion(messages: list) -> str:
"""Generates a completion using OpenAI's API based on input messages."""
assert OPENAI_API_KEY, "OpenAI API key is required."
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {OPENAI_API_KEY}", "Content-Type": "application/json"}
data = {"model": OPENAI_MODEL, "messages": messages}
r = requests.post(url, headers=headers, json=data)
r.raise_for_status()
content = r.json()["choices"][0]["message"]["content"].strip()
remove = [" @giscus[bot]"]
for x in remove:
content = content.replace(x, "")
return content
+95
View File
@@ -0,0 +1,95 @@
# Ultralytics Actions 🚀, AGPL-3.0 license
# Overview:
# This pyproject.toml file manages the build, packaging, and distribution of the Ultralytics Actions package.
# It defines essential project metadata, dependencies, and settings used to develop and deploy the package.
# Key Sections:
# - [build-system]: Specifies the build requirements and backend (e.g., setuptools, wheel).
# - [project]: Includes details like name, version, description, authors, dependencies and more.
# - [project.optional-dependencies]: Provides additional, optional packages for extended features.
# - [tool.*]: Configures settings for various tools (pytest, ruff, etc.) used in the project.
# Installation:
# The Ultralytics Actions package is primarily used as a GitHub Actions workflow.
# For development purposes, you can install the package in editable mode with: 'pip install -e .'
# This approach allows for real-time code modifications without the need for re-installation.
# Documentation:
# For comprehensive documentation and usage instructions, visit: https://github.com/ultralytics/actions
[build-system]
requires = ["setuptools>=70.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "ultralytics-actions"
dynamic = ["version"]
description = "Ultralytics Actions for GitHub automation and PR management."
readme = "README.md"
requires-python = ">=3.8"
license = { text = "AGPL-3.0" }
keywords = [
"github-actions",
"ci-cd",
"workflow-automation",
"pull-request-automation",
"code-review",
"release-automation",
"markdown-processing",
"devops",
"github-integration",
"continuous-integration"
]
authors = [
{ name = "Glenn Jocher", email = "[email protected]" },
]
maintainers = [
{ name = "Ultralytics", email = "[email protected]" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet :: WWW/HTTP",
"Operating System :: OS Independent",
]
dependencies = [
"requests>=2.26.0",
"ruff>=0.1.6",
"docformatter>=1.7.5",
]
[project.optional-dependencies]
dev = [
"pytest",
"black",
]
[project.urls]
"Homepage" = "https://ultralytics.com"
"Source" = "https://github.com/ultralytics/actions"
"Documentation" = "https://docs.ultralytics.com"
"Bug Reports" = "https://github.com/ultralytics/actions/issues"
"Changelog" = "https://github.com/ultralytics/actions/releases"
[project.scripts]
ultralytics-actions-first-interaction = "actions.first_interaction:main"
ultralytics-actions-summarize-pr = "actions.summarize_pr:main"
ultralytics-actions-summarize-release = "actions.summarize_release:main"
ultralytics-actions-update-markdown-code-blocks = "actions.update_markdown_code_blocks:main"
[tool.setuptools]
packages = { find = { where = ["."], include = ["actions", "actions.*"] } }
[tool.setuptools.dynamic]
version = { attr = "actions.__version__" }