From 79f7c96939b71850d8c00464bdbe972a7fa46437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 12 Jun 2026 17:19:19 +0200 Subject: [PATCH] Move Claude workflow concurrency to job level so PR chatter can't cancel queued runs (#21498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem A second `@claude` request on a PR while a Claude run is already in progress gets silently cancelled. Concrete case on #21443: [this comment](https://github.com/twentyhq/twenty/pull/21443#discussion_r3404037004) ("@claude please investigate and report", 14:24:28) produced [a run](https://github.com/twentyhq/twenty/actions/runs/27421867558) that was cancelled 3 seconds after creation, while [the run for an earlier comment](https://github.com/twentyhq/twenty/actions/runs/27421856742) was still in progress. Claude never responded. ## Root cause The `concurrency` block is declared at the **workflow** level, keyed on the PR number. Two GitHub Actions behaviors combine badly here: 1. A concurrency group holds at most one running + **one pending** run; every new run entering the group cancels the previously pending one (`cancel-in-progress: false` only protects the *running* run). 2. Workflow-level concurrency is acquired **before** job-level `if` conditions are evaluated — so every `issue_comment` / `pull_request_review_comment` / `pull_request_review` event on the PR enters the group, even ones with no `@claude` that end up skipped. So while a Claude run is in progress, any PR activity evicts the queued `@claude` run. It's even self-defeating: a review-thread reply fires *two* webhook events (`pull_request_review_comment` + a companion `pull_request_review` with an empty body), so the companion event cancels the queued comment run ~2s later. Claude's own "finished" reply also fires events that kill whatever is queued. ## Fix Move concurrency to the **job** level. A job whose `if` evaluates false is skipped before it ever requests the concurrency slot, so only genuine `@claude` jobs enter the queue. Real Claude runs on the same PR are still serialized (no parallel pushes to the same branch). Also gives `claude-cross-repo` its own group keyed on source repo + issue number — previously a cross-repo dispatch for issue N shared a group with PR N in this repo and they could needlessly queue behind each other. ## Remaining limitation GitHub keeps only one pending job per group: posting three `@claude` requests while the first is still running will still cancel the second when the third arrives. Zero-loss queueing would require a unique group per comment, which would allow concurrent runs pushing to the same PR branch — not worth it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Review in cubic --- .github/workflows/claude.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b92a175227..835e2ec3b7 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -14,10 +14,6 @@ on: permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }} - cancel-in-progress: false - jobs: claude: if: | @@ -45,6 +41,11 @@ jobs: (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association) ) + # Job-level (not workflow-level) concurrency: skipped jobs never enter the + # group, so PR chatter without @claude can't evict a queued Claude run. + concurrency: + group: claude-code-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false runs-on: ubuntu-latest timeout-minutes: 60 permissions: @@ -118,6 +119,9 @@ jobs: claude-cross-repo: if: github.event_name == 'repository_dispatch' + concurrency: + group: claude-cross-repo-${{ github.event.client_payload.repo_full_name }}-${{ github.event.client_payload.issue_number }} + cancel-in-progress: false runs-on: ubuntu-latest timeout-minutes: 60 permissions: