litellm/.github/workflows/release-docker.yml
lee-mcfaul2 ab81dd40ea
ci: add OIDC-rooted keyless release pipeline with SLSA L3 provenance
Publishes LiteLLM to PyPI and GHCR entirely over OIDC, with no
long-lived signing keys or registry credentials:

- PyPI: OIDC Trusted Publisher upload with PEP 740 attestations,
  SLSA L3 build provenance via actions/attest-build-provenance,
  and detached keyless cosign signatures on the sdist and wheel.
- Docker: a reusable build-push-sign workflow for the three images
  (litellm, -database, -non-root), keyless cosign signing, and
  SLSA L3 provenance attached as an OCI referrer.
- Consumer-style verify jobs that re-check every signature and
  attestation the way a downstream user would (gh attestation
  verify, cosign verify), so a broken pipeline fails loudly.
- A regression test enforcing the supply-chain invariants:
  SHA-pinned actions, keyless-only, OIDC-only, no static key.

The static cosign.pub is removed; keyless verification roots in
Fulcio/Rekor, not a checked-in public key.
2026-05-16 00:13:24 -04:00

173 lines
5.9 KiB
YAML

name: Build, Publish and Sign LiteLLM Docker images
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v1.83.14-stable)"
required: true
commit_hash:
description: "Source commit hash the tag must resolve to"
required: true
release_type:
description: "stable | nightly | rc | poc"
type: string
default: "stable"
enable_docker_hub:
description: "Also push to docker.io/litellm/* (requires Docker Hub OIDC federation)"
type: boolean
default: false
permissions: {}
jobs:
preflight:
name: Preflight tag/commit verification
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
environment: docker-release
# Only run in the canonical repository — never in forks.
if: github.repository == 'BerriAI/litellm'
outputs:
tag: ${{ inputs.tag }}
commit_hash: ${{ inputs.commit_hash }}
steps:
- name: Enforce tag-ref dispatch
env:
EXPECTED_REF: refs/tags/${{ inputs.tag }}
run: |
set -euo pipefail
if [ "${GITHUB_REF}" != "${EXPECTED_REF}" ]; then
echo "::error::Dispatch this workflow from the release tag, not a branch. github.ref=${GITHUB_REF}, expected ${EXPECTED_REF}. The keyless cosign certificate identity binds to the dispatch ref; a branch dispatch would push images and then fail signature verification."
exit 1
fi
echo "Dispatch ref OK: ${GITHUB_REF}"
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
ref: ${{ inputs.tag }}
persist-credentials: false
- name: Verify tag resolves to commit_hash
env:
TAG: ${{ inputs.tag }}
EXPECTED: ${{ inputs.commit_hash }}
run: |
set -euo pipefail
RESOLVED=$(git rev-parse "${TAG}^{commit}")
if [ "${RESOLVED}" != "${EXPECTED}" ]; then
echo "::error::Tag ${TAG} resolves to ${RESOLVED}, expected ${EXPECTED}"
exit 1
fi
echo "Tag ${TAG} -> ${RESOLVED} OK"
build-litellm:
name: Build, push, sign — litellm
needs: preflight
permissions:
id-token: write
contents: read
packages: write
attestations: write
uses: ./.github/workflows/_publish-container.yml
with:
image-name: litellm
dockerfile: ./Dockerfile
context: .
tag: ${{ needs.preflight.outputs.tag }}
commit-hash: ${{ needs.preflight.outputs.commit_hash }}
enable-docker-hub: ${{ inputs.enable_docker_hub }}
build-litellm-database:
name: Build, push, sign — litellm-database
needs: preflight
permissions:
id-token: write
contents: read
packages: write
attestations: write
uses: ./.github/workflows/_publish-container.yml
with:
image-name: litellm-database
dockerfile: ./docker/Dockerfile.database
context: .
tag: ${{ needs.preflight.outputs.tag }}
commit-hash: ${{ needs.preflight.outputs.commit_hash }}
enable-docker-hub: ${{ inputs.enable_docker_hub }}
build-litellm-non-root:
name: Build, push, sign — litellm-non-root
needs: preflight
permissions:
id-token: write
contents: read
packages: write
attestations: write
uses: ./.github/workflows/_publish-container.yml
with:
image-name: litellm-non-root
dockerfile: ./docker/Dockerfile.non_root
context: .
tag: ${{ needs.preflight.outputs.tag }}
commit-hash: ${{ needs.preflight.outputs.commit_hash }}
enable-docker-hub: ${{ inputs.enable_docker_hub }}
verify-all:
name: Final verification (consumer-style)
needs:
- preflight
- build-litellm
- build-litellm-database
- build-litellm-non-root
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: read
attestations: read
strategy:
fail-fast: false
matrix:
include:
- name: litellm
digest: ${{ needs.build-litellm.outputs.digest }}
- name: litellm-database
digest: ${{ needs.build-litellm-database.outputs.digest }}
- name: litellm-non-root
digest: ${{ needs.build-litellm-non-root.outputs.digest }}
steps:
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: 'v3.0.6' # Keep in sync with _publish-container.yml's default
- name: Log in to GHCR (read-only, for attestation referrer pull)
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify cosign signature
env:
DIGEST: ${{ matrix.digest }}
TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
TAG_ESC=$(printf '%s' "${TAG}" | sed 's/\./\\./g')
# Cert identity reflects the reusable workflow that signed (job_workflow_ref).
IDENTITY_RE="^https://github\.com/${GITHUB_REPOSITORY}/\.github/workflows/_publish-container\.yml@refs/tags/${TAG_ESC}$"
cosign verify \
--certificate-identity-regexp="${IDENTITY_RE}" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"ghcr.io/${{ github.repository_owner }}/${{ matrix.name }}@${DIGEST}"
- name: Verify SLSA provenance via gh attestation
env:
DIGEST: ${{ matrix.digest }}
IMAGE: ${{ matrix.name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh attestation verify \
"oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/${IMAGE}@${DIGEST}" \
--owner "${GITHUB_REPOSITORY_OWNER}"