Release workflow (personalrobotics/ssik)
The Release workflow from personalrobotics/ssik, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Release workflow from the personalrobotics/ssik repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-3-Clause license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Release
# Tag-driven release pipeline (#132).
#
# Tag patterns:
# v*rc* -> build wheels + sdist, publish to TestPyPI (dry run)
# v* -> build wheels + sdist, publish to real PyPI
# (manual workflow_dispatch also builds + publishes to TestPyPI)
#
# Auth: Trusted Publishing (PyPI / TestPyPI OIDC). No API tokens in CI.
# The publish jobs run inside named GitHub environments (`release` /
# `testpypi-release`) so the org's environment-protection rules apply.
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
jobs:
build_wheels:
name: Wheels - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Linux x86_64 (manylinux), macOS arm64 + x86_64 (both from the
# M-class runner via cibuildwheel cross-compile -- see
# [tool.cibuildwheel.macos] in pyproject.toml), Windows x86_64.
# GitHub's Intel macOS runner pool (macos-13) has multi-hour
# queue latency as Intel hosts are deprecated; cross-compiling
# x86_64 from arm64 avoids the bottleneck without dropping
# Intel-Mac wheel coverage.
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # hatch-vcs derives version from git tags
- name: Build wheels
# v2.21 pinned ``packaging==24.1`` in its build-isolation constraints,
# which conflicts with ``hatchling>=1.27`` requiring ``packaging>=24.2``
# (broke macOS arm64 + Windows on v1.0.0rc1; Linux happened to resolve
# because the manylinux constraints file is slightly different).
# v3.x has updated pins.
uses: pypa/cibuildwheel@v3.4.1
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
path: wheelhouse/*.whl
build_sdist:
name: Source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up uv
uses: astral-sh/setup-uv@v4
- name: Build sdist
run: uv build --sdist
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
publish_testpypi:
name: Publish → TestPyPI
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
# Run on RC tags (e.g. v1.0.0rc1) and manual triggers. The real PyPI
# job below runs on non-RC v* tags only.
if: contains(github.ref, 'rc') || github.event_name == 'workflow_dispatch'
environment:
name: testpypi-release
url: https://test.pypi.org/p/ssik
permissions:
id-token: write # Trusted Publishing OIDC
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Verify built version matches the tag
# Same guard as publish_pypi (#364): catch a version/tag mismatch in
# the rc dry-run, before the real release.
run: |
TAG="${GITHUB_REF#refs/tags/v}"
echo "tag version: $TAG"; ls -1 dist/
SDIST=$(ls dist/ssik-*.tar.gz 2>/dev/null | head -1)
[ -n "$SDIST" ] || { echo "::error::no sdist in dist/"; exit 1; }
BUILT=$(basename "$SDIST" .tar.gz); BUILT="${BUILT#ssik-}"
echo "built version: $BUILT"
if [ "$BUILT" != "$TAG" ]; then
echo "::error::built version '$BUILT' != tag '$TAG' -- refusing to publish (likely two tags on one commit; ensure a single v* tag points at the release commit)"
exit 1
fi
for w in dist/ssik-*.whl; do
case "$(basename "$w")" in
ssik-"$TAG"-*) ;;
*) echo "::error::wheel $(basename "$w") is not version $TAG"; exit 1 ;;
esac
done
echo "OK: all dist artifacts are version $TAG"
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish_pypi:
name: Publish → PyPI
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
# Real PyPI: only on v* tags that DON'T contain 'rc' (e.g. v1.0.0,
# v1.0.1). The RC dry-run job above handles v*rc* tags.
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'rc')
environment:
name: release
url: https://pypi.org/p/ssik
permissions:
id-token: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Verify built version matches the tag
# Guard against the "two v* tags on one commit" footgun (#364): when
# both e.g. v3.0.0rc1 and v3.0.0 point at the same commit, hatch-vcs
# (via ``git describe``) can resolve the *rc*, so the v3.0.0 run builds
# ssik-3.0.0rc1 wheels and would publish those to real PyPI. Refuse to
# publish unless every artifact carries the tag's exact version. Tag
# convention: ``vX.Y.Z`` / ``vX.Y.ZrcN`` (no dash; already PEP 440).
run: |
TAG="${GITHUB_REF#refs/tags/v}"
echo "tag version: $TAG"; ls -1 dist/
SDIST=$(ls dist/ssik-*.tar.gz 2>/dev/null | head -1)
[ -n "$SDIST" ] || { echo "::error::no sdist in dist/"; exit 1; }
BUILT=$(basename "$SDIST" .tar.gz); BUILT="${BUILT#ssik-}"
echo "built version: $BUILT"
if [ "$BUILT" != "$TAG" ]; then
echo "::error::built version '$BUILT' != tag '$TAG' -- refusing to publish (likely two tags on one commit; ensure a single v* tag points at the release commit)"
exit 1
fi
for w in dist/ssik-*.whl; do
case "$(basename "$w")" in
ssik-"$TAG"-*) ;;
*) echo "::error::wheel $(basename "$w") is not version $TAG"; exit 1 ;;
esac
done
echo "OK: all dist artifacts are version $TAG"
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
create_github_release:
name: Create GitHub Release
needs: publish_pypi
runs-on: ubuntu-latest
# Only on stable releases (non-RC). The GitHub Release is what fires
# the Zenodo GitHub-integration webhook, which mints a new version DOI
# (concept DOI stays constant). Without this step, tags exist but
# Zenodo never archives them.
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'rc')
permissions:
contents: write # required to create the Release
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for auto-generated release notes
- name: Create Release
# action-gh-release is idempotent: if a Release already exists for
# this tag (e.g. created manually before the workflow finished),
# it updates rather than failing.
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ssik ${{ github.ref_name }}
generate_release_notes: true # GitHub auto-generates from merged PRs since previous tag
draft: false
prerelease: false
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Release # Tag-driven release pipeline (#132). # # Tag patterns: # v*rc* -> build wheels + sdist, publish to TestPyPI (dry run) # v* -> build wheels + sdist, publish to real PyPI # (manual workflow_dispatch also builds + publishes to TestPyPI) # # Auth: Trusted Publishing (PyPI / TestPyPI OIDC). No API tokens in CI. # The publish jobs run inside named GitHub environments (`release` / # `testpypi-release`) so the org's environment-protection rules apply. on: push: tags: ["v*"] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build_wheels: timeout-minutes: 30 name: Wheels - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: # Linux x86_64 (manylinux), macOS arm64 + x86_64 (both from the # M-class runner via cibuildwheel cross-compile -- see # [tool.cibuildwheel.macos] in pyproject.toml), Windows x86_64. # GitHub's Intel macOS runner pool (macos-13) has multi-hour # queue latency as Intel hosts are deprecated; cross-compiling # x86_64 from arm64 avoids the bottleneck without dropping # Intel-Mac wheel coverage. os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # hatch-vcs derives version from git tags - name: Build wheels # v2.21 pinned ``packaging==24.1`` in its build-isolation constraints, # which conflicts with ``hatchling>=1.27`` requiring ``packaging>=24.2`` # (broke macOS arm64 + Windows on v1.0.0rc1; Linux happened to resolve # because the manylinux constraints file is slightly different). # v3.x has updated pins. uses: pypa/cibuildwheel@v3.4.1 - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl build_sdist: timeout-minutes: 30 name: Source distribution runs-on: latchkey-small steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@v4 - name: Build sdist run: uv build --sdist - name: Upload sdist uses: actions/upload-artifact@v4 with: name: sdist path: dist/*.tar.gz publish_testpypi: timeout-minutes: 30 name: Publish → TestPyPI needs: [build_wheels, build_sdist] runs-on: latchkey-small # Run on RC tags (e.g. v1.0.0rc1) and manual triggers. The real PyPI # job below runs on non-RC v* tags only. if: contains(github.ref, 'rc') || github.event_name == 'workflow_dispatch' environment: name: testpypi-release url: https://test.pypi.org/p/ssik permissions: id-token: write # Trusted Publishing OIDC steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Verify built version matches the tag # Same guard as publish_pypi (#364): catch a version/tag mismatch in # the rc dry-run, before the real release. run: | TAG="${GITHUB_REF#refs/tags/v}" echo "tag version: $TAG"; ls -1 dist/ SDIST=$(ls dist/ssik-*.tar.gz 2>/dev/null | head -1) [ -n "$SDIST" ] || { echo "::error::no sdist in dist/"; exit 1; } BUILT=$(basename "$SDIST" .tar.gz); BUILT="${BUILT#ssik-}" echo "built version: $BUILT" if [ "$BUILT" != "$TAG" ]; then echo "::error::built version '$BUILT' != tag '$TAG' -- refusing to publish (likely two tags on one commit; ensure a single v* tag points at the release commit)" exit 1 fi for w in dist/ssik-*.whl; do case "$(basename "$w")" in ssik-"$TAG"-*) ;; *) echo "::error::wheel $(basename "$w") is not version $TAG"; exit 1 ;; esac done echo "OK: all dist artifacts are version $TAG" - name: Publish to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ publish_pypi: timeout-minutes: 30 name: Publish → PyPI needs: [build_wheels, build_sdist] runs-on: latchkey-small # Real PyPI: only on v* tags that DON'T contain 'rc' (e.g. v1.0.0, # v1.0.1). The RC dry-run job above handles v*rc* tags. if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'rc') environment: name: release url: https://pypi.org/p/ssik permissions: id-token: write steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Verify built version matches the tag # Guard against the "two v* tags on one commit" footgun (#364): when # both e.g. v3.0.0rc1 and v3.0.0 point at the same commit, hatch-vcs # (via ``git describe``) can resolve the *rc*, so the v3.0.0 run builds # ssik-3.0.0rc1 wheels and would publish those to real PyPI. Refuse to # publish unless every artifact carries the tag's exact version. Tag # convention: ``vX.Y.Z`` / ``vX.Y.ZrcN`` (no dash; already PEP 440). run: | TAG="${GITHUB_REF#refs/tags/v}" echo "tag version: $TAG"; ls -1 dist/ SDIST=$(ls dist/ssik-*.tar.gz 2>/dev/null | head -1) [ -n "$SDIST" ] || { echo "::error::no sdist in dist/"; exit 1; } BUILT=$(basename "$SDIST" .tar.gz); BUILT="${BUILT#ssik-}" echo "built version: $BUILT" if [ "$BUILT" != "$TAG" ]; then echo "::error::built version '$BUILT' != tag '$TAG' -- refusing to publish (likely two tags on one commit; ensure a single v* tag points at the release commit)" exit 1 fi for w in dist/ssik-*.whl; do case "$(basename "$w")" in ssik-"$TAG"-*) ;; *) echo "::error::wheel $(basename "$w") is not version $TAG"; exit 1 ;; esac done echo "OK: all dist artifacts are version $TAG" - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 create_github_release: timeout-minutes: 30 name: Create GitHub Release needs: publish_pypi runs-on: latchkey-small # Only on stable releases (non-RC). The GitHub Release is what fires # the Zenodo GitHub-integration webhook, which mints a new version DOI # (concept DOI stays constant). Without this step, tags exist but # Zenodo never archives them. if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'rc') permissions: contents: write # required to create the Release steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # needed for auto-generated release notes - name: Create Release # action-gh-release is idempotent: if a Release already exists for # this tag (e.g. created manually before the workflow finished), # it updates rather than failing. uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: ssik ${{ github.ref_name }} generate_release_notes: true # GitHub auto-generates from merged PRs since previous tag draft: false prerelease: false
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
4 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
This workflow runs 5 jobs (7 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.