replace manual venv removal with remove_virtualenv #1318
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Build and publish Docker images. | |
# | |
# 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. | |
# | |
# 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: | |
inputs: | |
plan: | |
required: true | |
type: string | |
pull_request: | |
paths: | |
# We want to ensure that the maturin builds still work when we change | |
# Project metadata | |
- pyproject.toml | |
- Cargo.toml | |
- .cargo/config.toml | |
# Toolchain or dependency versions | |
- Cargo.lock | |
- rust-toolchain.toml | |
# The Dockerfile itself | |
- Dockerfile | |
# And the workflow itself | |
- .github/workflows/build-docker.yml | |
env: | |
UV_GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/uv | |
UV_DOCKERHUB_IMAGE: docker.io/astral/uv | |
jobs: | |
docker-plan: | |
name: plan | |
runs-on: ubuntu-latest | |
outputs: | |
login: ${{ steps.plan.outputs.login }} | |
push: ${{ steps.plan.outputs.push }} | |
tag: ${{ steps.plan.outputs.tag }} | |
action: ${{ steps.plan.outputs.action }} | |
steps: | |
- name: Set push variable | |
env: | |
DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} | |
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }} | |
IS_LOCAL_PR: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} | |
id: plan | |
run: | | |
if [ "${{ env.DRY_RUN }}" == "false" ]; then | |
echo "login=true" >> "$GITHUB_OUTPUT" | |
echo "push=true" >> "$GITHUB_OUTPUT" | |
echo "tag=${{ env.TAG }}" >> "$GITHUB_OUTPUT" | |
echo "action=build and publish" >> "$GITHUB_OUTPUT" | |
else | |
echo "login=${{ env.IS_LOCAL_PR }}" >> "$GITHUB_OUTPUT" | |
echo "push=false" >> "$GITHUB_OUTPUT" | |
echo "tag=dry-run" >> "$GITHUB_OUTPUT" | |
echo "action=build" >> "$GITHUB_OUTPUT" | |
fi | |
docker-publish-base: | |
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} | |
name: ${{ needs.docker-plan.outputs.action }} uv | |
needs: | |
- docker-plan | |
runs-on: ubuntu-latest | |
permissions: | |
contents: read | |
id-token: write # for Depot OIDC and GHCR signing | |
packages: write # for GHCR image pushes | |
attestations: write # for GHCR attestations | |
environment: | |
name: ${{ needs.docker-plan.outputs.push == 'true' && '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: | |
submodules: recursive | |
# Login to DockerHub (when not pushing, it's to avoid rate-limiting) | |
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
if: ${{ needs.docker-plan.outputs.login == 'true' }} | |
with: | |
username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} | |
password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} | |
- 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: ${{ needs.docker-plan.outputs.push == 'true' }} | |
run: | | |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') | |
if [ "${{ needs.docker-plan.outputs.tag }}" != "${version}" ]; then | |
echo "The input tag does not match the version from pyproject.toml:" >&2 | |
echo "${{ needs.docker-plan.outputs.tag }}" >&2 | |
echo "${version}" >&2 | |
exit 1 | |
else | |
echo "Releasing ${version}" | |
fi | |
- 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 }} | |
${{ env.UV_DOCKERHUB_IMAGE }} | |
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name | |
tags: | | |
type=raw,value=dry-run,enable=${{ needs.docker-plan.outputs.push == 'false' }} | |
type=pep440,pattern={{ version }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} | |
type=pep440,pattern={{ major }}.{{ minor }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} | |
- name: Build and push by digest | |
id: build | |
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0 | |
with: | |
project: 7hd4vdzmw5 # astral-sh/uv | |
context: . | |
platforms: linux/amd64,linux/arm64 | |
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@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 | |
with: | |
subject-name: ${{ env.UV_GHCR_IMAGE }} | |
subject-digest: ${{ steps.build.outputs.digest }} | |
docker-publish-extra: | |
name: ${{ needs.docker-plan.outputs.action }} ${{ matrix.image-mapping }} | |
runs-on: ubuntu-latest | |
environment: | |
name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} | |
needs: | |
- docker-plan | |
- docker-publish-base | |
permissions: | |
id-token: write # for Depot OIDC and GHCR signing | |
packages: write # for GHCR image pushes | |
attestations: write # for GHCR attestations | |
strategy: | |
fail-fast: false | |
matrix: | |
# Mapping of base image followed by a comma followed by one or more base tags (comma separated) | |
# Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first) | |
image-mapping: | |
- alpine:3.21,alpine3.21,alpine | |
- debian:bookworm-slim,bookworm-slim,debian-slim | |
- buildpack-deps:bookworm,bookworm,debian | |
- python:3.14-rc-alpine,python3.14-rc-alpine | |
- python:3.13-alpine,python3.13-alpine | |
- python:3.12-alpine,python3.12-alpine | |
- python:3.11-alpine,python3.11-alpine | |
- python:3.10-alpine,python3.10-alpine | |
- python:3.9-alpine,python3.9-alpine | |
- python:3.8-alpine,python3.8-alpine | |
- python:3.14-rc-bookworm,python3.14-rc-bookworm | |
- python:3.13-bookworm,python3.13-bookworm | |
- python:3.12-bookworm,python3.12-bookworm | |
- python:3.11-bookworm,python3.11-bookworm | |
- python:3.10-bookworm,python3.10-bookworm | |
- python:3.9-bookworm,python3.9-bookworm | |
- python:3.8-bookworm,python3.8-bookworm | |
- python:3.14-rc-slim-bookworm,python3.14-rc-bookworm-slim | |
- python:3.13-slim-bookworm,python3.13-bookworm-slim | |
- python:3.12-slim-bookworm,python3.12-bookworm-slim | |
- python:3.11-slim-bookworm,python3.11-bookworm-slim | |
- python:3.10-slim-bookworm,python3.10-bookworm-slim | |
- python:3.9-slim-bookworm,python3.9-bookworm-slim | |
- python:3.8-slim-bookworm,python3.8-bookworm-slim | |
steps: | |
# Login to DockerHub (when not pushing, it's to avoid rate-limiting) | |
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
if: ${{ needs.docker-plan.outputs.login == 'true' }} | |
with: | |
username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} | |
password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} | |
- 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: | | |
set -euo pipefail | |
# Extract the image and tags from the matrix variable | |
IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}" | |
# Generate Dockerfile content | |
cat <<EOF > Dockerfile | |
FROM ${BASE_IMAGE} | |
COPY --from=${{ env.UV_GHCR_IMAGE }}:latest /uv /uvx /usr/local/bin/ | |
ENV UV_TOOL_BIN_DIR="/usr/local/bin" | |
ENTRYPOINT [] | |
CMD ["/usr/local/bin/uv"] | |
EOF | |
# Initialize a variable to store all tag docker metadata patterns | |
TAG_PATTERNS="" | |
# Loop through all base tags and append its docker metadata pattern to the list | |
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version | |
IFS=','; for TAG in ${BASE_TAGS}; do | |
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ needs.docker-plan.outputs.tag }}\n" | |
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ needs.docker-plan.outputs.tag }}\n" | |
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n" | |
done | |
# Remove the trailing newline from the pattern list | |
TAG_PATTERNS="${TAG_PATTERNS%\\n}" | |
# Export tag patterns using the multiline env var syntax | |
{ | |
echo "TAG_PATTERNS<<EOF" | |
echo -e "${TAG_PATTERNS}" | |
echo EOF | |
} >> $GITHUB_ENV | |
- name: Extract metadata (tags, labels) for Docker | |
id: meta | |
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 | |
# ghcr.io prefers index level annotations | |
env: | |
DOCKER_METADATA_ANNOTATIONS_LEVELS: index | |
with: | |
images: | | |
${{ env.UV_GHCR_IMAGE }} | |
${{ env.UV_DOCKERHUB_IMAGE }} | |
flavor: | | |
latest=false | |
tags: | | |
${{ env.TAG_PATTERNS }} | |
- name: Build and push | |
id: build-and-push | |
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0 | |
with: | |
context: . | |
project: 7hd4vdzmw5 # astral-sh/uv | |
platforms: linux/amd64,linux/arm64 | |
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 }} | |
# 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: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} | |
needs: | |
- docker-plan | |
- docker-publish-base | |
- 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 }} | |
# 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: | |
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 ... <IMG>@sha256:<sha256>` | |
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 | |
# 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 }} |