diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1f5229aef..1ac48ee0f 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -80,7 +80,9 @@ jobs: name: release outputs: image-tags: ${{ steps.meta.outputs.tags }} + image-annotations: ${{ steps.meta.outputs.annotations }} image-digest: ${{ steps.build.outputs.digest }} + image-version: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -117,6 +119,8 @@ jobs: - 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_GHCR_IMAGE }} @@ -137,10 +141,12 @@ jobs: push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # TODO(zanieb): Annotations are not supported by Depot yet and are ignored + annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation for base image if: ${{ needs.docker-plan.outputs.push == 'true' }} - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} @@ -153,7 +159,6 @@ jobs: needs: - docker-plan - docker-publish-base - if: ${{ needs.docker-plan.outputs.push == 'true' }} permissions: id-token: write # for Depot OIDC and GHCR signing packages: write # for GHCR image pushes @@ -263,20 +268,66 @@ jobs: context: . project: 7hd4vdzmw5 # astral-sh/uv platforms: linux/amd64,linux/arm64 - push: true + push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # TODO(zanieb): Annotations are not supported by Depot yet and are ignored annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation + if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build-and-push.outputs.digest }} - # Re-tag the base image, to ensure it's shown as the newest on the registry UI - docker-retag-base: - name: retag uv + # Push annotations manually. + # See `docker-annotate-base` for details. + - name: Add annotations to images + if: ${{ needs.docker-plan.outputs.push == 'true' }} + env: + IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" + DIGEST: ${{ steps.build-and-push.outputs.digest }} + TAGS: ${{ steps.meta.outputs.tags }} + ANNOTATIONS: ${{ steps.meta.outputs.annotations }} + run: | + set -x + readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done + for image in $IMAGES; do + readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done + docker buildx imagetools create \ + "${annotations[@]}" \ + "${tags[@]}" \ + "${image}@${DIGEST}" + done + + # See `docker-annotate-base` for details. + - name: Export manifest digest + id: manifest-digest + if: ${{ needs.docker-plan.outputs.push == 'true' }} + env: + IMAGE: ${{ env.UV_GHCR_IMAGE }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + digest="$( + docker buildx imagetools inspect \ + "${IMAGE}:${VERSION}" \ + --format '{{json .Manifest}}' \ + | jq -r '.digest' + )" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + # See `docker-annotate-base` for details. + - name: Generate artifact attestation + if: ${{ needs.docker-plan.outputs.push == 'true' }} + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + with: + subject-name: ${{ env.UV_GHCR_IMAGE }} + subject-digest: ${{ steps.manifest-digest.outputs.digest }} + + # Annotate the base image + docker-annotate-base: + name: annotate uv runs-on: ubuntu-latest environment: name: release @@ -286,24 +337,67 @@ jobs: - docker-publish-extra if: ${{ needs.docker-plan.outputs.push == 'true' }} steps: + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + username: astral + password: ${{ secrets.DOCKERHUB_TOKEN_RW }} + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Push tags + # Depot doesn't support annotating images, so we need to do so manually + # afterwards. Mutating the manifest is desirable regardless, because we + # want to bump the base image to appear at the top of the list on GHCR. + # However, once annotation support is added to Depot, this step can be + # minimized to just touch the GHCR manifest. + - name: Add annotations to images env: - IMAGE: ${{ env.UV_GHCR_IMAGE }} + IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }} TAGS: ${{ needs.docker-publish-base.outputs.image-tags }} + ANNOTATIONS: ${{ needs.docker-publish-base.outputs.image-annotations }} + # The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces) + # The final command becomes `docker buildx imagetools create --annotation 'index:foo=1' --annotation 'index:bar=2' ... -t tag1 -t tag2 ... @sha256:` run: | - docker pull "${IMAGE}@${DIGEST}" - for tag in $TAGS; do - # Skip re-tag for DockerHub - if [[ "$tag" == "${{ env.UV_DOCKERHUB_IMAGE }}"* ]]; then - continue - fi - docker tag "${IMAGE}@${DIGEST}" "${tag}" - docker push "${tag}" + set -x + readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done + for image in $IMAGES; do + readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done + docker buildx imagetools create \ + "${annotations[@]}" \ + "${tags[@]}" \ + "${image}@${DIGEST}" done + + # Now that we've modified the manifest, we need to attest it again. + # Note we only generate an attestation for GHCR. + - name: Export manifest digest + id: manifest-digest + env: + IMAGE: ${{ env.UV_GHCR_IMAGE }} + VERSION: ${{ needs.docker-publish-base.outputs.image-version }} + # 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 \ + "${IMAGE}:${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_GHCR_IMAGE }} + subject-digest: ${{ steps.manifest-digest.outputs.digest }}