From 430ef1c58350e33a08327705b5a4cfaf6b563e67 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 28 Aug 2026 12:31:17 -0700 Subject: [PATCH] release: avoid duplicate Docker browser install --- .github/workflows/docker-image-smoke.yml | 128 ++++++++++++++++++ ci/release/docker/Dockerfile | 13 +- .../prepare_continuity_dockerfile_test.go | 19 +++ 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docker-image-smoke.yml diff --git a/.github/workflows/docker-image-smoke.yml b/.github/workflows/docker-image-smoke.yml new file mode 100644 index 0000000000..836bdb3fc2 --- /dev/null +++ b/.github/workflows/docker-image-smoke.yml @@ -0,0 +1,128 @@ +name: Docker image smoke + +on: + pull_request: + paths: + - .github/workflows/docker-image-smoke.yml + - ci/release/docker/Dockerfile + - ci/release/docker/entrypoint.sh + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + smoke: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: amd64 + platform: linux/amd64 + - runner: ubuntu-24.04-arm + arch: arm64 + platform: linux/arm64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + env: + ARCH: ${{ matrix.arch }} + IMAGE: d2:docker-smoke-${{ matrix.arch }} + PLATFORM: ${{ matrix.platform }} + VERSION: v0.0.0-docker-smoke + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: ./go.mod + cache: true + + - name: Build representative release archive + run: ./ci/release/build.sh --rebuild --run="linux/$ARCH" --version="$VERSION" + + - name: Prepare Docker context + run: | + set -euo pipefail + context="$RUNNER_TEMP/docker-context" + mkdir -p "$context" + cp "ci/release/build/$VERSION/d2-$VERSION-linux-$ARCH.tar.gz" "$context/" + cp ci/release/docker/entrypoint.sh "$context/entrypoint.sh" + + - name: Build and measure image + id: image + run: | + set -euo pipefail + builder="d2-docker-smoke-$ARCH" + oci_archive="$RUNNER_TEMP/d2-$ARCH.oci.tar" + context="$RUNNER_TEMP/docker-context" + + docker buildx create --name "$builder" --driver docker-container --use + docker buildx inspect --bootstrap + docker buildx build \ + --platform "$PLATFORM" \ + --provenance=false \ + --output "type=oci,dest=$oci_archive,compression=gzip,compression-level=6,force-compression=true" \ + --file ci/release/docker/Dockerfile \ + "$context" + + manifest_digest=$(tar -xOf "$oci_archive" index.json | jq -er '.manifests[0].digest') + manifest_path="blobs/sha256/${manifest_digest#sha256:}" + compressed_bytes=$(tar -xOf "$oci_archive" "$manifest_path" | \ + jq -er '[.layers[].size] | add') + rm "$oci_archive" + + docker buildx build \ + --platform "$PLATFORM" \ + --provenance=false \ + --load \ + --tag "$IMAGE" \ + --file ci/release/docker/Dockerfile \ + "$context" + uncompressed_bytes=$(docker image inspect --format '{{.Size}}' "$IMAGE") + + printf 'Compressed OCI layers: %s bytes\n' "$compressed_bytes" + printf 'Uncompressed image: %s bytes\n' "$uncompressed_bytes" + echo "compressed_bytes=$compressed_bytes" >>"$GITHUB_OUTPUT" + echo "uncompressed_bytes=$uncompressed_bytes" >>"$GITHUB_OUTPUT" + { + echo "### $PLATFORM" + echo + echo "- Compressed OCI layers: $compressed_bytes bytes" + echo "- Uncompressed image: $uncompressed_bytes bytes" + } >>"$GITHUB_STEP_SUMMARY" + + - name: Smoke-test CLI, SVG, and PNG + run: | + set -euo pipefail + smoke="$RUNNER_TEMP/smoke" + mkdir -p "$smoke" + printf 'x -> y\n' >"$smoke/input.d2" + + version_output=$(docker run --rm --platform "$PLATFORM" "$IMAGE" --version) + version_output=${version_output%$'\r'} + test "$version_output" = "$VERSION" + + docker run --rm --platform "$PLATFORM" \ + -u "$(id -u):$(id -g)" \ + -v "$smoke:/home/debian/src" \ + "$IMAGE" input.d2 output.svg + test -s "$smoke/output.svg" + grep -q '/dev/null 2>&1 || true + docker buildx rm "d2-docker-smoke-$ARCH" >/dev/null 2>&1 || true diff --git a/ci/release/docker/Dockerfile b/ci/release/docker/Dockerfile index e4fb16a07a..f461d9d3b7 100644 --- a/ci/release/docker/Dockerfile +++ b/ci/release/docker/Dockerfile @@ -4,11 +4,18 @@ FROM ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e8 ARG TARGETARCH ARG FIXUID_VERSION=0.6.0 -RUN apt-get update && apt-get install -y ca-certificates curl dumb-init sudo +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl dumb-init sudo \ + && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash -s - && \ - apt-get install -y nodejs -RUN npx playwright@1.61.1 install --with-deps chromium + apt-get install -y --no-install-recommends nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Install Chromium's system libraries without downloading a browser. The +# non-root d2 init-playwright step below installs the single runtime copy. +RUN npx --yes playwright@1.61.1 install-deps chromium \ + && rm -rf /root/.npm /var/lib/apt/lists/* RUN adduser --gecos '' --disabled-password debian \ && echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd diff --git a/ci/release/docker/prepare_continuity_dockerfile_test.go b/ci/release/docker/prepare_continuity_dockerfile_test.go index 4aae376c26..5c5e4f856c 100644 --- a/ci/release/docker/prepare_continuity_dockerfile_test.go +++ b/ci/release/docker/prepare_continuity_dockerfile_test.go @@ -20,6 +20,25 @@ func TestPrepareV082DockerfileMakesPlaywrightNonInteractive(t *testing.T) { } } +func TestReleaseDockerfileInstallsOnePlaywrightBrowser(t *testing.T) { + t.Parallel() + + dockerfile, err := os.ReadFile("Dockerfile") + if err != nil { + t.Fatal(err) + } + source := string(dockerfile) + if strings.Contains(source, "install --with-deps chromium") { + t.Fatal("release Dockerfile downloads a root-owned Playwright browser") + } + if got := strings.Count(source, "install-deps chromium"); got != 1 { + t.Fatalf("release Dockerfile has %d Playwright dependency installs, want 1", got) + } + if got := strings.Count(source, "RUN CI=1 d2 init-playwright"); got != 1 { + t.Fatalf("release Dockerfile has %d runtime Playwright installs, want 1", got) + } +} + func TestPrepareV082DockerfileRejectsUnexpectedSource(t *testing.T) { t.Parallel()