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 <cynarAI@users.noreply.github.com>

* test: avoid biome noTemplateCurlyInString in publish workflow asserts

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-09 10:11:39 +02:00 committed by GitHub
parent f07d17e927
commit b1b76a846e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 5 deletions

View File

@ -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-<run>-<image>-<arch>` handoff tags are extra aliases on the per-arch digest; GHCR does not
# expose a safe tag-only delete API (DELETE /v2/.../manifests/<tag> 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:

View File

@ -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");