Refactor Docker image builds to use Depot (#13459)

## Summary

Simplify the Docker image build process to leverage Depot container
builders for faster layer caching and native multi-platform image
builds. The combo of the two removes the need to save cache to
registries and do complex merge operations across GHA runners to get a
multi-platform image.

## Test Plan

UV team will need to add a trust relationship in Depot so that the
container builds can authenticate and run. This can be done following
these docs:
https://depot.dev/docs/cli/authentication#adding-a-trust-relationship-for-github-actions.

Once that is done, this should just work as before, but without all of
the extra work around manifests. We should double that all of the
tagging still makes sense for you all, as some bits of that were
unclear.

Additional context in this draft PR:
https://github.com/astral-sh/uv/pull/9156

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Kyle Galbraith 2025-06-16 20:32:35 +02:00 committed by GitHub
parent 423cfaabf5
commit d73d3e8b53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 57 additions and 177 deletions

View File

@ -1,11 +1,19 @@
# Build and publish a Docker image.
# Build and publish Docker images.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
# Uses Depot for multi-platform builds. Includes both a `uv` base image, which
# is just the binary in a scratch image, and a set of extra, common images with
# the uv binary installed.
#
# TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but
# sharing the built image as an artifact between jobs is challenging.
name: "Build Docker image"
# Images are built on all runs.
#
# On release, assumed to run as a subworkflow of .github/workflows/release.yml;
# specifically, as a local artifacts job within `cargo-dist`. In this case,
# images are published based on the `plan`.
#
# TODO(charlie): Ideally, the publish step would happen as a publish job within
# `cargo-dist`, but sharing the built image as an artifact between jobs is
# challenging.
name: "Docker images"
on:
workflow_call:
@ -32,18 +40,19 @@ env:
UV_BASE_IMG: ghcr.io/${{ github.repository_owner }}/uv
jobs:
docker-build:
docker-publish-base:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
name: Build Docker image (ghcr.io/astral-sh/uv) for ${{ matrix.platform }}
name: uv
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for Depot OIDC
packages: write # for GHCR
environment:
name: release
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
outputs:
image-tags: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
@ -57,14 +66,14 @@ jobs:
username: astralshbot
password: ${{ secrets.DOCKERHUB_TOKEN_RO }}
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: |
@ -87,96 +96,33 @@ jobs:
tags: |
type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Normalize Platform Pair (replace / with -)
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: 7hd4vdzmw5 # astral-sh/uv
context: .
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=uv-${{ env.PLATFORM_TUPLE }}
cache-to: type=gha,mode=min,scope=uv-${{ env.PLATFORM_TUPLE }}
platforms: linux/amd64,linux/arm64
push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.UV_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Export digests
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Generate artifact attestation for base image
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
name: digests-${{ env.PLATFORM_TUPLE }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
docker-publish:
name: Publish Docker image (ghcr.io/astral-sh/uv)
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-build
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
# Login to DockerHub first, to avoid rate-limiting
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: astralshbot
password: ${{ secrets.DOCKERHUB_TOKEN_RO }}
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.UV_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
tags: |
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Create manifest list and push
working-directory: /tmp/digests
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
# The printf will expand the base image with the `<UV_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <UV_BASE_IMG>@sha256:<sha256_1> <UV_BASE_IMG>@sha256:<sha256_2> ...`
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.UV_BASE_IMG }}@sha256:%s ' *)
subject-name: ${{ env.UV_BASE_IMG }}
subject-digest: ${{ steps.build.outputs.digest }}
docker-publish-extra:
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
name: ${{ matrix.image-mapping }}
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-publish
- docker-publish-base
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
permissions:
packages: write
@ -215,18 +161,19 @@ jobs:
steps:
# Login to DockerHub first, to avoid rate-limiting
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }}
with:
username: astralshbot
password: ${{ secrets.DOCKERHUB_TOKEN_RO }}
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5
- name: Generate Dynamic Dockerfile Tags
shell: bash
run: |
@ -257,9 +204,6 @@ jobs:
# Remove the trailing newline from the pattern list
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
# Export image cache name
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
# Export tag patterns using the multiline env var syntax
{
echo "TAG_PATTERNS<<EOF"
@ -282,13 +226,11 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
context: .
project: 7hd4vdzmw5 # astral-sh/uv
platforms: linux/amd64,linux/arm64
# We do not really need to cache here as the Dockerfile is tiny
#cache-from: type=gha,scope=uv-${{ env.IMAGE_REF }}
#cache-to: type=gha,mode=min,scope=uv-${{ env.IMAGE_REF }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@ -299,93 +241,31 @@ jobs:
with:
subject-name: ${{ env.UV_BASE_IMG }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
# push-to-registry is explicitly not enabled to maintain full control over the top image
# This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/uv/pkgs/container/uv
# show the uv base image first since GitHub always shows the last updated image digests
# This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io
docker-republish:
name: Annotate Docker image (ghcr.io/astral-sh/uv)
# Re-tag the base image, to ensure it's shown as the newest on the registry UI
docker-retag-base:
name: retag uv
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-publish-base
- docker-publish-extra
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
permissions:
packages: write
attestations: write # needed to push image attestations to the Github attestation store
id-token: write # needed for signing the images with GitHub OIDC Token
steps:
# Login to DockerHub first, to avoid rate-limiting
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: astralshbot
password: ${{ secrets.DOCKERHUB_TOKEN_RO }}
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
images: ${{ env.UV_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
tags: |
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Create manifest list and push
working-directory: /tmp/digests
# The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces)
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
# The printf will expand the base image with the `<UV_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <UV_BASE_IMG>@sha256:<sha256_1> <UV_BASE_IMG>@sha256:<sha256_2> ...`
- name: Push tags
env:
IMAGE: ${{ env.UV_BASE_IMG }}
DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }}
TAGS: ${{ needs.docker-publish-base.outputs.image-tags }}
run: |
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
docker buildx imagetools create \
"${annotations[@]}" \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.UV_BASE_IMG }}@sha256:%s ' *)
- name: Share manifest digest
id: manifest-digest
# To sign the manifest, we need it's digest. Unfortunately "docker
# buildx imagetools create" does not (yet) have a clean way of sharing
# the digest of the manifest it creates (see docker/buildx#2407), so
# we use a separate command to retrieve it.
# imagetools inspect [TAG] --format '{{json .Manifest}}' gives us
# the machine readable JSON description of the manifest, and the
# jq command extracts the digest from this. The digest is then
# sent to the Github step output file for sharing with other steps.
run: |
digest="$(
docker buildx imagetools inspect \
"${UV_BASE_IMG}:${DOCKER_METADATA_OUTPUT_VERSION}" \
--format '{{json .Manifest}}' \
| jq -r '.digest'
)"
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
- name: Generate artifact attestation
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-name: ${{ env.UV_BASE_IMG }}
subject-digest: ${{ steps.manifest-digest.outputs.digest }}
# push-to-registry is explicitly not enabled to maintain full control over the top image
docker pull "${IMAGE}@${DIGEST}"
for tag in $TAGS; do
docker tag "${IMAGE}@${DIGEST}" "${tag}"
docker push "${tag}"
done