Publish Python π distributions π¦ to PyPI and TestPyPI workflow (graphistry/pygraphistry)
The Publish Python π distributions π¦ to PyPI and TestPyPI workflow from graphistry/pygraphistry, explained and optimized by Latchkey.
CI health: F - at risk
Point runs-on at Latchkey and get caching, 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 Publish Python π distributions π¦ to PyPI and TestPyPI workflow from the graphistry/pygraphistry 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: Publish Python π distributions π¦ to PyPI and TestPyPI
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
release_mode:
description: "Release mode: evidence (no publish), test (TestPyPI), or release (TestPyPI + PyPI)"
required: true
type: choice
options:
- evidence
- test
- release
default: evidence
env:
UV_EXCLUDE_NEWER: "6 days"
EXPECTED_REPOSITORY: graphistry/pygraphistry
EXPECTED_WORKFLOW_PATH: .github/workflows/publish-pypi.yml
jobs:
build-evidence:
name: Build release artifacts and evidence
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
(github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
with: # fetch tag for versioneer
fetch-depth: 0
persist-credentials: false
- name: Verify trusted release trigger
run: |
set -euo pipefail
expected_workflow_ref="${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_PATH}@${GITHUB_REF}"
if [[ "${GITHUB_REPOSITORY}" != "${EXPECTED_REPOSITORY}" ]]; then
echo "::error::Unexpected repository ${GITHUB_REPOSITORY}; expected ${EXPECTED_REPOSITORY}."
exit 1
fi
if [[ -z "${GITHUB_WORKFLOW_REF:-}" || "${GITHUB_WORKFLOW_REF}" != "${expected_workflow_ref}" ]]; then
echo "::error::Unexpected workflow ref ${GITHUB_WORKFLOW_REF:-<unset>}; expected ${expected_workflow_ref}."
exit 1
fi
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
tag="${GITHUB_REF#refs/tags/}"
if [[ ! "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9._-]*)?$ ]]; then
echo "::error::Tag ${tag} is not a supported release tag format (expected X.Y.Z[extra])."
exit 1
fi
# Refuse tag publishes that do not point to commits in master history.
git fetch --no-tags origin master
if ! git merge-base --is-ancestor "${GITHUB_SHA}" "origin/master"; then
echo "::error::Ref ${GITHUB_REF} (${GITHUB_SHA}) is not in origin/master history; refusing publish."
exit 1
fi
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
# Dispatch-specific publish restrictions are enforced at job/step `if` guards.
true
else
echo "::error::Unexpected event ${GITHUB_EVENT_NAME}; refusing publish."
exit 1
fi
- name: Set up Python 3.8
uses: actions/setup-python@v5
with:
python-version: '3.8'
- name: Normalize version for TestPyPI dispatch publishes
if: >-
github.event_name == 'workflow_dispatch' &&
github.event.inputs.release_mode == 'test'
run: |
set -euo pipefail
synthetic_version="0.0.dev${GITHUB_RUN_ID}"
git tag -f "${synthetic_version}" "${GITHUB_SHA}"
echo "Using synthetic TestPyPI version: ${synthetic_version}"
git describe --tags --dirty --always --long
- name: Install uv (pinned)
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check --no-deps "uv==0.11.3"
- name: Install pypa/build
run: >-
UV_SYSTEM_PYTHON=1 uv pip install -e .[build]
- name: Build a binary wheel and a source tarball
run: >-
./bin/build.sh
- name: Generate SBOM (CycloneDX JSON)
run: |
set -euo pipefail
UV_SYSTEM_PYTHON=1 uv pip install cyclonedx-bom
mkdir -p evidence
cyclonedx-py environment --of JSON -o evidence/sbom-cyclonedx.json
- name: Attest built distributions provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: |
dist/*.whl
dist/*.tar.gz
- name: Upload built distributions
uses: actions/upload-artifact@v4
with:
name: release-dists-${{ github.run_id }}
path: |
dist/*.whl
dist/*.tar.gz
if-no-files-found: error
- name: Upload release evidence artifacts
uses: actions/upload-artifact@v4
with:
name: release-evidence-${{ github.run_id }}
path: |
evidence/sbom-cyclonedx.json
if-no-files-found: error
publish-testpypi:
name: Publish distribution π¦ to Test PyPI
needs: build-evidence
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.release_mode == 'test' || github.event.inputs.release_mode == 'release'))
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Download release evidence artifacts
uses: actions/download-artifact@v4
with:
name: release-dists-${{ github.run_id }}
path: dist
- name: Verify dist files for TestPyPI publish
run: |
set -euo pipefail
ls -al dist
ls -1 dist/*.whl dist/*.tar.gz >/dev/null
- name: Publish distribution π¦ to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
attestations: true
publish-pypi:
name: Publish distribution π¦ to PyPI
needs:
- build-evidence
- publish-testpypi
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.release_mode == 'release' &&
github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
permissions:
id-token: write
# Configure required reviewers on the `pypi-release` environment in repository settings.
# This creates a human approval gate before PyPI publish can mint OIDC creds.
environment:
name: pypi-release
steps:
- name: Download release evidence artifacts
uses: actions/download-artifact@v4
with:
name: release-dists-${{ github.run_id }}
path: dist
- name: Verify dist files for PyPI publish
run: |
set -euo pipefail
ls -al dist
ls -1 dist/*.whl dist/*.tar.gz >/dev/null
- name: Publish distribution π¦ to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
name: Publish Python π distributions π¦ to PyPI and TestPyPI on: push: tags: - '*' workflow_dispatch: inputs: release_mode: description: "Release mode: evidence (no publish), test (TestPyPI), or release (TestPyPI + PyPI)" required: true type: choice options: - evidence - test - release default: evidence env: UV_EXCLUDE_NEWER: "6 days" EXPECTED_REPOSITORY: graphistry/pygraphistry EXPECTED_WORKFLOW_PATH: .github/workflows/publish-pypi.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-evidence: timeout-minutes: 30 name: Build release artifacts and evidence if: >- (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || (github.event_name == 'workflow_dispatch') runs-on: latchkey-small permissions: contents: read id-token: write attestations: write steps: - uses: actions/checkout@v4 with: # fetch tag for versioneer fetch-depth: 0 persist-credentials: false - name: Verify trusted release trigger run: | set -euo pipefail expected_workflow_ref="${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_PATH}@${GITHUB_REF}" if [[ "${GITHUB_REPOSITORY}" != "${EXPECTED_REPOSITORY}" ]]; then echo "::error::Unexpected repository ${GITHUB_REPOSITORY}; expected ${EXPECTED_REPOSITORY}." exit 1 fi if [[ -z "${GITHUB_WORKFLOW_REF:-}" || "${GITHUB_WORKFLOW_REF}" != "${expected_workflow_ref}" ]]; then echo "::error::Unexpected workflow ref ${GITHUB_WORKFLOW_REF:-<unset>}; expected ${expected_workflow_ref}." exit 1 fi if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then tag="${GITHUB_REF#refs/tags/}" if [[ ! "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9._-]*)?$ ]]; then echo "::error::Tag ${tag} is not a supported release tag format (expected X.Y.Z[extra])." exit 1 fi # Refuse tag publishes that do not point to commits in master history. git fetch --no-tags origin master if ! git merge-base --is-ancestor "${GITHUB_SHA}" "origin/master"; then echo "::error::Ref ${GITHUB_REF} (${GITHUB_SHA}) is not in origin/master history; refusing publish." exit 1 fi elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then # Dispatch-specific publish restrictions are enforced at job/step `if` guards. true else echo "::error::Unexpected event ${GITHUB_EVENT_NAME}; refusing publish." exit 1 fi - name: Set up Python 3.8 uses: actions/setup-python@v5 with: cache: 'pip' python-version: '3.8' - name: Normalize version for TestPyPI dispatch publishes if: >- github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'test' run: | set -euo pipefail synthetic_version="0.0.dev${GITHUB_RUN_ID}" git tag -f "${synthetic_version}" "${GITHUB_SHA}" echo "Using synthetic TestPyPI version: ${synthetic_version}" git describe --tags --dirty --always --long - name: Install uv (pinned) run: | set -euo pipefail python -m pip install --disable-pip-version-check --no-deps "uv==0.11.3" - name: Install pypa/build run: >- UV_SYSTEM_PYTHON=1 uv pip install -e .[build] - name: Build a binary wheel and a source tarball run: >- ./bin/build.sh - name: Generate SBOM (CycloneDX JSON) run: | set -euo pipefail UV_SYSTEM_PYTHON=1 uv pip install cyclonedx-bom mkdir -p evidence cyclonedx-py environment --of JSON -o evidence/sbom-cyclonedx.json - name: Attest built distributions provenance uses: actions/attest-build-provenance@v2 with: subject-path: | dist/*.whl dist/*.tar.gz - name: Upload built distributions uses: actions/upload-artifact@v4 with: name: release-dists-${{ github.run_id }} path: | dist/*.whl dist/*.tar.gz if-no-files-found: error - name: Upload release evidence artifacts uses: actions/upload-artifact@v4 with: name: release-evidence-${{ github.run_id }} path: | evidence/sbom-cyclonedx.json if-no-files-found: error publish-testpypi: timeout-minutes: 30 name: Publish distribution π¦ to Test PyPI needs: build-evidence if: >- (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.release_mode == 'test' || github.event.inputs.release_mode == 'release')) runs-on: latchkey-small permissions: id-token: write steps: - name: Download release evidence artifacts uses: actions/download-artifact@v4 with: name: release-dists-${{ github.run_id }} path: dist - name: Verify dist files for TestPyPI publish run: | set -euo pipefail ls -al dist ls -1 dist/*.whl dist/*.tar.gz >/dev/null - name: Publish distribution π¦ to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ attestations: true publish-pypi: timeout-minutes: 30 name: Publish distribution π¦ to PyPI needs: - build-evidence - publish-testpypi if: >- (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'release' && github.ref == 'refs/heads/master') runs-on: latchkey-small permissions: id-token: write # Configure required reviewers on the `pypi-release` environment in repository settings. # This creates a human approval gate before PyPI publish can mint OIDC creds. environment: name: pypi-release steps: - name: Download release evidence artifacts uses: actions/download-artifact@v4 with: name: release-dists-${{ github.run_id }} path: dist - name: Verify dist files for PyPI publish run: | set -euo pipefail ls -al dist ls -1 dist/*.whl dist/*.tar.gz >/dev/null - name: Publish distribution π¦ to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: attestations: true
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.
- Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.