diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 66c505e3f8..fb10c9e268 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,13 +11,17 @@ permissions: contents: read packages: write +# Serialise builds per ref without killing an in-flight one: a newer push +# supersedes only the pending slot, so the image build that is already +# running always finishes and publishes. +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: false + jobs: build-and-push: runs-on: ubuntu-latest timeout-minutes: 60 - concurrency: - group: docker-${{ github.ref }} - cancel-in-progress: true steps: - name: Checkout uses: actions/checkout@v7 diff --git a/server/src/__tests__/cloud-image-bundled-plugins.test.ts b/server/src/__tests__/cloud-image-bundled-plugins.test.ts index ad944fd0cd..98955e0860 100644 --- a/server/src/__tests__/cloud-image-bundled-plugins.test.ts +++ b/server/src/__tests__/cloud-image-bundled-plugins.test.ts @@ -75,4 +75,18 @@ describe("cloud image bundled plugins", () => { // to the self-hosted tags. expect(workflow).toMatch(/^\s*target: production$/m); }); + + it("throttles the docker workflow with cancel-in-progress: false", () => { + // Concurrency is declared at the workflow (top) level so a single group + // spans the whole run, and cancel-in-progress is false so an in-flight + // image build always finishes — a newer push only supersedes the pending + // slot instead of killing the build that is already publishing. + expect(workflow).toMatch(/^concurrency:$/m); + // Pin the per-ref group key: without it the block could keep + // cancel-in-progress: false yet lose the group that scopes serialization + // to a single ref, silently changing which builds queue behind each other. + expect(workflow).toContain("group: docker-${{ github.ref }}"); + expect(workflow).toContain("cancel-in-progress: false"); + expect(workflow).not.toContain("cancel-in-progress: true"); + }); });