From 634b3f823b997f56e2bf134c97db1309790456fd Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sat, 10 Oct 2026 06:50:07 +0200 Subject: [PATCH] ci: stop image publish legs from cancelling each other (#191) Every main push since the concurrency change cancelled two of the three image builds: the build job's concurrency group named only the ref, so the app, updater and computer legs of one run shared it and cancelled each other, and the publish job never ran. Main has not published an edge image since. The build and publish groups now name the matrix leg. The Playwright report publisher also read the jobs of a run's latest attempt, so a re-run already in progress made it fail on an empty conclusion. It now reads the attempt that finished. Claude-Session: https://claude.ai/code/session_01R8L4XgqjAemFR2remB5hVY Co-authored-by: Claude --- .github/workflows/publish-playwright-report.yml | 4 +++- .github/workflows/publish-server-image.yml | 4 ++-- infra/updater/src/publish-workflow.test.ts | 11 ++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-playwright-report.yml b/.github/workflows/publish-playwright-report.yml index 4d3dc4c..95b0aeb 100644 --- a/.github/workflows/publish-playwright-report.yml +++ b/.github/workflows/publish-playwright-report.yml @@ -44,9 +44,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUN_ID: ${{ github.event.workflow_run.id }} + RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} shell: bash run: | - result="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + # Read the attempt that finished: a re-run already in progress has no conclusion yet. + result="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/attempts/${RUN_ATTEMPT}/jobs?per_page=100" \ --jq '[.jobs[] | select(.name == "Web E2E / Web E2E") | .conclusion] | unique | if length == 1 then .[0] else error("expected exactly one Playwright job") end')" case "$result" in success|failure) ;; diff --git a/.github/workflows/publish-server-image.yml b/.github/workflows/publish-server-image.yml index 0be2989..bded3e7 100644 --- a/.github/workflows/publish-server-image.yml +++ b/.github/workflows/publish-server-image.yml @@ -174,7 +174,7 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 90 concurrency: - group: publish-server-image-build-${{ github.ref }} + group: publish-server-image-build-${{ github.ref }}-${{ matrix.name }}-${{ matrix.arch }} cancel-in-progress: ${{ github.ref == 'refs/heads/main' }} permissions: contents: read @@ -261,7 +261,7 @@ jobs: runs-on: [self-hosted, evoo-ci, Linux] timeout-minutes: 15 concurrency: - group: publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }} + group: publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}-${{ matrix.name }} cancel-in-progress: false permissions: contents: read diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index 779dcd8..4f32905 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -9,6 +9,7 @@ const workflowText = readFileSync( ); interface WorkflowJob { if?: string; + concurrency?: { group?: string }; needs?: string; "runs-on"?: string; strategy?: { matrix?: { arch?: string[]; include?: Array> } }; @@ -106,9 +107,13 @@ describe("server image publish workflow", () => { expect(workflowText).toContain( "cancel-in-progress: ${{ github.event_name == 'pull_request' }}", ); - expect(workflowText).toContain("publish-server-image-build-${{ github.ref }}"); - expect(workflowText).toContain( - "publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}", + // Matrix legs share a job-level concurrency group unless it names the leg, and a shared + // group cancels sibling legs, so no image is published. + expect(build.concurrency?.group).toBe( + "publish-server-image-build-${{ github.ref }}-${{ matrix.name }}-${{ matrix.arch }}", + ); + expect(publish.concurrency?.group).toBe( + "publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}-${{ matrix.name }}", ); expect(workflowText).toContain("push: false"); for (const match of workflowText.matchAll(/uses:\s+([^\s#]+)/g)) {