From 24ae0af82b68b7f6d8382ba098b72fb31ae3fa98 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 16:02:02 +0000 Subject: [PATCH] ci: avoid duplicate Docker builds when a labeled PR is merged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes prevent the pre-merge validation build from overlapping with the post-merge publish build: - release-candidate.yml gets a per-PR concurrency group with cancel-in-progress, so pushing new commits to a labeled PR cancels the previous (now-stale) validation build instead of stacking builds. - version-bump.yml, as its first step, cancels any release-candidate validation run still in flight for the merged PR's branch. When a labeled PR is merged immediately, the validation build is redundant — the release.yml run dispatched at the end of version-bump.yml validates and publishes the real (version-bumped, multi-arch) image anyway. Not reusing the PR-built image as the release is deliberate: the release image is built from the chore(release) commit (bumped package.json, so a different baked-in APP_VERSION) and is multi-arch, whereas the validation build is single-arch and pre-bump — the artifacts are not interchangeable. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01ST7p7inT3agkLEp8qbPSC6 --- .github/workflows/release-candidate.yml | 8 ++++++++ .github/workflows/version-bump.yml | 19 +++++++++++++++++++ docs/releasing.md | 9 +++++++++ 3 files changed, 36 insertions(+) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index db4b633..ccd3671 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -11,6 +11,14 @@ on: pull_request: types: [labeled, synchronize] +# One validation build per PR at a time: a new push cancels the previous +# (now-stale) build for the same PR. version-bump.yml additionally cancels +# whatever is still running here the moment the PR is merged, so a labeled +# PR merged immediately doesn't build the image both here and post-merge. +concurrency: + group: release-candidate-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: docker-build: if: contains(github.event.pull_request.labels.*.name, 'release-candidate') diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index da2bcaa..2954ac8 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -42,6 +42,25 @@ jobs: !startsWith(github.event.pull_request.title, 'chore(release):') runs-on: ubuntu-latest steps: + - name: Cancel now-redundant release-candidate build + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + # The pre-merge Docker validation (release-candidate.yml) is moot + # once this PR is merged: the build dispatched at the end of this + # job validates AND publishes the real (version-bumped, multi-arch) + # image. Cancel any validation run still in flight for this branch + # so a labeled PR merged immediately doesn't build the image twice. + ids=$(gh run list --repo "$REPO" --workflow=release-candidate.yml \ + --branch "$HEAD_REF" --json databaseId,status \ + --jq '.[] | select(.status=="in_progress" or .status=="queued") | .databaseId' || true) + for id in $ids; do + echo "Cancelling redundant release-candidate run $id" + gh run cancel "$id" --repo "$REPO" || true + done + - uses: actions/checkout@v4 with: ref: main diff --git a/docs/releasing.md b/docs/releasing.md index bf09064..91f90bd 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -42,6 +42,15 @@ GitHub Actions, gated behind a label so releases are a deliberate choice. the merge itself, and the versioned tags (including `:latest`) follow once `version-bump.yml` tags and dispatches the release build. + **No duplicate builds on merge.** The pre-merge validation build + (`release-candidate.yml`) only builds — it never publishes. If you merge a + labeled PR while that validation build is still running, `version-bump.yml` + cancels it as its first step, since the post-merge `release.yml` run + validates and publishes the real image anyway. The two post-merge + `release.yml` runs (`:edge` from the merge, semver from the tag) share the + GitHub Actions layer cache, so the second is mostly cache hits rather than + a full second build. + 4. **Update the deployment** (e.g. Portainer): - If the stack uses `:latest` (default), re-pull the image and recreate the container ("Re-pull image and redeploy" in Portainer).