From 48c371f25a83805b7c89f587053a4b8ec1080f9c Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 17:38:25 +0200 Subject: [PATCH 1/9] Add publish.yml release workflow with PyPI publishing commented out --- .github/workflows/publish.yml | 183 ++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..98f58b5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,183 @@ +# 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: + if: github.repository == 'ultralytics/template' && github.actor == 'glenn-jocher' + 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 }} + 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" ]; then + INCREMENT=True + echo "Version changed $OLD_VERSION โ†’ $NEW_VERSION, ready to tag and release โœ…." + 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: | + 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" + ultralytics-actions-summarize-release + 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 + # 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. + # + # environment: # for GitHub Deployments tab + # name: Release - PyPI + # url: https://pypi.org/p/template + # permissions: + # id-token: write # for PyPI trusted publishing + 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 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + notify: + needs: [check, publish] + 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]+)@@g") + echo "title=$TITLE" >> "$GITHUB_OUTPUT" + - name: Notify Success + if: needs.publish.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: " *${{ github.workflow }}* โœ… `${{ github.repository }}` ${{ steps.release.outputs.title || needs.check.outputs.current_tag }} ยท " + - name: Notify Failure + if: needs.publish.result != 'success' + uses: slackapi/slack-github-action@v3.0.3 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} + payload: | + text: " *${{ github.workflow }}* โŒ `${{ github.repository }}` ${{ needs.check.outputs.current_tag }} " From 6b11ce0fd0f7714bd8496e9fb99b51295de6afee Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 17:40:52 +0200 Subject: [PATCH 2/9] Address PR review: drop actor gate, add publish permissions, gate notify on SBOM --- .github/workflows/publish.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98f58b5..0a68fa7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ on: jobs: check: - if: github.repository == 'ultralytics/template' && github.actor == 'glenn-jocher' + if: github.repository == 'ultralytics/template' runs-on: ubuntu-latest permissions: contents: write @@ -107,14 +107,15 @@ jobs: 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. + # (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 - # permissions: - # id-token: write # for PyPI trusted publishing steps: - uses: actions/download-artifact@v8 with: @@ -149,7 +150,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify: - needs: [check, publish] + needs: [check, publish, sbom] if: always() && needs.check.outputs.increment == 'True' runs-on: ubuntu-latest permissions: @@ -166,7 +167,7 @@ jobs: TITLE=$(printf '%s' "$TITLE" | sed -E "s@#([0-9]+)@@g") echo "title=$TITLE" >> "$GITHUB_OUTPUT" - name: Notify Success - if: needs.publish.result == 'success' && github.event_name == 'push' + 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 @@ -174,7 +175,7 @@ jobs: payload: | text: " *${{ github.workflow }}* โœ… `${{ github.repository }}` ${{ steps.release.outputs.title || needs.check.outputs.current_tag }} ยท " - name: Notify Failure - if: needs.publish.result != 'success' + if: needs.publish.result != 'success' || needs.sbom.result != 'success' uses: slackapi/slack-github-action@v3.0.3 with: webhook-type: incoming-webhook From 70f9394a95ecdd0f0eb189ab797c51b363368c6d Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 17:43:49 +0200 Subject: [PATCH 3/9] Guard release against non-main dispatch and existing tags --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a68fa7..d13e214 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ on: jobs: check: - if: github.repository == 'ultralytics/template' + if: github.repository == 'ultralytics/template' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write @@ -57,7 +57,7 @@ jobs: 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" ]; then + 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 โœ…." else From 75e2be27361386e267417027b0e11f5da589e1e1 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 17:47:28 +0200 Subject: [PATCH 4/9] Support workflow_dispatch recovery re-runs after partial release failures --- .github/workflows/publish.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d13e214..080ff29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,6 +60,8 @@ jobs: 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 [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + INCREMENT=True # manual recovery re-run for v$NEW_VERSION after a partial failure else INCREMENT=False fi @@ -76,11 +78,13 @@ jobs: PREVIOUS_TAG: ${{ steps.check_version.outputs.previous_tag }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | - 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" - ultralytics-actions-summarize-release + 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" + ultralytics-actions-summarize-release + fi uv cache prune --ci build: @@ -145,7 +149,7 @@ jobs: format: spdx-json output-file: sbom.spdx.json path: sbom-env - - run: gh release upload ${{ needs.check.outputs.current_tag }} sbom.spdx.json + - run: gh release upload ${{ needs.check.outputs.current_tag }} sbom.spdx.json --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 89211ec064b0a2ff6548502fd01728c00e4ad8d9 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 17:50:27 +0200 Subject: [PATCH 5/9] Gate release summarization on release existence, not tag existence --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 080ff29..eab62b0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -83,6 +83,8 @@ jobs: 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 From dc72f8374affb6826833442d8fe3518d03fe31a4 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 20:31:46 +0200 Subject: [PATCH 6/9] Restore actor gate on release check job --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eab62b0..c6625d4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ on: jobs: check: - if: github.repository == 'ultralytics/template' && github.ref == 'refs/heads/main' + if: github.repository == 'ultralytics/template' && github.actor == 'glenn-jocher' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write From 4765c7d5bce466a85ddf3f631e7ffc4dea498b20 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 20:49:01 +0200 Subject: [PATCH 7/9] Drop actor gate: template is a copy-me starter, forks add their own --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6625d4..eab62b0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ on: jobs: check: - if: github.repository == 'ultralytics/template' && github.actor == 'glenn-jocher' && github.ref == 'refs/heads/main' + if: github.repository == 'ultralytics/template' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write From dd6cd73e61151e2cdea89d3182a4988d405c6611 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 20:52:48 +0200 Subject: [PATCH 8/9] Gate template manual dispatch on the pypi input --- .github/workflows/publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eab62b0..d79b6a1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ on: 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: @@ -51,6 +52,7 @@ jobs: - 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) @@ -60,7 +62,7 @@ jobs: 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 [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + elif [ "$PYPI_DISPATCH" = "true" ]; then INCREMENT=True # manual recovery re-run for v$NEW_VERSION after a partial failure else INCREMENT=False From 6c1533887b585c9a43985af227dbe13279ee831d Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 4 Jul 2026 20:52:48 +0200 Subject: [PATCH 9/9] Document publish.yml release workflow and actor-gate convention --- AGENTS.md | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6e56469..18d0d22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index f459b07..88f160a 100644 --- a/README.md +++ b/README.md @@ -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)