Skip to content
Latchkey

Release to PyPI workflow (cocoindex-io/cocoindex-code)

The Release to PyPI workflow from cocoindex-io/cocoindex-code, explained and optimized by Latchkey.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: cocoindex-io/cocoindex-code.github/workflows/release.ymlLicense Apache-2.0View source

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

workflow (.yml)
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

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:

This workflow runs 5 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow