From 8b3787060022d7d056d04c1355e0aaceba00ae9d Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Fri, 23 Oct 2026 21:58:56 -0400 Subject: [PATCH] fix(release-please): serialize reusable job with queued concurrency This serializes the reusable release-please job per caller repo with a concurrency group that queues every run, plus a 30-minute timeout. Overlapping runs race to update the release PR branch and the loser fails with "Error updating ref heads/release-please--branches--main." On 2026-10-24 six Dependabot merges landed in oce-fe-harness within 25 seconds and one run failed this way (oce-fe-harness run 40051491975). The group is repo-wide, not per-ref, because release-please always targets the default branch. Queued runs read the branch's current HEAD, so a run whose commit was overtaken finds nothing to do. The timeout matters because a hung run holds the group. The slowest caller's job takes under five minutes. A caller that declares a concurrency group named release-please-reusable would deadlock with this job, so the README warns against it. PR #39 tried the same group but was closed because GitHub's default keeps one pending run and cancels it when a newer one queues. This shows as a failed check. The `queue: max` setting avoids this but was blocked on actionlint. PR #41 fixed that by moving the shared check to the actionlint fork build v1.7.12-oce.3. Upstream actionlint 1.7.12 still rejects `queue`, so repos linting with it need the fork build. Closes #40. Refs #39. Assisted-by: Claude Code --- .github/workflows/release-please-reusable.yml | 16 ++++++++++++++++ README.md | 2 ++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/release-please-reusable.yml b/.github/workflows/release-please-reusable.yml index a3da862..43d041d 100644 --- a/.github/workflows/release-please-reusable.yml +++ b/.github/workflows/release-please-reusable.yml @@ -116,6 +116,22 @@ jobs: permissions: contents: write pull-requests: write + # Serialize runs within the caller's repo. Unserialized, overlapping runs + # (a burst of merges, or one push GitHub delivers twice) each build a + # release commit, and the slower one fails updating the release branch + # ref. The group is repo-wide, not per-ref, because release-please always + # targets the default branch. `queue: max` keeps every run waiting its + # turn; without it GitHub keeps one pending run and cancels it when a + # newer one queues, which shows as a failed check on that commit. A + # caller that declares a group of this name deadlocks with this job. + concurrency: + group: release-please-reusable + queue: max + cancel-in-progress: false + # Bound a hung run: it holds the concurrency group, so no later run in + # the repo can start until it ends. The slowest caller's job takes just + # under five minutes. + timeout-minutes: 30 # Hoist credentials to job env so step-level `if:` can read them — GitHub # allows the `env` context in step `if:` only when env is defined at the # workflow or job level, and the `secrets` context isn't allowed in `if:` diff --git a/README.md b/README.md index 432770d..67c9220 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ jobs: Treat `body`, `pr`, `prs` and `name` as untrusted text: they come from commit messages and PR titles. Pass them to a script through `env:` — never interpolate them directly into a `run:` block. +The job runs in a repo-wide `release-please-reusable` concurrency group with `queue: max` and without `cancel-in-progress`, so overlapping pushes (a burst of merges, or a single push GitHub delivers twice) wait their turn instead of racing to update the release PR branch. Each queued run reads the branch's current HEAD, so a run that starts after its commit has been overtaken finds nothing left to do. The job times out after 30 minutes so a hung run cannot hold the group indefinitely. Don't declare a concurrency group of the same name in the caller; caller and callee would deadlock on it. + Inputs: | Name | Type | Default | Description |