diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1488774b0..ce12d3c5d 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -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 `@sha256: ...` for each sha256 in the directory - # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... @sha256: @sha256: ...` - 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<@sha256: ...` for each sha256 in the directory - # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... @sha256: @sha256: ...` + - 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