Files

450 lines
20 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 with first responses"
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: "true"
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 claude-sonnet-5 based on API key"
required: false
review_model:
description: "AI Model for PR reviews (optional, defaults to gpt-5.6-luna or claude-opus-5 based on API key)"
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
with:
ignore-empty-workdir: true
- name: Install Dependencies
# Note tomli required for codespell with pyproject.toml
env:
INPUTS_SPELLING: ${{ inputs.spelling }}
ACTION_PATH: ${{ github.action_path }}
run: |
echo "::group::Install Dependencies"
trap 'echo "::endgroup::"' EXIT
package_dir=$(mktemp -d "$RUNNER_TEMP/ultralytics-actions.XXXXXX")
uv build --wheel "$ACTION_PATH" --out-dir "$package_dir"
packages=("$package_dir"/*.whl)
if [ "$INPUTS_SPELLING" = "true" ]; then
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_name == 'pull_request' && (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 }}
token: ${{ inputs.token }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
# File header ------------------------------------------------------------------------------------------------------
- name: File Header
if: inputs.header != 'false' && github.event_name == 'pull_request' && (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' && 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' && 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' && (inputs.prettier == 'true' || inputs.markdown == 'true') && (github.event.action == 'opened' || github.event.action == 'synchronize')
env:
INPUTS_BIOME: ${{ inputs.biome }}
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
extensions="css,less,scss,yml,yaml,html,vue,svelte"
[ "${INPUTS_BIOME:-false}" = "true" ] && { [ -f "biome.json" ] || [ -f "biome.jsonc" ]; } || extensions="js,jsx,ts,tsx,json,$extensions"
npx prettier --write --list-different --print-width 120 "**/*.{$extensions}" '!**/*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' && 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 --------------------------------------------------------------------------------------------------
# Flutter bundles Dart and resolves Flutter packages; without `pub get`, `dart format` cannot read the package
# language version and silently falls back to the pre-3.7 short style.
- name: Setup Flutter SDK
if: github.event_name == 'pull_request' && inputs.dart == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
uses: ultralytics/actions/retry@main
with:
retries: 2
timeout_minutes: 15
run: |
echo "::group::Setup Flutter SDK"
trap 'echo "::endgroup::"' EXIT
rm -rf "$RUNNER_TEMP/flutter"
git clone --depth 1 --branch stable https://github.com/flutter/flutter.git "$RUNNER_TEMP/flutter"
echo "$RUNNER_TEMP/flutter/bin" >> "$GITHUB_PATH"
- name: Run Dart Formatter
if: github.event_name == 'pull_request' && inputs.dart == 'true' && (github.event.action == 'opened' || github.event.action == 'synchronize')
run: |
echo "::group::Run Dart Formatter"
trap 'echo "::endgroup::"' EXIT
ok=true
while IFS= read -r -d '' pubspec; do
(cd "$(dirname "$pubspec")" && flutter pub get) || ok=false
done < <(find . -name pubspec.yaml -not -path '*/.*' -print0)
$ok && dart format . || ok=false # never format with an unresolved language version
git clean -fdq # formatting never creates files, so drop generated pub and Flutter metadata
git ls-files -z '*pubspec.lock' | xargs -0 -r git checkout --
$ok
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' && 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,tbe,dfine" \
--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
# Bun Update ---------------------------------------------------------------------------------------------------------
- name: Bun Update
if: github.event_name == 'pull_request' && 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.action == 'opened' || github.event.action == 'synchronize') &&
(github.event.action == 'opened' || github.actor != inputs.github_username) &&
github.event.pull_request.head.repo.full_name == github.repository
env:
GITHUB_USERNAME: ${{ inputs.github_username }}
GITHUB_EMAIL: ${{ inputs.github_email }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
echo "::group::Commit and Push Changes"
trap 'echo "::endgroup::"' EXIT
COMMIT_MSG="Auto-format by https://ultralytics.com/actions"
git config user.name "$GITHUB_USERNAME"
git config 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 origin "HEAD:refs/heads/${PR_HEAD_REF}"
else
echo "No changes to commit"
fi
shell: bash
continue-on-error: true
# PR automation and first interaction ------------------------------------------------------------------------------
- name: PR Automation and First Interaction
if: |
(inputs.labels == 'true' && (github.event.action == 'opened' || github.event.action == 'created')) ||
((github.event_name == 'pull_request' || github.event_name == 'pull_request_target') &&
github.event.action == 'opened' && (inputs.summary == 'true' || inputs.review == 'true'))
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 }}
LABELS: ${{ inputs.labels }}
SUMMARY: ${{ inputs.summary }}
REVIEW: ${{ inputs.review }}
run: |
echo "::group::PR Automation and First Interaction"
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
# Broken links -----------------------------------------------------------------------------------------------------
# lychee-action only ships Linux binaries, so install lychee directly to support macOS runners (Swift repos) too.
- name: Broken Link Checker
if: inputs.links == 'true' && github.event_name == 'pull_request' && github.event.action != 'closed'
env:
GITHUB_TOKEN: ${{ inputs.token }}
# 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")
run: |
echo "::group::Broken Link Checker"
trap 'echo "::endgroup::"' EXIT
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) target=x86_64-unknown-linux-gnu ;;
Linux-aarch64) target=aarch64-unknown-linux-gnu ;;
Darwin-arm64) target=aarch64-apple-darwin ;;
Darwin-x86_64) target=x86_64-apple-darwin ;;
esac
mkdir -p "$RUNNER_TEMP/lychee"
archive="lychee-${target}.tar.gz"
url="https://github.com/lycheeverse/lychee/releases/latest/download/$archive"
curl --retry 3 --retry-all-errors --retry-max-time 180 --connect-timeout 10 --max-time 60 -fsSL "$url" -o "$RUNNER_TEMP/lychee/$archive"
curl --retry 3 --retry-all-errors --retry-max-time 180 --connect-timeout 10 --max-time 60 -fsSL "$url.sha256" -o "$RUNNER_TEMP/lychee/$archive.sha256"
if command -v sha256sum >/dev/null; then
(cd "$RUNNER_TEMP/lychee" && sha256sum -c "$archive.sha256")
else
(cd "$RUNNER_TEMP/lychee" && shasum -a 256 -c "$archive.sha256")
fi
tar -xzf "$RUNNER_TEMP/lychee/$archive" -C "$RUNNER_TEMP/lychee"
"$(find "$RUNNER_TEMP/lychee" -type f -name lychee)" \
--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"
shell: bash