diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b8a16cac69..ed52dfa700 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -183,6 +183,10 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["mirror.gcr.io"] # Deployment tooling reads these labels from the registry to verify an # image's schema expectations against a migrator before deploying it, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdb91e941d..4c2dfb4881 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -224,6 +224,10 @@ jobs: pnpm install --resolution-only --ignore-scripts --ignore-pnpmfile --no-frozen-lockfile echo "TOOLS_EPOCH=$(date -u +%G-W%V)" >> "$GITHUB_ENV" - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["mirror.gcr.io"] - name: Build the immutable cloud image without registry credentials uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: diff --git a/doc/RELEASING.md b/doc/RELEASING.md index 5e0666508a..cc92f17212 100644 --- a/doc/RELEASING.md +++ b/doc/RELEASING.md @@ -304,6 +304,20 @@ suppresses push-triggered workflow runs for those pushes. The release jobs therefore dispatch `docker.yml` explicitly at the new tag ref; the tag mapping keys off `github.ref` either way. +The Docker build matrix and the release preview image builder configure +BuildKit to check `mirror.gcr.io` for Docker Hub images first. This covers +both the Dockerfile frontend and base images. BuildKit keeps Docker Hub as +the fallback when the cache has no usable copy. The image references, +build arguments, GHCR publication, and release gates stay the same. + +This is an availability improvement, not an outage guarantee. Google can +evict cached images, and mutable tags can lag upstream changes or deletions. +Matching manifests at one point does not guarantee later tag freshness or +blob availability. The initial `moby/buildkit` bootstrap still uses the host +Docker daemon and can fail on Docker Hub before this configuration takes +effect. See the [BuildKit mirror configuration](https://docs.docker.com/build/ci/github-actions/configure-builder/#registry-mirror) +and [Google cache limits](https://docs.cloud.google.com/artifact-registry/docs/pull-cached-dockerhub-images). + ## Local Commands ### Preview a canary locally diff --git a/scripts/__tests__/release-verify-workflow.test.mjs b/scripts/__tests__/release-verify-workflow.test.mjs index bef97786dc..10b3dddb7f 100644 --- a/scripts/__tests__/release-verify-workflow.test.mjs +++ b/scripts/__tests__/release-verify-workflow.test.mjs @@ -17,6 +17,45 @@ function readWorkflow(name) { return readFileSync(path.join(repoRoot, ".github/workflows", name), "utf8"); } +test("Docker Hub mirrors apply to both image builders without widening publication access", () => { + const dockerWorkflow = readWorkflow("docker.yml"); + const releaseWorkflow = readWorkflow("release.yml"); + const docker = dockerWorkflow.split(" build-and-push:\n")[1].split("\n merge-and-push:")[0]; + const preview = releaseWorkflow.split(" image_preview:\n")[1].split("\n publish_image_preview:")[0]; + + for (const job of [docker, preview]) { + const steps = job.split(/\n(?= - )/); + const builder = steps.find((step) => /uses: docker\/setup-buildx-action@/.test(step)); + const build = steps.find((step) => /uses: docker\/build-push-action@/.test(step)); + assert.ok(builder && build, "the mirror must configure the builder that runs the image build"); + assert.ok(steps.indexOf(builder) < steps.indexOf(build)); + const config = builder.match(/buildkitd-config-inline: \|\n((?: {12,}[^\n]*(?:\n|$))+)/)?.[1]; + assert.ok(config, "configure the BuildKit container, not only the host Docker daemon"); + assert.deepEqual([...config.matchAll(/^\s*\[registry\."([^"]+)"\]/gm)].map((match) => match[1]), ["docker.io"], + "GHCR publishing and cache traffic must not use the pull mirror"); + const mirrors = config.match(/^\s*mirrors = (\[[^\n]+\])$/m)?.[1]; + assert.ok(mirrors); + assert.deepEqual(JSON.parse(mirrors), ["mirror.gcr.io"]); + assert.doesNotMatch(config, /^\s*(?:http|insecure|ca|keypair)\s*=/m, + "keep verified HTTPS and anonymous public-image pulls"); + assert.doesNotMatch(builder, /driver-opts:|buildkitd-flags:|secrets\./, + "do not change bootstrap images, entitlements, or credentials"); + } + + assert.match(docker, /platform: linux\/amd64/); + assert.match(docker, /platform: linux\/arm64/); + assert.match(docker, /permissions:\n\s+contents: read\n\s+packages: write/); + assert.match(docker, /outputs: type=image,name=ghcr\.io\/\$\{\{ github\.repository \}\},push-by-digest=true,name-canonical=true,push=true/); + assert.match(docker, /cache-to: type=registry,ref=ghcr\.io\/\$\{\{ github\.repository \}\}:buildcache-\$\{\{ matrix\.arch \}\},mode=max/); + assert.match(preview, /permissions:\n\s+contents: read\n\s+steps:/); + assert.match(preview, /platforms: linux\/amd64\n\s+push: false/); + assert.match(preview, /outputs: type=docker,dest=\$\{\{ runner\.temp \}\}\/preview-image\.tar/); + assert.doesNotMatch(preview, /docker\/login-action|secrets\.|packages: write|id-token: write/); + assert.match(releaseWorkflow, /publish_image_preview:\n[\s\S]*?needs: \[plan_preview, image_preview\]/); + assert.doesNotMatch(dockerWorkflow.split(" merge-and-push:\n")[1], /buildkitd-config-inline/, + "the registry-only manifest merge does not need a Docker Hub mirror"); +}); + test("chaos verification isolates callers that verify the same source commit", () => { const chaosWorkflow = readWorkflow("runner-chaos-evals.yml"); const group = chaosWorkflow.match(/^ group: (.+)$/m)?.[1];