From 9912a7aebd5ce8d0c4542c06e849f023c10dbdd4 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 15 Sep 2026 09:33:57 -0700 Subject: [PATCH] devops(docker): move docker publishing to Azure Pipelines Mirrors microsoft/playwright's .azure-pipelines/publish-docker.yml: the GitHub Actions workflow can no longer log in to ACR via OIDC. - Add .azure-pipelines/publish-docker.yml triggered by v* release tags, with a 'canary' channel for manual runs. - Pull base images through the ACR cache and apt packages through the Azure Ubuntu mirror; forward PIP_INDEX_URL to image builds as a BuildKit secret. - Build arm64 images before amd64, as upstream does. - Only publish stable images for plain X.Y.Z versions. --- .azure-pipelines/publish-docker.yml | 128 +++++++++++++++++++++++++++ .github/workflows/publish_docker.yml | 45 ---------- utils/docker/Dockerfile.jammy | 12 ++- utils/docker/Dockerfile.noble | 12 ++- utils/docker/Dockerfile.resolute | 12 ++- utils/docker/build.sh | 19 +++- utils/docker/publish_docker.sh | 108 ++++++++++++---------- 7 files changed, 234 insertions(+), 102 deletions(-) create mode 100644 .azure-pipelines/publish-docker.yml delete mode 100644 .github/workflows/publish_docker.yml diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml new file mode 100644 index 000000000..c3bba857d --- /dev/null +++ b/.azure-pipelines/publish-docker.yml @@ -0,0 +1,128 @@ +# Trigger: any `v*` release tag (e.g. v1.40.0). +# Can also be queued manually from the ADO UI. +trigger: + tags: + include: + - v* + +pr: none + +parameters: +- name: releaseChannel + displayName: "IMPORTANT: set this to 'canary' when triggering manually" + type: string + default: stable + values: + - stable + - canary + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + pool: + name: DevDivPlaywrightAzurePipelinesUbuntu2204 + os: linux + sdl: + sourceAnalysisPool: + name: DevDivPlaywrightAzurePipelinesWindows2022 + # The image must be windows-based due to restrictions of the SDL tools. See: https://aka.ms/AAo6v8e + os: windows + suppression: + suppressionFile: $(Build.SourcesDirectory)\.azure-pipelines\guardian\SDL\.gdnsuppress + stages: + - stage: Publish + jobs: + - job: PublishDocker + displayName: "Publish Docker images to ACR" + # arm64 images are cross-built under QEMU emulation, which is slow. + timeoutInMinutes: 360 + steps: + # Full history and tags: setuptools-scm derives the package version + # (and thus the image tags) from the release tag. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: "Checkout code" + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.10' + displayName: "Use Python" + + # Resolve pip installs through the DevDiv_PublicPackages feed. The task exports + # an authenticated PIP_INDEX_URL, which build.sh also forwards to the image builds. + - task: PipAuthenticate@1 + inputs: + artifactFeeds: DevDiv/DevDiv_PublicPackages + displayName: "Authenticate pip to DevDiv_PublicPackages feed" + + - task: UseNode@1 + inputs: + version: '24.x' + displayName: "Install Node.js" + + # Relocate the Docker data-root to the large /mnt volume: this job builds + # 6 images (jammy/noble/resolute x amd64/arm64) and `docker system prune`s + # between them, so the default disk fills up. + - task: Bash@3 + displayName: "Setup docker" + inputs: + targetType: "inline" + script: | + set -x + sudo service docker stop + sudo mkdir -p /etc/docker + echo '{ "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json + sudo service docker start + + # scripts/build_driver.py fetches playwright-core with `npm pack`, which + # picks up the registry and credentials from this .npmrc. + - task: Bash@3 + displayName: "setup .npmrc" + inputs: + targetType: "inline" + script: echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc + + - task: npmAuthenticate@0 + displayName: "authenticate the private npm registry" + inputs: + workingFile: .npmrc + + - script: | + python -m pip install --upgrade pip --disable-pip-version-check + pip install -r local-requirements.txt --disable-pip-version-check + pip install -r requirements.txt --disable-pip-version-check + pip install -e . --disable-pip-version-check + displayName: "Install dependencies" + + - task: AzureCLI@2 + displayName: "Login to ACR via OIDC" + inputs: + azureSubscription: "Playwright-CDN" + scriptType: "bash" + scriptLocation: "inlineScript" + inlineScript: "az acr login --name playwright" + + - task: Bash@3 + displayName: "Register QEMU (binfmt) for arm64 cross-build" + inputs: + targetType: "inline" + script: "docker run --rm --privileged ${ACR_CACHE_PREFIX}tonistiigi/binfmt --install arm64" + env: + ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/" + + - task: Bash@3 + displayName: "Build & publish Docker images" + inputs: + targetType: "inline" + script: "./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }}" + env: + ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/" + UBUNTU_MIRROR_PREFIX: "azure." diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml deleted file mode 100644 index 9f90320b5..000000000 --- a/.github/workflows/publish_docker.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: "publish release - Docker" - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - publish-docker-release: - name: "publish to DockerHub" - runs-on: ubuntu-22.04 - if: github.repository == 'microsoft/playwright-python' - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - environment: Docker - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Azure login - uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 - with: - client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }} - - name: Login to ACR via OIDC - run: az acr login --name playwright - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.10" - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - name: Set up Docker QEMU for arm64 docker builds - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - with: - platforms: arm64 - - name: Install dependencies & browsers - run: | - python -m pip install --upgrade pip - pip install -r local-requirements.txt - pip install -r requirements.txt - pip install -e . - - run: ./utils/docker/publish_docker.sh stable diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 7692ad7c5..86b968d59 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -1,15 +1,20 @@ -FROM ubuntu:jammy +ARG ACR_CACHE_PREFIX +FROM ${ACR_CACHE_PREFIX}ubuntu:jammy ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-jammy" +ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 # === INSTALL Python === -RUN apt-get update && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ + -exec sed -i "s|http://archive.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}archive.ubuntu.com|g; s|http://ports.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}ports.ubuntu.com|g" {} + && \ + apt-get update && \ # Install Python apt-get install -y python3 python3-distutils curl && \ update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \ @@ -35,7 +40,8 @@ COPY ./dist/*-manylinux*.whl /tmp/ # Browsers will be downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. -RUN mkdir /ms-playwright && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + mkdir /ms-playwright && \ mkdir /ms-playwright-agent && \ cd /ms-playwright-agent && \ pip install virtualenv && \ diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 2458236a3..fe43b745b 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -1,15 +1,20 @@ -FROM ubuntu:noble +ARG ACR_CACHE_PREFIX +FROM ${ACR_CACHE_PREFIX}ubuntu:noble ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-noble" +ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 # === INSTALL Python === -RUN apt-get update && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ + -exec sed -i "s|http://archive.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}archive.ubuntu.com|g; s|http://ports.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}ports.ubuntu.com|g" {} + && \ + apt-get update && \ # Install Python apt-get install -y python3 curl && \ # Align with upstream Python image and don't be externally managed: @@ -38,7 +43,8 @@ COPY ./dist/*-manylinux*.whl /tmp/ # Browsers will be downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. -RUN mkdir /ms-playwright && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + mkdir /ms-playwright && \ mkdir /ms-playwright-agent && \ cd /ms-playwright-agent && \ pip install virtualenv && \ diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index f6b6cf47c..a8f16c8ed 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -1,15 +1,20 @@ -FROM ubuntu:resolute +ARG ACR_CACHE_PREFIX +FROM ${ACR_CACHE_PREFIX}ubuntu:resolute ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-resolute" +ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 # === INSTALL Python === -RUN apt-get update && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ + -exec sed -i "s|http://archive.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}archive.ubuntu.com|g; s|http://ports.ubuntu.com|http://${UBUNTU_MIRROR_PREFIX}ports.ubuntu.com|g" {} + && \ + apt-get update && \ # Install Python apt-get install -y python3 curl && \ # Align with upstream Python image and don't be externally managed: @@ -38,7 +43,8 @@ COPY ./dist/*-manylinux*.whl /tmp/ # Browsers will be downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. -RUN mkdir /ms-playwright && \ +RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf,required=false \ + mkdir /ms-playwright && \ mkdir /ms-playwright-agent && \ cd /ms-playwright-agent && \ pip install virtualenv && \ diff --git a/utils/docker/build.sh b/utils/docker/build.sh index 3ad2e2b23..0c58b3df6 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -17,6 +17,7 @@ fi function cleanup() { rm -rf "dist/" + rm -f "${PIP_CONF:-}" } trap "cleanup; cd $(pwd -P)" EXIT @@ -40,4 +41,20 @@ else exit 1 fi -docker build --platform "${PLATFORM}" -t "$3" -f "Dockerfile.$2" . +# Let pip inside the image use the same package index as the host. Passed as a +# BuildKit secret, so the (possibly authenticated) URL never lands in an image layer. +SECRET_ARGS=() +if [[ -n "${PIP_INDEX_URL:-}" ]]; then + PIP_CONF="$(mktemp)" + printf '[global]\nindex-url = %s\n' "${PIP_INDEX_URL}" > "${PIP_CONF}" + SECRET_ARGS+=(--secret "id=pipconf,src=${PIP_CONF}") +fi + +# Keep each arch image a plain single-platform manifest without the unknown/unknown platform entry. +export BUILDX_NO_DEFAULT_ATTESTATIONS=1 + +docker build --platform "${PLATFORM}" \ + --build-arg ACR_CACHE_PREFIX="${ACR_CACHE_PREFIX}" \ + --build-arg UBUNTU_MIRROR_PREFIX="${UBUNTU_MIRROR_PREFIX}" \ + "${SECRET_ARGS[@]}" \ + -t "$3" -f "Dockerfile.$2" . diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh index dfdb5e511..62b352b04 100755 --- a/utils/docker/publish_docker.sh +++ b/utils/docker/publish_docker.sh @@ -7,36 +7,58 @@ trap "cd $(pwd -P)" EXIT cd "$(dirname "$0")" MCR_IMAGE_NAME="playwright/python" -PW_VERSION=$(python -c "from playwright._repo_version import version;print(version)") RELEASE_CHANNEL="$1" -if [[ "${RELEASE_CHANNEL}" == "stable" ]]; then - if [[ "${PW_VERSION}" == *post* ]]; then - echo "ERROR: cannot publish stable docker with Playwright version '${PW_VERSION}'" - exit 1 - fi -else - echo "ERROR: unknown release channel - ${RELEASE_CHANNEL}" +if [[ "${RELEASE_CHANNEL}" != "stable" && "${RELEASE_CHANNEL}" != "canary" ]]; then + echo "ERROR: unknown release channel - '${RELEASE_CHANNEL}'" echo "Must be either 'stable' or 'canary'" exit 1 fi +PW_VERSION=$(python -c "from playwright._repo_version import version;print(version)") +if [[ "${RELEASE_CHANNEL}" == "stable" && ! "${PW_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: cannot publish stable docker with Playwright version '${PW_VERSION}'" + exit 1 +fi +VERSION_TAG="v${PW_VERSION}" +if [[ "${RELEASE_CHANNEL}" == "canary" ]]; then + # Drop the "+g" local version segment: "+" is not allowed in docker tags. + VERSION_TAG="v${PW_VERSION%%+*}-canary-$(date -u +'%Y%m%d%H%M%S')" + echo "== CANARY build: publishing to ${VERSION_TAG}-* tags ==" +fi + # Ubuntu 22.04 JAMMY_TAGS=( - "v${PW_VERSION}-jammy" + "${VERSION_TAG}-jammy" ) # Ubuntu 24.04 NOBLE_TAGS=( - "v${PW_VERSION}" - "v${PW_VERSION}-noble" + "${VERSION_TAG}-noble" ) +if [[ "${RELEASE_CHANNEL}" == "stable" ]]; then + NOBLE_TAGS+=("${VERSION_TAG}") +fi # Ubuntu 26.04 RESOLUTE_TAGS=( - "v${PW_VERSION}-resolute" + "${VERSION_TAG}-resolute" ) +tags_for_flavor() { + local FLAVOR="$1" + if [[ "$FLAVOR" == "jammy" ]]; then + echo "${JAMMY_TAGS[@]}" + elif [[ "$FLAVOR" == "noble" ]]; then + echo "${NOBLE_TAGS[@]}" + elif [[ "$FLAVOR" == "resolute" ]]; then + echo "${RESOLUTE_TAGS[@]}" + else + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble', or 'resolute'" >&2 + exit 1 + fi +} + tag_and_push() { local source="$1" local target="$2" @@ -60,25 +82,19 @@ install_oras_if_needed() { return fi local version="1.1.0" - curl -sLO "https://github.com/oras-project/oras/releases/download/v${version}/oras_${version}_linux_amd64.tar.gz" + local arch="amd64" + if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]]; then + arch="arm64" + fi + curl -sLO "https://github.com/oras-project/oras/releases/download/v${version}/oras_${version}_linux_${arch}.tar.gz" mkdir -p oras - tar -zxf oras_${version}_linux_amd64.tar.gz -C oras - rm oras_${version}_linux_amd64.tar.gz + tar -zxf oras_${version}_linux_${arch}.tar.gz -C oras + rm oras_${version}_linux_${arch}.tar.gz } publish_docker_images_with_arch_suffix() { local FLAVOR="$1" - local TAGS=() - if [[ "$FLAVOR" == "jammy" ]]; then - TAGS=("${JAMMY_TAGS[@]}") - elif [[ "$FLAVOR" == "noble" ]]; then - TAGS=("${NOBLE_TAGS[@]}") - elif [[ "$FLAVOR" == "resolute" ]]; then - TAGS=("${RESOLUTE_TAGS[@]}") - else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble', or 'resolute'" - exit 1 - fi + local TAGS=($(tags_for_flavor "$FLAVOR")) local ARCH="$2" if [[ "$ARCH" != "amd64" && "$ARCH" != "arm64" ]]; then echo "ERROR: unknown arch - $ARCH. Must be either 'amd64' or 'arm64'" @@ -96,17 +112,7 @@ publish_docker_images_with_arch_suffix() { publish_docker_manifest () { local FLAVOR="$1" - local TAGS=() - if [[ "$FLAVOR" == "jammy" ]]; then - TAGS=("${JAMMY_TAGS[@]}") - elif [[ "$FLAVOR" == "noble" ]]; then - TAGS=("${NOBLE_TAGS[@]}") - elif [[ "$FLAVOR" == "resolute" ]]; then - TAGS=("${RESOLUTE_TAGS[@]}") - else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble', or 'resolute'" - exit 1 - fi + local TAGS=($(tags_for_flavor "$FLAVOR")) for ((i = 0; i < ${#TAGS[@]}; i++)) do local TAG="${TAGS[$i]}" @@ -123,15 +129,23 @@ publish_docker_manifest () { done } -# Jammy -publish_docker_images_with_arch_suffix jammy amd64 -publish_docker_images_with_arch_suffix jammy arm64 -publish_docker_manifest jammy amd64 arm64 +build_and_push_arch() { + local ARCH="$1" + publish_docker_images_with_arch_suffix jammy "${ARCH}" # Ubuntu 22.04 + publish_docker_images_with_arch_suffix noble "${ARCH}" # Ubuntu 24.04 + publish_docker_images_with_arch_suffix resolute "${ARCH}" # Ubuntu 26.04 +} -publish_docker_images_with_arch_suffix noble amd64 -publish_docker_images_with_arch_suffix noble arm64 -publish_docker_manifest noble amd64 arm64 +publish_manifests() { + publish_docker_manifest jammy amd64 arm64 # Ubuntu 22.04 + publish_docker_manifest noble amd64 arm64 # Ubuntu 24.04 + publish_docker_manifest resolute amd64 arm64 # Ubuntu 26.04 +} -publish_docker_images_with_arch_suffix resolute amd64 -publish_docker_images_with_arch_suffix resolute arm64 -publish_docker_manifest resolute amd64 arm64 +# arm64 first: its QEMU-emulated builds must run while the host is fresh. Running +# them after the native amd64 builds have churned the host triggers a qemu +# segfault in aarch64 ldconfig during libc-bin setup. amd64 is a native build and +# is unaffected by preceding work, so it goes second. +build_and_push_arch arm64 +build_and_push_arch amd64 +publish_manifests