From 798cfb2277f46bff2bc86dd52c97f790fd25193a Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 6 Aug 2026 19:15:02 -0700 Subject: [PATCH 1/6] Add shared scripts to skip AI reviews already completed for a commit --- .../actions/claude-review-toolkit/README.md | 33 +++++++++++++ .../scripts/recordReviewComplete.sh | 36 ++++++++++++++ .../scripts/shouldSkipReview.sh | 47 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100755 .github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh create mode 100755 .github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh diff --git a/.github/actions/claude-review-toolkit/README.md b/.github/actions/claude-review-toolkit/README.md index 0f68cad..05a925a 100644 --- a/.github/actions/claude-review-toolkit/README.md +++ b/.github/actions/claude-review-toolkit/README.md @@ -46,6 +46,39 @@ Caller repos must ship a `.claude/skills/coding-standards/rules/` directory with | `createInlineComment.sh` | ` ` | Posts an inline review comment. Requires `GITHUB_REPOSITORY`, `GH_TOKEN`, and `ALLOWED_RULES_FILE` in env. The body must reference a rule tag matching `[A-Z]+(-[A-Z]+)*-[0-9]+` (e.g. `PERF-1`) that is present in the allowlist; otherwise the comment is rejected. | | `postCodeReviewResults.sh` | `` | Posts the result of a Claude code review. With no violations, adds a `+1` reaction to the PR; with violations, posts one inline comment per violation. Reads the JSON output from env `STRUCTURED_OUTPUT`. Requires `GH_TOKEN`, `GITHUB_REPOSITORY`, `ALLOWED_RULES_FILE`, and `STRUCTURED_OUTPUT` in env. Individual comment failures are swallowed so one rejected comment does not kill the loop. | | `extractAllowedRules.sh` | ` ` | Walks `` for `.md` rule files and writes their `ruleId:` tags to ``. Invoked automatically by the action; rarely called directly. | +| `shouldSkipReview.sh` | ` ` | Resolves the PR's head SHA and writes `head_sha` plus `skip=true\|false` to `$GITHUB_OUTPUT`. `skip` is `true` when a `success` commit status with `` already exists on that SHA. Requires `GH_TOKEN` and `GITHUB_REPOSITORY`. | +| `recordReviewComplete.sh` | ` [DESCRIPTION]` | Sets a `success` commit status with `` on ``, linking back to the workflow run. Requires `GH_TOKEN`, `GITHUB_REPOSITORY`, `statuses: write`. | + +## Skipping duplicate reviews + +A PR marked ready for review after being reviewed as a draft would otherwise be reviewed twice on the same commit. To avoid that, gate the review on `shouldSkipReview.sh` and record completion with `recordReviewComplete.sh`: + +```yaml +permissions: + statuses: write + +steps: + - name: Check for an existing review of this commit + id: skip + env: + GH_TOKEN: ${{ github.token }} + run: shouldSkipReview.sh "$PR_NUMBER" "ai-review/claude" + + - name: Run Claude Code + if: steps.skip.outputs.skip != 'true' + # ... + + - name: Record review completion + if: steps.skip.outputs.skip != 'true' && steps.code-review.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.skip.outputs.head_sha }} + run: recordReviewComplete.sh "$HEAD_SHA" "ai-review/claude" "Reviewed at this commit" +``` + +Record against `steps.skip.outputs.head_sha` — the SHA captured before the review started — rather than re-resolving it at the end. If the author pushed while the review was running, the status lands on the commit that was actually reviewed and the next event correctly triggers a fresh review. + +The status is recorded whether or not the review found anything, so re-running on an unchanged commit never reposts the same findings. A comment trigger (`@claude review`, `/codex-review`) bypasses the gate and is the way to force a re-review. ## Schema extension diff --git a/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh new file mode 100755 index 0000000..bd7ceed --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Record that an AI review completed for a specific commit by setting a commit status. +# A later "ready for review" event reads this status and skips the duplicate review. +# Usage: recordReviewComplete.sh [DESCRIPTION] +# Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID +set -eu + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 [DESCRIPTION]" >&2 + exit 1 +fi + +if ! [[ "$1" =~ ^[0-9a-f]{40}$ ]]; then + echo "Error: HEAD_SHA must be a full 40-character commit SHA" >&2 + exit 1 +fi + +if ! [[ "$2" =~ ^[a-z0-9]([a-z0-9/_-]*[a-z0-9])?$ ]]; then + echo "Error: CONTEXT must be lowercase alphanumeric with '/', '_' or '-' separators" >&2 + exit 1 +fi + +readonly HEAD_SHA="$1" +readonly CONTEXT="$2" +# GitHub rejects status descriptions longer than 140 characters. +readonly DESCRIPTION="${3:-Reviewed at this commit}" +readonly TRUNCATED_DESCRIPTION="${DESCRIPTION:0:140}" +readonly REPO="${GITHUB_REPOSITORY}" +readonly RUN_URL="${GITHUB_SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID}" + +gh api -X POST "/repos/$REPO/statuses/$HEAD_SHA" \ + -f state=success \ + -f context="$CONTEXT" \ + -f description="$TRUNCATED_DESCRIPTION" \ + -f target_url="$RUN_URL" diff --git a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh new file mode 100755 index 0000000..2f9ff0f --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Decide whether an AI review already completed for the PR's current head commit. +# Writes "head_sha=" and "skip=true|false" to $GITHUB_OUTPUT. +# Usage: shouldSkipReview.sh +# Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_OUTPUT +set -eu + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +if ! [[ "$1" =~ ^[0-9]+$ ]]; then + echo "Error: PR_NUMBER must be a positive integer" >&2 + exit 1 +fi + +if ! [[ "$2" =~ ^[a-z0-9]([a-z0-9/_-]*[a-z0-9])?$ ]]; then + echo "Error: CONTEXT must be lowercase alphanumeric with '/', '_' or '-' separators" >&2 + exit 1 +fi + +readonly PR_NUMBER="$1" +readonly CONTEXT="$2" +readonly REPO="${GITHUB_REPOSITORY}" + +HEAD_SHA=$(gh api "/repos/$REPO/pulls/$PR_NUMBER" --jq '.head.sha') +readonly HEAD_SHA + +if [[ -z "$HEAD_SHA" ]]; then + echo "::error::Could not resolve head SHA for PR #$PR_NUMBER" >&2 + exit 1 +fi + +# The combined status endpoint returns only the most recent status per context. +STATE=$(gh api "/repos/$REPO/commits/$HEAD_SHA/status" --jq ".statuses[] | select(.context == \"$CONTEXT\") | .state") +readonly STATE + +echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" + +if [[ "$STATE" == "success" ]]; then + echo "$CONTEXT already completed for $HEAD_SHA, skipping review" >&2 + echo "skip=true" >> "$GITHUB_OUTPUT" +else + echo "skip=false" >> "$GITHUB_OUTPUT" +fi From c94bc3b1e68a5b85094efe476c964d4435cb270b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 6 Aug 2026 19:40:05 -0700 Subject: [PATCH 2/6] Document what the review context argument is for --- .../claude-review-toolkit/scripts/recordReviewComplete.sh | 7 +++++++ .../claude-review-toolkit/scripts/shouldSkipReview.sh | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh index bd7ceed..abc6bf5 100755 --- a/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh +++ b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh @@ -4,6 +4,13 @@ # A later "ready for review" event reads this status and skips the duplicate review. # Usage: recordReviewComplete.sh [DESCRIPTION] # Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID +# +# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review/claude", +# "ai-review/codex"). GitHub allows one status per context per commit, so a second +# status with the same context replaces the first rather than stacking up. It is also +# the key shouldSkipReview.sh looks for, so both scripts must be passed the same value +# or the review will never be recognised as already done. It shows up as the status's +# label in the PR's checks list. set -eu if [[ $# -lt 2 ]]; then diff --git a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh index 2f9ff0f..e7194d7 100755 --- a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh +++ b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh @@ -4,6 +4,10 @@ # Writes "head_sha=" and "skip=true|false" to $GITHUB_OUTPUT. # Usage: shouldSkipReview.sh # Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_OUTPUT +# +# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review/claude", +# "ai-review/codex"). It must match the value recordReviewComplete.sh was given, since +# that is the commit status this looks for. set -eu if [[ $# -lt 2 ]]; then From 096f415ac7bb77cae9189d1061350563def00a99 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 6 Aug 2026 19:42:57 -0700 Subject: [PATCH 3/6] Name the marker status ai-review-completed so it is not read as a review result --- .github/actions/claude-review-toolkit/README.md | 6 ++++-- .../claude-review-toolkit/scripts/recordReviewComplete.sh | 4 ++-- .../claude-review-toolkit/scripts/shouldSkipReview.sh | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/actions/claude-review-toolkit/README.md b/.github/actions/claude-review-toolkit/README.md index 05a925a..ef3ee7a 100644 --- a/.github/actions/claude-review-toolkit/README.md +++ b/.github/actions/claude-review-toolkit/README.md @@ -62,7 +62,7 @@ steps: id: skip env: GH_TOKEN: ${{ github.token }} - run: shouldSkipReview.sh "$PR_NUMBER" "ai-review/claude" + run: shouldSkipReview.sh "$PR_NUMBER" "ai-review-completed/claude" - name: Run Claude Code if: steps.skip.outputs.skip != 'true' @@ -73,13 +73,15 @@ steps: env: GH_TOKEN: ${{ github.token }} HEAD_SHA: ${{ steps.skip.outputs.head_sha }} - run: recordReviewComplete.sh "$HEAD_SHA" "ai-review/claude" "Reviewed at this commit" + run: recordReviewComplete.sh "$HEAD_SHA" "ai-review-completed/claude" "Reviewed at this commit" ``` Record against `steps.skip.outputs.head_sha` — the SHA captured before the review started — rather than re-resolving it at the end. If the author pushed while the review was running, the status lands on the commit that was actually reviewed and the next event correctly triggers a fresh review. The status is recorded whether or not the review found anything, so re-running on an unchanged commit never reposts the same findings. A comment trigger (`@claude review`, `/codex-review`) bypasses the gate and is the way to force a re-review. +Name the context so it does not read as a second review result. The review job itself already appears in the PR's checks list, and a neighbouring green row called `ai-review/claude` looks like a duplicate verdict — `ai-review-completed/claude` reads as the record of a past run, which is what it is. + ## Schema extension Repos that need extra fields on top of the canonical schema should `jq`-merge them in a follow-up step before feeding `claude_args`. Read the canonical schema from `schema_path` (a file) rather than piping `schema_json` through `echo`, so the shell never sees the schema's `"` characters: diff --git a/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh index abc6bf5..657c062 100755 --- a/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh +++ b/.github/actions/claude-review-toolkit/scripts/recordReviewComplete.sh @@ -5,8 +5,8 @@ # Usage: recordReviewComplete.sh [DESCRIPTION] # Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID # -# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review/claude", -# "ai-review/codex"). GitHub allows one status per context per commit, so a second +# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review-completed/claude", +# "ai-review-completed/codex"). GitHub allows one status per context per commit, so a second # status with the same context replaces the first rather than stacking up. It is also # the key shouldSkipReview.sh looks for, so both scripts must be passed the same value # or the review will never be recognised as already done. It shows up as the status's diff --git a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh index e7194d7..7cdc9ef 100755 --- a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh +++ b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh @@ -5,8 +5,8 @@ # Usage: shouldSkipReview.sh # Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_OUTPUT # -# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review/claude", -# "ai-review/codex"). It must match the value recordReviewComplete.sh was given, since +# CONTEXT names the reviewer, one per reviewer per repo (e.g. "ai-review-completed/claude", +# "ai-review-completed/codex"). It must match the value recordReviewComplete.sh was given, since # that is the commit status this looks for. set -eu From f80a5ac9480edb3633861efcb6e5d70464f5a362 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 6 Aug 2026 20:12:06 -0700 Subject: [PATCH 4/6] Explain use of shouldSkipReview script --- .../actions/claude-review-toolkit/scripts/shouldSkipReview.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh index 7cdc9ef..f706c77 100755 --- a/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh +++ b/.github/actions/claude-review-toolkit/scripts/shouldSkipReview.sh @@ -1,6 +1,7 @@ #!/bin/bash -# Decide whether an AI review already completed for the PR's current head commit. +# Decide whether an AI review already completed for the PR's current head commit. Useful when a PR author manually requests a review while the PR is still a draft, +# and then marks it ready for review once the AI review passes. In that case, this skips running the AI review again, since it already completed successfully for the same commit. # Writes "head_sha=" and "skip=true|false" to $GITHUB_OUTPUT. # Usage: shouldSkipReview.sh # Env: GH_TOKEN, GITHUB_REPOSITORY, GITHUB_OUTPUT From d30e9112bdff00130b4cd8a042914562c7fd45b6 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 7 Aug 2026 08:12:16 -0700 Subject: [PATCH 5/6] Add throwaway workflow to test the AI review skip gate --- .github/workflows/testSkipGate.yml | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/testSkipGate.yml diff --git a/.github/workflows/testSkipGate.yml b/.github/workflows/testSkipGate.yml new file mode 100644 index 0000000..3549b67 --- /dev/null +++ b/.github/workflows/testSkipGate.yml @@ -0,0 +1,48 @@ +# Throwaway workflow used to exercise shouldSkipReview.sh and recordReviewComplete.sh on a real PR. +# It stands in for an AI reviewer without calling one. Do not merge this file. +name: Test AI review skip gate + +on: + pull_request: + types: [opened, synchronize, ready_for_review] + +permissions: + contents: read + statuses: write + +jobs: + testSkipGate: + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - name: Checkout + # 4.2.2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Add toolkit scripts to PATH + run: echo "$GITHUB_WORKSPACE/.github/actions/claude-review-toolkit/scripts" >> "$GITHUB_PATH" + + - name: Check for an existing review of this commit + id: skip + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: shouldSkipReview.sh "$PR_NUMBER" "ai-review-completed/test" + + - name: Report the gate decision + env: + ACTION: ${{ github.event.action }} + SKIP: ${{ steps.skip.outputs.skip }} + HEAD_SHA: ${{ steps.skip.outputs.head_sha }} + run: echo "::notice::action=$ACTION skip=$SKIP head_sha=$HEAD_SHA" + + - name: Stand in for the AI review + id: code-review + if: steps.skip.outputs.skip != 'true' + run: echo "The real reviewer would run here" + + - name: Record review completion + if: steps.code-review.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.skip.outputs.head_sha }} + run: recordReviewComplete.sh "$HEAD_SHA" "ai-review-completed/test" "Reviewed at this commit - comment @claude review to re-run" From 1945767fc4415837cacab0f9c8a24d2a40f25a0d Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 7 Aug 2026 08:25:43 -0700 Subject: [PATCH 6/6] Push a new commit to verify the gate reopens on a new SHA