Co-authored-by: UltralyticsAssistant <[email protected]>
412 lines
18 KiB
YAML
412 lines
18 KiB
YAML
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
|
|
|
name: "Ultralytics Actions"
|
|
author: "Ultralytics"
|
|
description: "Optimize code and docs with official Ultralytics Actions for syntax, spelling, and link checks."
|
|
branding:
|
|
icon: "code"
|
|
color: "blue"
|
|
inputs:
|
|
token:
|
|
description: "GitHub token"
|
|
required: true
|
|
labels:
|
|
description: "Run issue and PR auto-labeling"
|
|
required: false
|
|
default: "false"
|
|
python:
|
|
description: "Run Python formatting"
|
|
required: false
|
|
default: "false"
|
|
python-version:
|
|
description: "Python version to set up before running Python tooling (empty = runner default)"
|
|
required: false
|
|
default: ""
|
|
python_docstrings:
|
|
description: "Run Python docstring formatting"
|
|
required: false
|
|
default: "true"
|
|
markdown:
|
|
description: "Run Markdown formatting (deprecated in favor of prettier)"
|
|
required: false
|
|
default: "false"
|
|
biome:
|
|
description: "Run Biome formatting"
|
|
required: false
|
|
default: "false"
|
|
prettier:
|
|
description: "Run Prettier formatting for JavaScript, JSX, Angular, Vue, Flow, TypeScript, CSS, HTML, JSON, GraphQL, Markdown, YAML"
|
|
required: false
|
|
default: "false"
|
|
swift:
|
|
description: "Run Swift formatting"
|
|
required: false
|
|
default: "false"
|
|
dart:
|
|
description: "Run Dart formatting"
|
|
required: false
|
|
default: "false"
|
|
spelling:
|
|
description: "Run Spelling checks"
|
|
required: false
|
|
default: "false"
|
|
links:
|
|
description: "Run Broken Links checks"
|
|
required: false
|
|
default: "false"
|
|
summary:
|
|
description: "Run PR Summary"
|
|
required: false
|
|
default: "false"
|
|
review:
|
|
description: "Run PR Review"
|
|
required: false
|
|
default: "true"
|
|
header:
|
|
description: "File header"
|
|
required: false
|
|
default: ""
|
|
brave_api_key:
|
|
description: "Brave API Key"
|
|
required: false
|
|
openai_api_key:
|
|
description: "OpenAI API Key"
|
|
required: false
|
|
anthropic_api_key:
|
|
description: "Anthropic API Key"
|
|
required: false
|
|
model:
|
|
description: "AI Model - defaults to GPT-5.6 Luna or Sonnet 4.6 based on API key"
|
|
required: false
|
|
review_model:
|
|
description: "AI Model for PR reviews (optional, defaults to gpt-5.6-terra with medium reasoning)"
|
|
required: false
|
|
first_issue_response:
|
|
description: "Example response to a new issue"
|
|
required: false
|
|
bun_update:
|
|
description: "Run bun update to update bun.lock if bun project detected"
|
|
required: false
|
|
default: "false"
|
|
github_username:
|
|
description: "GitHub username for commits"
|
|
required: false
|
|
default: "UltralyticsAssistant"
|
|
# default: "github-actions[bot]"
|
|
github_email:
|
|
description: "GitHub email for commits"
|
|
required: false
|
|
default: "[email protected]"
|
|
# default: "41898282+github-actions[bot]@users.noreply.github.com"
|
|
runs:
|
|
using: "composite"
|
|
steps:
|
|
- name: Setup Python
|
|
if: inputs.python == 'true' && inputs.python-version != ''
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ inputs.python-version }}
|
|
|
|
- uses: ultralytics/actions/setup-uv@main
|
|
- name: Install Dependencies
|
|
# Note tomli required for codespell with pyproject.toml
|
|
env:
|
|
INPUTS_SPELLING: ${{ inputs.spelling }}
|
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
GITHUB_SHA: ${{ github.sha }}
|
|
run: |
|
|
echo "::group::Install Dependencies"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
# Source tarballs install ~2x faster than git+ URLs, which clone the repository first. Test the current commit
|
|
# in the ultralytics/actions repo, otherwise install main.
|
|
if [ "$GITHUB_REPOSITORY" = "ultralytics/actions" ]; then
|
|
echo "Installing from commit: $GITHUB_SHA"
|
|
packages="https://github.com/ultralytics/actions/archive/${GITHUB_SHA}.tar.gz"
|
|
else
|
|
packages="https://github.com/ultralytics/actions/archive/refs/heads/main.tar.gz"
|
|
fi
|
|
|
|
if [ "$INPUTS_SPELLING" = "true" ]; then
|
|
packages="$packages codespell tomli"
|
|
fi
|
|
# On macOS, don't use sudo as it can cause environment issues
|
|
if [ "$(uname)" = "Darwin" ]; then
|
|
uv pip install --system --break-system-packages $packages
|
|
else
|
|
sudo env "PATH=$PATH" uv pip install --system --break-system-packages $packages
|
|
fi
|
|
ultralytics-actions-info
|
|
shell: bash
|
|
|
|
# Checkout Repository ----------------------------------------------------------------------------------------------
|
|
- name: Checkout Repository
|
|
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'review_requested'
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
|
token: ${{ inputs.token }}
|
|
ref: ${{ github.head_ref || github.ref }}
|
|
fetch-depth: 2
|
|
|
|
# File header ------------------------------------------------------------------------------------------------------
|
|
- name: File Header
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
env:
|
|
HEADER: ${{ inputs.header }}
|
|
run: |
|
|
echo "::group::File Header"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
ultralytics-actions-headers
|
|
shell: bash
|
|
continue-on-error: false
|
|
|
|
# Python formatting ------------------------------------------------------------------------------------------------
|
|
# Ignores the following Docs rules to match Google-style docstrings:
|
|
# D100: Missing docstring in public module
|
|
# D104: Missing docstring in public package
|
|
# D203: 1 blank line required before class docstring
|
|
# D205: 1 blank line required between summary line and description
|
|
# D212: Multi-line docstring summary should start at the first line
|
|
# D213: Multi-line docstring summary should start at the second line
|
|
# D401: First line of docstring should be in imperative mood
|
|
# D406: Section name should end with a newline
|
|
# D407: Missing dashed underline after section
|
|
# D413: Missing blank line after last section
|
|
# --target-version is Python 3.8 for --extend-select UP (pyupgrade)
|
|
- name: Run Python
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.python == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Python"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
ruff check \
|
|
--fix \
|
|
--unsafe-fixes \
|
|
--extend-select F,I,D,UP,RUF,FA \
|
|
--target-version py38 \
|
|
--ignore BLE001,D100,D104,D203,D205,D212,D213,D401,D406,D407,D413,RUF001,RUF002,RUF012,S110 \
|
|
. || true
|
|
ruff format \
|
|
--line-length 120 \
|
|
. || true
|
|
if [ "${{ inputs.python_docstrings }}" = "true" ]; then
|
|
ultralytics-actions-format-python-docstrings . || true
|
|
fi
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Biome (JavaScript, TypeScript, JSON, JSX, TSX) ------------------------------------------------------------------
|
|
- name: Run Biome
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.biome == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Biome"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
|
|
npm install -g @biomejs/biome@latest
|
|
npx biome --version
|
|
npx biome check --write --line-width=120 --max-diagnostics=1000 --diagnostic-level=error --unsafe .
|
|
else
|
|
echo "⚠️ No biome.json found - skipping Biome formatting. Add biome.json to enable."
|
|
fi
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Prettier (JavaScript, JSX, Angular, Vue, Flow, TypeScript, CSS, HTML, JSON, GraphQL, Markdown, YAML) -------------
|
|
- name: Run Prettier
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && (inputs.prettier == 'true' || inputs.markdown == 'true') && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Prettier"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
npm install -g [email protected]
|
|
curl -fsSL "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" -o "$(npm prefix -g)/bin/shfmt"
|
|
chmod +x "$(npm prefix -g)/bin/shfmt"
|
|
ultralytics-actions-update-markdown-code-blocks
|
|
npx prettier --write --list-different --print-width 120 "**/*.{js,jsx,ts,tsx,css,less,scss,json,yml,yaml,html,vue,svelte}" '!**/*lock.{json,yaml,yml}' '!**/*.lock' '!**/model.json' '!**/*.min.js' '!**/*.min.css'
|
|
# Handle Markdown separately
|
|
find . -name "*.md" -type f ! -path "*/docs/*" -exec npx prettier --write --list-different --print-width 120 {} +
|
|
if [ -d "./docs" ]; then
|
|
find ./docs -name "*.md" -type f ! -path "*/reference/*" -exec npx prettier --tab-width 4 --print-width 120 --write --list-different {} +
|
|
fi
|
|
# Bash last: shfmt exits non-zero when a script fails to parse, which would otherwise skip the formatting above
|
|
find . -name "*.sh" -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -exec shfmt -i 2 -sr -bn -ci -l -w {} +
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Swift formatting -------------------------------------------------------------------------------------------------
|
|
- name: Run Swift Formatter
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.swift == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Swift Formatter"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
brew install swift-format
|
|
swift-format --in-place --recursive .
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Dart formatting --------------------------------------------------------------------------------------------------
|
|
- name: Setup Dart SDK
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.dart == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
uses: dart-lang/setup-dart@v1
|
|
with:
|
|
sdk: stable
|
|
|
|
- name: Run Dart Formatter
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.dart == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Dart Formatter"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
dart format .
|
|
shell: bash
|
|
continue-on-error: true # Important so that commit step can pick up changes
|
|
|
|
# Spelling ---------------------------------------------------------------------------------------------------------
|
|
# --ignore-regex '\b[a-z]+[A-Z][a-zA-Z]*\b' ignores camelCase words (e.g. repoRes, dateA) to prevent false positives
|
|
- name: Run Codespell
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.spelling == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
|
|
run: |
|
|
echo "::group::Run Codespell"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
codespell \
|
|
--builtin clear,informal,en-GB_to_en-US \
|
|
--write-changes \
|
|
--uri-ignore-words-list "*" \
|
|
--ignore-regex '\b[a-z]+[A-Z][a-zA-Z]*\b' \
|
|
--ignore-words-list "nin,cancelled,MapPin,couldn,grey,writeable,RepResNet,Idenfy,Smoot,EHR,ALS,Carmel,FPR,Hach,Calle,crate,nd,ned,strack,dota,ane,segway,fo,gool,winn,nam,afterall,skelton,goin,cann,CANN" \
|
|
--skip "*.pt,*.pth,*.torchscript,*.onnx,*.tflite,*.pb,*.bin,*.param,*.mlmodel,*.engine,*.npy,*.data*,*.csv,*pnnx*,*venv*,*translat*,*lock*,__pycache__*,*.ico,*.jpg,*.png,*.webp,*.avif,*.mp4,*.mov,/runs,/.git,./docs/??/*.md,./docs/mkdocs_??.yml,action.yml"
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# PR Summary -------------------------------------------------------------------------------------------------------
|
|
- name: PR Summary
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.summary == 'true' && github.event.action == 'closed'
|
|
env:
|
|
GITHUB_TOKEN: ${{ inputs.token }}
|
|
BRAVE_API_KEY: ${{ inputs.brave_api_key }}
|
|
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
|
MODEL: ${{ inputs.model }}
|
|
run: |
|
|
echo "::group::PR Summary"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
ultralytics-actions-summarize-pr
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Autolabel Issues and PRs (labels and first interaction) ---------------------------------------------------------
|
|
- name: Autolabel Issues and PRs
|
|
if: inputs.labels == 'true' && (github.event.action == 'opened' || github.event.action == 'created')
|
|
env:
|
|
GITHUB_TOKEN: ${{ inputs.token }}
|
|
FIRST_ISSUE_RESPONSE: ${{ inputs.first_issue_response }}
|
|
BRAVE_API_KEY: ${{ inputs.brave_api_key }}
|
|
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
|
MODEL: ${{ inputs.model }}
|
|
REVIEW_MODEL: ${{ inputs.review_model }}
|
|
REVIEW: ${{ inputs.review }}
|
|
run: |
|
|
echo "::group::Autolabel Issues and PRs"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
ultralytics-actions-first-interaction
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# PR Review --------------------------------------------------------------------------------------------------------
|
|
- name: PR Review
|
|
if: |
|
|
(github.event_name == 'pull_request' || github.event_name == 'pull_request_target') &&
|
|
github.event.action == 'review_requested' &&
|
|
github.event.requested_reviewer.login == inputs.github_username
|
|
env:
|
|
GITHUB_TOKEN: ${{ inputs.token }}
|
|
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
|
MODEL: ${{ inputs.model }}
|
|
REVIEW_MODEL: ${{ inputs.review_model }}
|
|
run: |
|
|
echo "::group::PR Review"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
ultralytics-actions-review-pr
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Bun Update ---------------------------------------------------------------------------------------------------------
|
|
- name: Bun Update
|
|
if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && inputs.bun_update == 'true' && github.event.action == 'opened'
|
|
run: |
|
|
echo "::group::Bun Update"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
if [ -f "bun.lock" ] || [ -f "bun.lockb" ]; then
|
|
npm install -g bun
|
|
echo "Updating root..."
|
|
bun update --latest
|
|
for pkg in apps/*/package.json packages/*/package.json; do
|
|
[ -f "$pkg" ] || continue
|
|
dir=$(dirname "$pkg")
|
|
echo "Updating $dir..."
|
|
(cd "$dir" && bun update --latest)
|
|
done
|
|
echo "Resolving bun.lock versions..."
|
|
bun update
|
|
else
|
|
echo "No bun.lock found - skipping Bun update"
|
|
fi
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Commit Changes ---------------------------------------------------------------------------------------------------
|
|
- name: Commit and Push Changes
|
|
# Generated PRs may be opened by github_username (for example analytics data updates) and still need one
|
|
# Auto-format commit on the opened event. The recursion risk starts after that commit pushes a synchronize
|
|
# event as github_username, so keep the bot guard only for synchronize events.
|
|
if: |
|
|
(github.event_name == 'pull_request' || github.event_name == 'pull_request_target') &&
|
|
(github.event.action == 'opened' || github.event.action == 'synchronize') &&
|
|
(github.event.action == 'opened' || github.actor != inputs.github_username)
|
|
env:
|
|
GITHUB_USERNAME: ${{ inputs.github_username }}
|
|
GITHUB_EMAIL: ${{ inputs.github_email }}
|
|
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
echo "::group::Commit and Push Changes"
|
|
trap 'echo "::endgroup::"' EXIT
|
|
COMMIT_MSG="Auto-format by https://ultralytics.com/actions"
|
|
git config --global user.name "$GITHUB_USERNAME"
|
|
git config --global user.email "$GITHUB_EMAIL"
|
|
git add .
|
|
git reset HEAD -- .github/workflows/ # workflow changes are not permitted with default token
|
|
if ! git diff --staged --quiet; then
|
|
git commit -m "$COMMIT_MSG"
|
|
git push --force-with-lease="refs/heads/${PR_HEAD_REF}:${PR_HEAD_SHA}" origin "HEAD:refs/heads/${PR_HEAD_REF}"
|
|
else
|
|
echo "No changes to commit"
|
|
fi
|
|
shell: bash
|
|
continue-on-error: true
|
|
|
|
# Broken links -----------------------------------------------------------------------------------------------------
|
|
- name: Broken Link Checker
|
|
if: inputs.links == 'true' && github.event.action != 'closed'
|
|
uses: lycheeverse/[email protected]
|
|
with:
|
|
# Check all markdown and html files in repo. Ignores the following status codes to reduce false positives:
|
|
# - 401(generic, "unauthorized")
|
|
# - 403(OpenVINO, "forbidden")
|
|
# - 429(Instagram, "too many requests")
|
|
# - 500(Zenodo, "cached")
|
|
# - 502(Zenodo, "bad gateway")
|
|
# - 999(LinkedIn, "unknown status code")
|
|
args: |
|
|
--scheme https
|
|
--timeout 60
|
|
--insecure
|
|
--accept 100..=103,200..=299,401,403,429,500,502,999
|
|
--exclude-all-private
|
|
--exclude "https?://(www\.)?(github\.com|linkedin\.com|twitter\.com|instagram\.com|kaggle\.com|fonts\.gstatic\.com|url\.com)"
|
|
"./**/*.md"
|
|
"./**/*.html"
|
|
token: ${{ inputs.token }}
|
|
output: ../lychee/results.md
|
|
fail: true
|
|
continue-on-error: false
|