From b1b76a846ed494fadd0e743720464aa8440be7c7 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:11:39 +0200 Subject: [PATCH] ci: isolate buildx builders on shared evo Docker hosts (#178) * ci: isolate buildx builders for parallel publish-server-image builds Parallel matrix jobs on shared evo runners raced on ~/.docker/buildx current builder; one job's setup-buildx cleanup then stopped another job's BuildKit during cache export. Use per-job DOCKER_CONFIG, stable builder names, and wire build-push to steps.buildx.outputs.name. Co-authored-by: cynarAI * test: avoid biome noTemplateCurlyInString in publish workflow asserts Co-authored-by: cynarAI --------- Co-authored-by: Cursor Agent Co-authored-by: cynarAI Co-authored-by: Claude --- .github/workflows/publish-server-image.yml | 28 ++++++++++++++++++---- infra/updater/src/publish-workflow.test.ts | 14 +++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-server-image.yml b/.github/workflows/publish-server-image.yml index 96efacf..0be2989 100644 --- a/.github/workflows/publish-server-image.yml +++ b/.github/workflows/publish-server-image.yml @@ -3,8 +3,9 @@ name: publish-server-image # Release tags publish the source-addressed image used by in-app updates plus the human-readable # semver tags. Main publishes `edge`. Pull requests build with a read-only token and an isolated # cache, so untrusted code can validate the images without receiving package-publish authority. -# Self-hosted runners on one host often share ~/.docker; build/publish set DOCKER_CONFIG under -# RUNNER_TEMP so docker/login-action post-logout cannot evict a concurrent job's GHCR session. +# Self-hosted evo runners on one host share a Docker daemon and often ~/.docker; parallel matrix +# jobs must use DOCKER_CONFIG under RUNNER_TEMP plus a per-job buildx builder name wired into +# build-push-action, or setup-buildx --use races and one job's post cleanup stops another's BuildKit. # `ci---` handoff tags are extra aliases on the per-arch digest; GHCR does not # expose a safe tag-only delete API (DELETE /v2/.../manifests/ is unsupported or digest-scoped), # so they are left in place and do not affect edge/sha-* manifests assembled in publish. @@ -129,7 +130,16 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: persist-credentials: false - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - name: Prepare isolated Docker config directory + run: | + set -euo pipefail + cfg="${RUNNER_TEMP}/docker-config" + mkdir -p "$cfg" + echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV" + - id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + with: + name: ci-pr-${{ github.event.pull_request.number }}-${{ matrix.name }} - name: Resolve the lowercase image namespace id: ns # GHCR rejects repository names with capitals; the GitHub repository slug has them. @@ -141,6 +151,7 @@ jobs: tags: type=sha,prefix=sha- - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: + builder: ${{ steps.buildx.outputs.name }} context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} push: false @@ -197,7 +208,10 @@ jobs: cfg="${RUNNER_TEMP}/docker-config" mkdir -p "$cfg" echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV" - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + with: + name: ci-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.name }}-${{ matrix.arch }} - name: Log in to GHCR uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: @@ -217,6 +231,7 @@ jobs: - id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: + builder: ${{ steps.buildx.outputs.name }} context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} platforms: linux/${{ matrix.arch }} @@ -267,7 +282,10 @@ jobs: cfg="${RUNNER_TEMP}/docker-config" mkdir -p "$cfg" echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV" - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + with: + name: ci-${{ github.run_id }}-${{ github.run_attempt }}-publish-${{ matrix.name }} - name: Log in to GHCR uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index cc75541..779dcd8 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -62,6 +62,20 @@ describe("server image publish workflow", () => { expect(workflowText).toContain("ci-${GITHUB_RUN_ID}-"); }); + it("isolates buildx per matrix job on shared self-hosted Docker hosts", () => { + expect(workflowText).toContain('echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"'); + expect(workflowText).toMatch(/builder:\s+\$\{\{\s*steps\.buildx\.outputs\.name\s*\}\}/); + expect(workflowText).toMatch( + /name: ci-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-\$\{\{ matrix\.name \}\}-\$\{\{ matrix\.arch \}\}/, + ); + expect(workflowText).toMatch( + /name: ci-pr-\$\{\{ github\.event\.pull_request\.number \}\}-\$\{\{ matrix\.name \}\}/, + ); + expect(workflowText).toMatch( + /name: ci-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-publish-\$\{\{ matrix\.name \}\}/, + ); + }); + it("isolates Docker registry auth per job and does not delete handoff tags via the registry API", () => { expect(workflowText).toContain('echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"'); expect(workflowText).toContain("Prepare isolated Docker config directory");