diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index ebb021868a..3b91edabab 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -21,7 +21,12 @@ on: types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: - group: claude-review-${{ github.event.pull_request.number }} + # NOTE: this predicate and the review job's `if:` must stay textually + # in sync — GitHub Actions YAML has no anchors to share them. + # Ineligible events (per the review job's opt-in gate) get a unique + # run_id group so they can never cancel an in-flight eligible review — + # workflow-level cancel-in-progress fires before job `if:` is evaluated. + group: claude-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: @@ -35,7 +40,7 @@ jobs: # `ready_for_review` is admitted when the PR still carries the # opt-in label, so a draft opted in via `claude-review` resumes # review when it flips ready even with CLAUDE_ALWAYS_ON unset. - if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review')) }} + if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index 272e609d08..c8941064f6 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -39,7 +39,10 @@ concurrency: # in parallel on the same PR. cancel-in-progress is per-group, so a # synchronize push cancels the in-flight Gemini run without touching # the Claude run (and vice versa). - group: gemini-review-${{ github.event.pull_request.number }} + # NOTE: this predicate and the review job's `if:` must stay textually + # in sync — GitHub Actions YAML has no anchors to share them. + # Ineligible events get a unique run_id group — see claude-review.yml. + group: gemini-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: @@ -53,7 +56,7 @@ jobs: # `_gemini-review.yml`'s authorize `if:`, not in this caller. # `ready_for_review` is admitted when the PR still carries the # opt-in label — mirrors claude-review.yml. - if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review')) }} + if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below