diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 000000000000..8a263f966749 --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,336 @@ +name: Release Docker Image + +on: + push: + tags: + # Internal release tags only: v1.83.10-internal.1, v1.83.10-internal.2, etc. + - 'v*-internal.*' + +permissions: + contents: read + +env: + # Change this if you want a different Docker Hub repo name. + IMAGE_NAME: zsk2026/litellm + +jobs: + # --------------------------------------------------------------------------- + # 1. Build multi-arch image and push to Docker Hub + # --------------------------------------------------------------------------- + build-and-push: + name: Build and push multi-arch image + runs-on: ubuntu-latest + timeout-minutes: 90 + # Use an Environment to require manual approval before pushing. + # Optional — remove `environment:` if you trust tag pushes implicitly. + environment: docker-release + outputs: + digest: ${{ steps.build.outputs.digest }} + tag: ${{ steps.version.outputs.tag }} + base_version: ${{ steps.version.outputs.base_version }} + git_sha: ${{ steps.version.outputs.git_sha }} + git_sha_full: ${{ steps.version.outputs.git_sha_full }} + + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + fetch-depth: 0 # need full history for changelog generation in release job + + - name: Free up disk space + run: | + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/share/boost + sudo apt-get clean + df -h / + + - name: Verify tag is reachable from ship/* branch + run: | + TAG="${GITHUB_REF_NAME}" + echo "Validating tag: ${TAG}" + git fetch origin '+refs/heads/ship/*:refs/remotes/origin/ship/*' --no-tags + CONTAINING=$(git branch -r --contains "${TAG}" | grep 'origin/ship/' || true) + if [ -z "${CONTAINING}" ]; then + echo "::error::Tag ${TAG} is not on any ship/* branch. Refusing to publish." + exit 1 + fi + echo "Tag is on:${CONTAINING}" + + - name: Extract version components + id: version + run: | + TAG="${GITHUB_REF_NAME}" + BASE_VERSION="${TAG%-internal.*}" + GIT_SHA=$(git rev-parse --short HEAD) + GIT_SHA_FULL=$(git rev-parse HEAD) + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "base_version=${BASE_VERSION}" >> "$GITHUB_OUTPUT" + echo "git_sha=${GIT_SHA}" >> "$GITHUB_OUTPUT" + echo "git_sha_full=${GIT_SHA_FULL}" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU (for arm64 emulation) + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12 + + - name: Log in to Docker Hub + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: build + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }} + ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}-${{ steps.version.outputs.git_sha }} + ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.base_version }}-stable + labels: | + org.opencontainers.image.title=litellm + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ steps.version.outputs.git_sha_full }} + org.opencontainers.image.version=${{ steps.version.outputs.tag }} + org.opencontainers.image.created=${{ github.event.repository.updated_at }} + cache-from: type=gha + cache-to: type=gha,mode=max + # provenance=true would attach SLSA provenance attestation. We're + # using cosign signing in the next job instead. + provenance: false + + - name: Build summary + run: | + cat >> "$GITHUB_STEP_SUMMARY" < /dev/null + echo "Signature verified." + + - name: Sign summary + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cat >> "$GITHUB_STEP_SUMMARY" </dev/null 2>&1; then + PREV_TAG="${BASE_VERSION}-stable" + RANGE="${PREV_TAG}..${TAG}" + echo "First internal tag, using upstream pin: ${PREV_TAG}" + else + RANGE="HEAD~20..${TAG}" + echo "No baseline tag found, using last 20 commits" + fi + fi + + # Build changelog. Use HEREDOC delimiter for multi-line output. + { + echo "changelog<> "$GITHUB_OUTPUT" + + - name: Compose release notes + id: notes + env: + TAG: ${{ needs.build-and-push.outputs.tag }} + BASE_VERSION: ${{ needs.build-and-push.outputs.base_version }} + GIT_SHA: ${{ needs.build-and-push.outputs.git_sha }} + GIT_SHA_FULL: ${{ needs.build-and-push.outputs.git_sha_full }} + DIGEST: ${{ needs.build-and-push.outputs.digest }} + CHANGELOG: ${{ steps.changelog.outputs.changelog }} + PREV_TAG: ${{ steps.changelog.outputs.prev_tag }} + run: | + cat > release-notes.md <> "$GITHUB_STEP_SUMMARY" <&2 + exit 2 +fi + +# Tag format: v..-internal. +if ! [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-internal\.[0-9]+$ ]]; then + echo "Error: tag must match vX.Y.Z-internal.N (got: ${VERSION})" >&2 + exit 2 +fi + +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "${BRANCH}" != ship/* ]]; then + echo "Error: must release from a ship/* branch (currently on: ${BRANCH})" >&2 + exit 2 +fi + +if [[ -n "$(git status --porcelain)" ]]; then + echo "Error: worktree is dirty. Commit or stash first." >&2 + exit 2 +fi + +# Ensure local branch is up to date with origin +git fetch origin "${BRANCH}" +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse "origin/${BRANCH}") +if [[ "${LOCAL}" != "${REMOTE}" ]]; then + echo "Error: local ${BRANCH} is not in sync with origin (local=${LOCAL} remote=${REMOTE})" >&2 + echo "Run: git pull --ff-only origin ${BRANCH}" >&2 + exit 2 +fi + +# Refuse to overwrite an existing tag +if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then + echo "Error: tag ${VERSION} already exists locally." >&2 + exit 2 +fi +if git ls-remote --tags origin "${VERSION}" | grep -q "${VERSION}"; then + echo "Error: tag ${VERSION} already exists on origin." >&2 + exit 2 +fi + +# Auto-generate changelog body from commits since the last internal tag +BASE_VERSION="${VERSION%-internal.*}" +LAST_TAG=$(git tag --list "${BASE_VERSION}-internal.*" --sort=-v:refname | head -n1 || true) +if [[ -n "${LAST_TAG}" ]]; then + RANGE="${LAST_TAG}..HEAD" + echo "Generating changelog from ${LAST_TAG}..HEAD" +else + RANGE="HEAD~10..HEAD" + echo "No previous internal tag found, using last 10 commits" +fi + +CHANGELOG=$(git log --pretty=format:'- %s (%h)' "${RANGE}" --no-merges | head -n 30) + +cat </litellm:${VERSION} + docker pull /litellm:${BASE_VERSION}-stable +EOF