Release to PyPI workflow (cocoindex-io/cocoindex-code)
The Release to PyPI workflow from cocoindex-io/cocoindex-code, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, 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 to PyPI workflow from the cocoindex-io/cocoindex-code repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Release to PyPI
on:
release:
types: [published]
workflow_dispatch:
inputs:
test_pypi_only:
description: 'Publish to TestPyPI only'
required: false
type: boolean
default: false
test_docker:
description: 'Build & push the Docker image (tagged :test) without running PyPI publish - use to verify Docker Hub / GHCR credentials'
required: false
type: boolean
default: false
permissions:
contents: read
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Build package
run: uv build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
publish-to-testpypi:
name: Publish to TestPyPI
if: github.event_name == 'workflow_dispatch' && inputs.test_pypi_only
needs:
- build
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/cocoindex-code
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-to-pypi:
name: Publish to PyPI
if: github.event_name == 'release'
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/cocoindex-code
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github-release:
name: Upload distribution to GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Upload distributions to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release upload
'${{ github.ref_name }}' dist/*
--repo '${{ github.repository }}'
publish-docker:
name: Build & push Docker image (${{ matrix.variant }})
# Runs on real releases, and on manual dispatch with `test_docker=true`
# for verifying registry credentials before the first release.
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.test_docker)
runs-on: ubuntu-latest
environment:
name: docker-hub
url: https://hub.docker.com/r/cocoindex/cocoindex-code
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
# slim (default) - LiteLLM-only, ~300 MB. Publishes as `:latest`.
- variant: slim
install_spec: /ccc-src
# full - bundles sentence-transformers + torch + baked model,
# ~2 GB. Publishes as `:full`.
- variant: full
install_spec: /ccc-src[full]
steps:
- uses: actions/checkout@v4
# QEMU lets buildx cross-compile linux/arm64 on the x86_64 runner.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tags
id: tags
# Tag scheme:
# slim on release: :latest, :<version>
# full on release: :full, :<version>-full
# slim on dispatch: :test
# full on dispatch: :test-full
# Dispatched tags stay out of the `:latest` / `:<version>` namespace
# so manual test runs don't clobber what users pull.
run: |
variant="${{ matrix.variant }}"
if [ "$variant" = "slim" ]; then
slim_suffix=""
else
slim_suffix="-$variant"
fi
if [ "${{ github.event_name }}" = "release" ]; then
version="${{ github.ref_name }}"
if [ "$variant" = "slim" ]; then
latest_tag="latest"
else
latest_tag="$variant"
fi
{
echo "tags<<EOF"
echo "cocoindex/cocoindex-code:${latest_tag}"
echo "cocoindex/cocoindex-code:${version}${slim_suffix}"
echo "ghcr.io/cocoindex-io/cocoindex-code:${latest_tag}"
echo "ghcr.io/cocoindex-io/cocoindex-code:${version}${slim_suffix}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
test_tag="test${slim_suffix}"
{
echo "tags<<EOF"
echo "cocoindex/cocoindex-code:${test_tag}"
echo "ghcr.io/cocoindex-io/cocoindex-code:${test_tag}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- name: Build and push to both registries
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
# Native amd64 for Linux servers + Intel Macs; arm64 (cross-compiled
# via QEMU) for Apple Silicon Macs and arm64 Linux hosts.
platforms: linux/amd64,linux/arm64
# Install cocoindex-code from the checked-out source tree, not PyPI.
# Avoids a race where a just-published version hasn't propagated to
# PyPI's CDN yet (which happened on v0.2.24 release), and ensures
# the image matches the tagged commit byte-for-byte.
build-args: |
CCC_VARIANT=${{ matrix.variant }}
CCC_INSTALL_SPEC=${{ matrix.install_spec }}
tags: ${{ steps.tags.outputs.tags }}
# Per-variant BuildKit cache so slim and full don't evict each
# other's layers. The heavy `deps` layer (torch + friends for
# full; empty for slim) reuses across releases.
# Registry-backed BuildKit cache, stored as a separate tag
# (`buildcache-<variant>`) on the same GHCR package. Independent of
# branch/tag, so cache reuses across releases (every release tag is
# a different ref, which would have made GHA-scoped caching miss).
# `mode=max` exports all layers, including intermediate ones.
cache-from: type=registry,ref=ghcr.io/cocoindex-io/cocoindex-code:buildcache-${{ matrix.variant }}
cache-to: type=registry,ref=ghcr.io/cocoindex-io/cocoindex-code:buildcache-${{ matrix.variant }},mode=max
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: Release to PyPI on: release: types: [published] workflow_dispatch: inputs: test_pypi_only: description: 'Publish to TestPyPI only' required: false type: boolean default: false test_docker: description: 'Build & push the Docker image (tagged :test) without running PyPI publish - use to verify Docker Hub / GHCR credentials' required: false type: boolean default: false permissions: contents: read jobs: build: timeout-minutes: 30 name: Build distribution runs-on: latchkey-small steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: cache: 'pip' python-version: '3.11' - name: Install uv uses: astral-sh/setup-uv@v5 - name: Build package run: uv build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-testpypi: timeout-minutes: 30 name: Publish to TestPyPI if: github.event_name == 'workflow_dispatch' && inputs.test_pypi_only needs: - build runs-on: latchkey-small environment: name: testpypi url: https://test.pypi.org/p/cocoindex-code permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ publish-to-pypi: timeout-minutes: 30 name: Publish to PyPI if: github.event_name == 'release' needs: - build runs-on: latchkey-small environment: name: pypi url: https://pypi.org/p/cocoindex-code permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 github-release: timeout-minutes: 30 name: Upload distribution to GitHub Release needs: - publish-to-pypi runs-on: latchkey-small permissions: contents: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Upload distributions to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release upload '${{ github.ref_name }}' dist/* --repo '${{ github.repository }}' publish-docker: timeout-minutes: 30 name: Build & push Docker image (${{ matrix.variant }}) # Runs on real releases, and on manual dispatch with `test_docker=true` # for verifying registry credentials before the first release. if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.test_docker) runs-on: latchkey-small environment: name: docker-hub url: https://hub.docker.com/r/cocoindex/cocoindex-code permissions: contents: read packages: write strategy: fail-fast: false matrix: include: # slim (default) - LiteLLM-only, ~300 MB. Publishes as `:latest`. - variant: slim install_spec: /ccc-src # full - bundles sentence-transformers + torch + baked model, # ~2 GB. Publishes as `:full`. - variant: full install_spec: /ccc-src[full] steps: - uses: actions/checkout@v4 # QEMU lets buildx cross-compile linux/arm64 on the x86_64 runner. - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub uses: docker/login-action@v3 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Compute image tags id: tags # Tag scheme: # slim on release: :latest, :<version> # full on release: :full, :<version>-full # slim on dispatch: :test # full on dispatch: :test-full # Dispatched tags stay out of the `:latest` / `:<version>` namespace # so manual test runs don't clobber what users pull. run: | variant="${{ matrix.variant }}" if [ "$variant" = "slim" ]; then slim_suffix="" else slim_suffix="-$variant" fi if [ "${{ github.event_name }}" = "release" ]; then version="${{ github.ref_name }}" if [ "$variant" = "slim" ]; then latest_tag="latest" else latest_tag="$variant" fi { echo "tags<<EOF" echo "cocoindex/cocoindex-code:${latest_tag}" echo "cocoindex/cocoindex-code:${version}${slim_suffix}" echo "ghcr.io/cocoindex-io/cocoindex-code:${latest_tag}" echo "ghcr.io/cocoindex-io/cocoindex-code:${version}${slim_suffix}" echo "EOF" } >> "$GITHUB_OUTPUT" else test_tag="test${slim_suffix}" { echo "tags<<EOF" echo "cocoindex/cocoindex-code:${test_tag}" echo "ghcr.io/cocoindex-io/cocoindex-code:${test_tag}" echo "EOF" } >> "$GITHUB_OUTPUT" fi - name: Build and push to both registries uses: docker/build-push-action@v5 with: context: . file: docker/Dockerfile push: true # Native amd64 for Linux servers + Intel Macs; arm64 (cross-compiled # via QEMU) for Apple Silicon Macs and arm64 Linux hosts. platforms: linux/amd64,linux/arm64 # Install cocoindex-code from the checked-out source tree, not PyPI. # Avoids a race where a just-published version hasn't propagated to # PyPI's CDN yet (which happened on v0.2.24 release), and ensures # the image matches the tagged commit byte-for-byte. build-args: | CCC_VARIANT=${{ matrix.variant }} CCC_INSTALL_SPEC=${{ matrix.install_spec }} tags: ${{ steps.tags.outputs.tags }} # Per-variant BuildKit cache so slim and full don't evict each # other's layers. The heavy `deps` layer (torch + friends for # full; empty for slim) reuses across releases. # Registry-backed BuildKit cache, stored as a separate tag # (`buildcache-<variant>`) on the same GHCR package. Independent of # branch/tag, so cache reuses across releases (every release tag is # a different ref, which would have made GHA-scoped caching miss). # `mode=max` exports all layers, including intermediate ones. cache-from: type=registry,ref=ghcr.io/cocoindex-io/cocoindex-code:buildcache-${{ matrix.variant }} cache-to: type=registry,ref=ghcr.io/cocoindex-io/cocoindex-code:buildcache-${{ matrix.variant }},mode=max
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. - 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.
6 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.
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:
- Container pulls and builds
This workflow runs 5 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.