Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Ultralytics πŸš€ AGPL-3.0 License - https://ultralytics.com/license

# Tag, release, and (optionally) publish pip package to PyPI on version increment

name: Publish to PyPI

on:
push:
branches: [main]
workflow_dispatch:
inputs:
pypi:
type: boolean
description: Publish to PyPI

jobs:
check:
# Templates omit the github.actor maintainer gate that product repos keep for security; forks add their own.
if: github.repository == 'ultralytics/template' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
increment: ${{ steps.check_version.outputs.increment }}
current_tag: ${{ steps.check_version.outputs.current_tag }}
previous_tag: ${{ steps.check_version.outputs.previous_tag }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- run: uv pip install --system --no-cache ultralytics-actions
# This template gates on __version__ changing in the pushed diff since the 'template' name on
# PyPI belongs to an unrelated package. When publishing your own package, replace this step
# with the PyPI version check used in
# https://github.com/ultralytics/mkdocs/blob/main/.github/workflows/publish.yml:
#
# - id: check_pypi
# shell: python
# run: |
# import os
# from actions.utils import check_pypi_version
# local_version, online_version, publish = check_pypi_version()
# 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_version
env:
BASE: ${{ github.event.before }}
PYPI_DISPATCH: ${{ github.event.inputs.pypi }}
run: |
if [ -z "$BASE" ] || ! git cat-file -e "$BASE" 2>/dev/null; then
BASE=$(git rev-parse HEAD~1)
fi
OLD_VERSION=$(git show "$BASE:template/__init__.py" | sed -n 's/^__version__ = "\(.*\)"$/\1/p')
NEW_VERSION=$(python -c 'import template; print(template.__version__)')
if [ "$NEW_VERSION" != "$OLD_VERSION" ] && [ -z "$(git tag -l "v$NEW_VERSION")" ]; then
INCREMENT=True
echo "Version changed $OLD_VERSION β†’ $NEW_VERSION, ready to tag and release βœ…."
elif [ "$PYPI_DISPATCH" = "true" ]; then
INCREMENT=True # manual recovery re-run for v$NEW_VERSION after a partial failure
else
INCREMENT=False
fi
{
echo "increment=$INCREMENT"
echo "current_tag=v$NEW_VERSION"
echo "previous_tag=v$OLD_VERSION"
} >> "$GITHUB_OUTPUT"
- name: Tag and Release
if: steps.check_version.outputs.increment == 'True'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_TAG: ${{ steps.check_version.outputs.current_tag }}
PREVIOUS_TAG: ${{ steps.check_version.outputs.previous_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
if [ -z "$(git tag -l "$CURRENT_TAG")" ]; then
git config --global user.name "UltralyticsAssistant"
git config --global user.email "web@ultralytics.com"
git tag -a "$CURRENT_TAG" -m "$(git log -1 --pretty=%B)"
git push origin "$CURRENT_TAG"
fi
if ! gh release view "$CURRENT_TAG" >/dev/null 2>&1; then
ultralytics-actions-summarize-release
fi
uv cache prune --ci

build:
needs: check
if: needs.check.outputs.increment == 'True'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- run: uv pip install --system --no-cache build
- run: python -m build
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
- run: uv cache prune --ci

publish:
needs: [check, build]
if: needs.check.outputs.increment == 'True'
runs-on: ubuntu-latest
permissions:
contents: read
# To publish to PyPI: rename the package in pyproject.toml, set up PyPI trusted publishing
# (https://docs.pypi.org/trusted-publishers/), then uncomment the lines below and add
# 'id-token: write' to the permissions above.
#
# environment: # for GitHub Deployments tab
# name: Release - PyPI
# url: https://pypi.org/p/template
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
# - uses: pypa/gh-action-pypi-publish@release/v1

sbom:
needs: [check, build, publish]
if: needs.check.outputs.increment == 'True'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- run: |
uv venv sbom-env
uv pip install -e .
env:
VIRTUAL_ENV: sbom-env
- uses: anchore/sbom-action@v0
with:
format: spdx-json
output-file: sbom.spdx.json
path: sbom-env
- run: gh release upload ${{ needs.check.outputs.current_tag }} sbom.spdx.json --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

notify:
needs: [check, publish, sbom]
if: always() && needs.check.outputs.increment == 'True'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Get release title
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.check.outputs.current_tag }}
run: |
TITLE=$(gh release view "$TAG" --json name -q .name 2>/dev/null | tr -d '\n\r"\\') || TITLE=""
TITLE=$(printf '%s' "$TITLE" | sed -E "s@#([0-9]+)@<https://github.com/$GH_REPO/pull/\1|#\1>@g")
echo "title=$TITLE" >> "$GITHUB_OUTPUT"
- name: Notify Success
if: needs.publish.result == 'success' && needs.sbom.result == 'success' && github.event_name == 'push'
uses: slackapi/slack-github-action@v3.0.3
with:
webhook-type: incoming-webhook
webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
payload: |
text: "<!subteam^S082BPCRAJ3> *${{ github.workflow }}* βœ… `${{ github.repository }}` ${{ steps.release.outputs.title || needs.check.outputs.current_tag }} <https://github.com/${{ github.repository }}/releases/tag/${{ needs.check.outputs.current_tag }}|*Release*> Β· <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|*Run*>"
- name: Notify Failure
if: needs.publish.result != 'success' || needs.sbom.result != 'success'
uses: slackapi/slack-github-action@v3.0.3
with:
webhook-type: incoming-webhook
webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
payload: |
text: "<!subteam^S082BPCRAJ3> *${{ github.workflow }}* ❌ `${{ github.repository }}` ${{ needs.check.outputs.current_tag }} <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|*Run*>"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ CI matrix: Python 3.9/3.13/3.14 Γ— ubuntu/macos/windows. CI runs the tests four

## Architecture

This is the Ultralytics template for new Python packages β€” a minimal, fully wired example meant to be copied and adapted. `template/` is the package: `__init__.py` holds `__version__` (read by setuptools dynamic versioning in `pyproject.toml`), and `module1.py` holds the example `add_numbers()`/`main()` backing the `example-cli-command` entry point in `[project.scripts]`. `tests/` demonstrates the same tests in both pytest style (`test_with_pytest.py`) and unittest style (`test_with_unittest.py`). `format.yml` runs Ultralytics Actions on PRs (Ruff, Prettier, codespell, link checks, AI labels/summaries) and commits fixes back to the PR branch.
This is the Ultralytics template for new Python packages β€” a minimal, fully wired example meant to be copied and adapted. `template/` is the package: `__init__.py` holds `__version__` (read by setuptools dynamic versioning in `pyproject.toml`), and `module1.py` holds the example `add_numbers()`/`main()` backing the `example-cli-command` entry point in `[project.scripts]`. `tests/` demonstrates the same tests in both pytest style (`test_with_pytest.py`) and unittest style (`test_with_unittest.py`). `format.yml` runs Ultralytics Actions on PRs (Ruff, Prettier, codespell, link checks, AI labels/summaries) and commits fixes back to the PR branch. `publish.yml` tags, releases, and (optionally) publishes to PyPI when `__version__` is bumped on `main`; its `check` job intentionally omits the `github.actor` maintainer gate that product repos keep for security, because a fork supplies its own.

## Conventions

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ your-project/
β”œβ”€β”€ .github/ # GitHub Actions workflows
β”‚ └── workflows/
β”‚ β”œβ”€β”€ ci.yml
β”‚ └── format.yml
β”‚ β”œβ”€β”€ format.yml
β”‚ └── publish.yml
β”‚
β”œβ”€β”€ .gitignore # Git ignore rules
β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit hook config (optional)
Expand Down
Loading