From b0b31113f60a172f26f052c37b75092a61d8b70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 12 May 2026 12:05:40 +0200 Subject: [PATCH 1/3] Add claude-review-toolkit composite action Introduces a shared composite action that consolidates the duplicated Claude PR review scaffolding currently maintained in three client repos (Expensify/App, Expensify/Auth, Expensify/Web-Expensify). The action ships: - addPrReaction.sh / removePrReaction.sh - strict-validation reaction helpers (PR_NUMBER must be numeric, REACTION must be one of the eight GitHub-supported values). - createInlineComment.sh - posts an inline PR comment; when ALLOWED_RULES_FILE is set and non-empty, enforces a per-comment rule-ID gate against the allowlist; otherwise skips validation. - extractAllowedRules.sh - walks a rules directory and writes a sorted, deduplicated allowlist of rule IDs. - schemas/code-review-output.json - canonical violations-only schema for the code-inline-reviewer agent output. The action prepends scripts/ to GITHUB_PATH, exposes schema_path and a compacted schema_json output for direct consumption by anthropics/claude-code-action, and (when enforce_allowed_rules=true) extracts the caller's rule allowlist and exports ALLOWED_RULES_FILE for downstream steps. --- .../actions/claude-review-toolkit/action.yml | 34 +++++++++ .../schemas/code-review-output.json | 27 +++++++ .../scripts/addPrReaction.sh | 30 ++++++++ .../scripts/createInlineComment.sh | 72 +++++++++++++++++++ .../scripts/extractAllowedRules.sh | 31 ++++++++ .../scripts/removePrReaction.sh | 40 +++++++++++ 6 files changed, 234 insertions(+) create mode 100644 .github/actions/claude-review-toolkit/action.yml create mode 100644 .github/actions/claude-review-toolkit/schemas/code-review-output.json create mode 100755 .github/actions/claude-review-toolkit/scripts/addPrReaction.sh create mode 100755 .github/actions/claude-review-toolkit/scripts/createInlineComment.sh create mode 100755 .github/actions/claude-review-toolkit/scripts/extractAllowedRules.sh create mode 100755 .github/actions/claude-review-toolkit/scripts/removePrReaction.sh diff --git a/.github/actions/claude-review-toolkit/action.yml b/.github/actions/claude-review-toolkit/action.yml new file mode 100644 index 0000000..1f38b04 --- /dev/null +++ b/.github/actions/claude-review-toolkit/action.yml @@ -0,0 +1,34 @@ +name: 'Claude Review Toolkit' +description: 'Sets up scripts and schema for the Claude PR review pipeline' +inputs: + enforce_allowed_rules: + description: 'When true, walks .claude/skills/coding-standards/rules/ in the caller workspace, writes an allowlist, and exports ALLOWED_RULES_FILE so createInlineComment.sh enforces a per-comment rule-ID gate.' + required: false + default: 'false' +outputs: + schema_path: + description: 'Filesystem path to the canonical schema JSON' + value: ${{ steps.schema.outputs.schema_path }} + schema_json: + description: 'Compacted JSON string of the canonical schema (ready for --json-schema)' + value: ${{ steps.schema.outputs.schema_json }} +runs: + using: 'composite' + steps: + - name: Add scripts to PATH + shell: bash + run: echo "$GITHUB_ACTION_PATH/scripts" >> "$GITHUB_PATH" + - name: Export schema + id: schema + shell: bash + run: | + echo "schema_path=$GITHUB_ACTION_PATH/schemas/code-review-output.json" >> "$GITHUB_OUTPUT" + echo "schema_json=$(jq -c . "$GITHUB_ACTION_PATH/schemas/code-review-output.json")" >> "$GITHUB_OUTPUT" + - name: Extract allowed rules (when enforce_allowed_rules=true) + if: inputs.enforce_allowed_rules == 'true' + shell: bash + run: | + "$GITHUB_ACTION_PATH/scripts/extractAllowedRules.sh" \ + "$GITHUB_WORKSPACE/.claude/skills/coding-standards/rules" \ + "$RUNNER_TEMP/allowed-rules.txt" + echo "ALLOWED_RULES_FILE=$RUNNER_TEMP/allowed-rules.txt" >> "$GITHUB_ENV" diff --git a/.github/actions/claude-review-toolkit/schemas/code-review-output.json b/.github/actions/claude-review-toolkit/schemas/code-review-output.json new file mode 100644 index 0000000..47c57b2 --- /dev/null +++ b/.github/actions/claude-review-toolkit/schemas/code-review-output.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "violations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ruleId": { + "type": "string" + }, + "path": { + "type": "string" + }, + "line": { + "type": "integer" + }, + "body": { + "type": "string" + } + }, + "required": ["ruleId", "path", "line", "body"] + } + } + }, + "required": ["violations"] +} diff --git a/.github/actions/claude-review-toolkit/scripts/addPrReaction.sh b/.github/actions/claude-review-toolkit/scripts/addPrReaction.sh new file mode 100755 index 0000000..23c8ec9 --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/addPrReaction.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Secure proxy script to add a reaction to a GitHub PR or Issue. +# Usage: addPrReaction.sh +# REACTION: +1, -1, laugh, confused, heart, hooray, rocket, eyes +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 + +case "$2" in + +1|-1|laugh|confused|heart|hooray|rocket|eyes) ;; + *) + echo "Error: REACTION must be one of: +1, -1, laugh, confused, heart, hooray, rocket, eyes" >&2 + exit 1 + ;; +esac + +readonly PR_NUMBER="$1" +readonly REACTION="$2" +readonly REPO="${GITHUB_REPOSITORY}" + +gh api -X POST "/repos/$REPO/issues/$PR_NUMBER/reactions" -f content="$REACTION" diff --git a/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh new file mode 100755 index 0000000..9220b0d --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Secure proxy script to create an inline comment on a GitHub PR. +# When ALLOWED_RULES_FILE is set and non-empty, the comment body is validated +# against the allowlist. When unset or empty, validation is skipped. +set -eu + +readonly ALLOWED_RULES_FILE="${ALLOWED_RULES_FILE:-}" + +# Print error and exit. +die() { + echo "Error: $*" >&2 + exit 1 +} + +# Usage helper to avoid repeated text. +usage() { + die "Usage: $0 " +} + +COMMENT_STATUS_REASON="" + +# Ensure the comment body references an allowed rule tag. +validate_rule() { + local body="$1" + local rule + + [[ -f "$ALLOWED_RULES_FILE" ]] || die "Comment rejected: allowed rules file missing at $ALLOWED_RULES_FILE" + + rule=$(echo "$body" | grep -oE '[A-Z]+(-[A-Z]+)*-[0-9]+' | head -1 || true) + [[ -n "$rule" ]] || die "Comment rejected: missing allowed rule reference (e.g. PERF-1)" + + if grep -qF "$rule" "$ALLOWED_RULES_FILE"; then + COMMENT_STATUS_REASON="rule $rule validated" + return 0 + fi + + die "Comment rejected: rule $rule not present in allowed list" +} + +readonly PATH_ARG="${1:-}" +readonly BODY_ARG="${2:-}" +readonly LINE_ARG="${3:-}" + +[[ -z "${PR_NUMBER:-}" ]] && die "Environment variable PR_NUMBER is required" +[[ -z "${GITHUB_REPOSITORY:-}" ]] && die "Environment variable GITHUB_REPOSITORY is required" +[[ -z "$PATH_ARG" || -z "$BODY_ARG" || -z "$LINE_ARG" ]] && usage + +if [[ -n "$ALLOWED_RULES_FILE" ]]; then + validate_rule "$BODY_ARG" + echo "Comment approved: $COMMENT_STATUS_REASON" +else + echo "Comment approved: ALLOWED_RULES_FILE not set, skipping rule validation" +fi + +COMMIT_ID=$(gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha') +readonly COMMIT_ID +readonly SHORT_SHA="${COMMIT_ID:0:7}" + +readonly FOOTER=$'\n\n---\n\n'"Reviewed at: [${SHORT_SHA}](https://github.com/${GITHUB_REPOSITORY}/commit/${COMMIT_ID}) | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency." +readonly COMMENT_BODY="${BODY_ARG}${FOOTER}" + +PAYLOAD=$(jq -n \ + --arg body "$COMMENT_BODY" \ + --arg path "$PATH_ARG" \ + --argjson line "$LINE_ARG" \ + --arg commit_id "$COMMIT_ID" \ + '{body: $body, path: $path, line: $line, side: "RIGHT", commit_id: $commit_id}') +readonly PAYLOAD + +gh api -X POST "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/comments" \ + --input - <<< "$PAYLOAD" || exit 1 diff --git a/.github/actions/claude-review-toolkit/scripts/extractAllowedRules.sh b/.github/actions/claude-review-toolkit/scripts/extractAllowedRules.sh new file mode 100755 index 0000000..5b84f43 --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/extractAllowedRules.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Extract allowed rules from individual rule files in the rules directory. +# Each rule file has YAML frontmatter with a ruleId field. +set -euo pipefail + +RULES_DIR="${1:-.claude/skills/coding-standards/rules}" +OUTPUT_FILE="${2:-.claude/allowed-rules.txt}" + +if [[ ! -d "$RULES_DIR" ]]; then + echo "Error: Rules directory not found: $RULES_DIR" >&2 + exit 1 +fi + +# Extract ruleId from YAML frontmatter of each non-underscore .md file +# Use multi-hyphen regex to validate rule ID format (e.g., PERF-1, CLEAN-REACT-PATTERNS-1) +true > "$OUTPUT_FILE" +for file in "$RULES_DIR"/[!_]*.md; do + [[ -f "$file" ]] || continue + grep -m1 '^ruleId:' "$file" | grep -oE '[A-Z]+(-[A-Z]+)*-[0-9]+' >> "$OUTPUT_FILE" || true +done + +sort -u -o "$OUTPUT_FILE" "$OUTPUT_FILE" + +if [[ ! -s "$OUTPUT_FILE" ]]; then + echo "Error: No allowed rules found in $RULES_DIR" >&2 + exit 1 +fi + +echo "Extracted allowed rules:" +cat "$OUTPUT_FILE" diff --git a/.github/actions/claude-review-toolkit/scripts/removePrReaction.sh b/.github/actions/claude-review-toolkit/scripts/removePrReaction.sh new file mode 100755 index 0000000..e2dc843 --- /dev/null +++ b/.github/actions/claude-review-toolkit/scripts/removePrReaction.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Secure proxy script to remove a reaction from a GitHub PR or Issue. +# Usage: removePrReaction.sh +# REACTION: +1, -1, laugh, confused, heart, hooray, rocket, eyes +set -eu + +if [[ $# -lt 3 ]]; 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 + +case "$2" in + +1|-1|laugh|confused|heart|hooray|rocket|eyes) ;; + *) + echo "Error: REACTION must be one of: +1, -1, laugh, confused, heart, hooray, rocket, eyes" >&2 + exit 1 + ;; +esac + +if [[ -z "$3" ]]; then + echo "Error: USER must be non-empty" >&2 + exit 1 +fi + +readonly PR_NUMBER="$1" +readonly REACTION="$2" +readonly USER="$3" +readonly REPO="${GITHUB_REPOSITORY}" + +ID=$(gh api "/repos/$REPO/issues/$PR_NUMBER/reactions" --jq ".[] | select(.content == \"$REACTION\" and .user.login == \"$USER\") | .id") + +if [[ -n "$ID" ]]; then + gh api --method DELETE "/repos/$REPO/issues/$PR_NUMBER/reactions/$ID" +fi From 6a962cb51594cac315410023c366aacd089cea2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 14 May 2026 02:42:31 +0200 Subject: [PATCH 2/3] Take PR_NUMBER as positional arg in createInlineComment.sh; add README createInlineComment.sh now matches addPrReaction.sh/removePrReaction.sh by accepting PR_NUMBER as $1 instead of reading it from env. Surfaces mismatches at the caller (e.g. a renamed PR_NUMBER_APP env var) instead of silently failing later. Adds README.md documenting the action's inputs, outputs, and the four scripts on PATH. Clarifies that schema extensions (Auth's missingQueryTimings) live in the caller's workflow, not in the toolkit. --- .../actions/claude-review-toolkit/README.md | 68 +++++++++++++++++++ .../scripts/createInlineComment.sh | 13 ++-- 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .github/actions/claude-review-toolkit/README.md diff --git a/.github/actions/claude-review-toolkit/README.md b/.github/actions/claude-review-toolkit/README.md new file mode 100644 index 0000000..e4606c8 --- /dev/null +++ b/.github/actions/claude-review-toolkit/README.md @@ -0,0 +1,68 @@ +# Claude Review Toolkit + +Composite action that ships the shared scaffolding for the Claude PR review pipeline used by `Expensify/App`, `Expensify/Auth`, and `Expensify/Web-Expensify`. + +It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violations-only JSON schema as both a file and a compacted string, and optionally enables a per-comment rule-ID security gate. + +## Usage + +```yaml +- name: Setup Claude review toolkit + id: toolkit + uses: Expensify/GitHub-Actions/.github/actions/claude-review-toolkit@ + with: + enforce_allowed_rules: 'true' # optional; default 'false' + +- name: Run Claude Code + uses: anthropics/claude-code-action@ + with: + claude_args: | + --json-schema '${{ steps.toolkit.outputs.schema_json }}' + +- name: Post inline comment + env: + GH_TOKEN: ${{ github.token }} + run: createInlineComment.sh "${{ github.event.pull_request.number }}" "src/foo.ts" "PERF-1: ..." 42 +``` + +## Inputs + +| Name | Default | Description | +| --- | --- | --- | +| `enforce_allowed_rules` | `false` | When `true`, walks `.claude/skills/coding-standards/rules/` in the caller's workspace, writes a deduplicated allowlist to `$RUNNER_TEMP/allowed-rules.txt`, and exports `ALLOWED_RULES_FILE` so `createInlineComment.sh` enforces a per-comment rule-ID gate. Today only `Expensify/App` opts in; `Auth` and `Web-Expensify` run without the gate. | + +## Outputs + +| Name | Description | +| --- | --- | +| `schema_path` | Absolute filesystem path to `schemas/code-review-output.json` (the canonical violations-only schema). | +| `schema_json` | Same schema compacted as a single-line JSON string, ready to pass to `claude-code-action --json-schema`. Callers that need a repo-specific extension (e.g. Auth's `missingQueryTimings` flag) can `jq`-merge on top of this value in a subsequent step rather than forking the schema. | + +## Side effects + +- Prepends `/scripts` to `GITHUB_PATH`, so the helper scripts below are callable by bare name in later steps. +- When `enforce_allowed_rules: 'true'`, exports `ALLOWED_RULES_FILE` to `$GITHUB_ENV`. + +## Scripts on `PATH` + +| Script | Signature | Notes | +| --- | --- | --- | +| `addPrReaction.sh` | ` ` | Adds a reaction (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) to the PR. | +| `removePrReaction.sh` | ` ` | Removes the matching reaction authored by `` (typically `github-actions[bot]`). Idempotent. | +| `createInlineComment.sh` | ` ` | Posts an inline review comment. Requires `GITHUB_REPOSITORY` and `GH_TOKEN` in env. When `ALLOWED_RULES_FILE` is set and non-empty, 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. When unset, validation is skipped. | +| `extractAllowedRules.sh` | ` ` | Walks `` for `.md` rule files and writes their rule-ID tags to ``. Invoked automatically by the action when `enforce_allowed_rules: 'true'`; rarely called directly. | + +## 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`: + +```yaml +- name: Extend schema + id: schema + run: | + EXTENDED=$(echo "${{ steps.toolkit.outputs.schema_json }}" \ + | jq -c '.properties.missingQueryTimings = {"type":"boolean"} | .required += ["missingQueryTimings"]') + echo "json=$EXTENDED" >> "$GITHUB_OUTPUT" +``` + +Keep extensions narrow - the canonical schema stays the source of truth for the violations array shared across all reviewers. diff --git a/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh index 9220b0d..bd4677f 100755 --- a/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh +++ b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh @@ -15,7 +15,7 @@ die() { # Usage helper to avoid repeated text. usage() { - die "Usage: $0 " + die "Usage: $0 " } COMMENT_STATUS_REASON="" @@ -38,13 +38,14 @@ validate_rule() { die "Comment rejected: rule $rule not present in allowed list" } -readonly PATH_ARG="${1:-}" -readonly BODY_ARG="${2:-}" -readonly LINE_ARG="${3:-}" +readonly PR_NUMBER="${1:-}" +readonly PATH_ARG="${2:-}" +readonly BODY_ARG="${3:-}" +readonly LINE_ARG="${4:-}" -[[ -z "${PR_NUMBER:-}" ]] && die "Environment variable PR_NUMBER is required" +[[ -z "$PR_NUMBER" || -z "$PATH_ARG" || -z "$BODY_ARG" || -z "$LINE_ARG" ]] && usage +[[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || die "PR_NUMBER must be a positive integer" [[ -z "${GITHUB_REPOSITORY:-}" ]] && die "Environment variable GITHUB_REPOSITORY is required" -[[ -z "$PATH_ARG" || -z "$BODY_ARG" || -z "$LINE_ARG" ]] && usage if [[ -n "$ALLOWED_RULES_FILE" ]]; then validate_rule "$BODY_ARG" From 1f41026331db0835363dd9ee8abfa1b03d69dc58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 14 May 2026 02:54:08 +0200 Subject: [PATCH 3/3] Always enforce allowed-rules gate; drop enforce_allowed_rules input The opt-in toggle was leftover from migration. All three consumer repos (App, Auth, Web-Expensify) already ship a .claude/skills/coding-standards/rules/ directory with valid ruleId frontmatter, and their reviewer agents already mandate one Rule ID per inline comment. Making the gate universal removes configuration drift and matches the toolkit's stated purpose of shipping the strictest version of each script. - action.yml: drop inputs.enforce_allowed_rules; always run the extract step - createInlineComment.sh: require ALLOWED_RULES_FILE; remove the skip branch - README: align with the new always-on behavior Caller workflows that previously passed enforce_allowed_rules: 'true' can drop that with: block; callers that did not opt in inherit the gate. --- .github/actions/claude-review-toolkit/README.md | 16 +++++----------- .github/actions/claude-review-toolkit/action.yml | 8 +------- .../scripts/createInlineComment.sh | 13 +++++-------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/actions/claude-review-toolkit/README.md b/.github/actions/claude-review-toolkit/README.md index e4606c8..d1976c9 100644 --- a/.github/actions/claude-review-toolkit/README.md +++ b/.github/actions/claude-review-toolkit/README.md @@ -2,7 +2,7 @@ Composite action that ships the shared scaffolding for the Claude PR review pipeline used by `Expensify/App`, `Expensify/Auth`, and `Expensify/Web-Expensify`. -It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violations-only JSON schema as both a file and a compacted string, and optionally enables a per-comment rule-ID security gate. +It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violations-only JSON schema as both a file and a compacted string, and enforces a per-comment rule-ID security gate on inline comments. ## Usage @@ -10,8 +10,6 @@ It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violat - name: Setup Claude review toolkit id: toolkit uses: Expensify/GitHub-Actions/.github/actions/claude-review-toolkit@ - with: - enforce_allowed_rules: 'true' # optional; default 'false' - name: Run Claude Code uses: anthropics/claude-code-action@ @@ -25,11 +23,7 @@ It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violat run: createInlineComment.sh "${{ github.event.pull_request.number }}" "src/foo.ts" "PERF-1: ..." 42 ``` -## Inputs - -| Name | Default | Description | -| --- | --- | --- | -| `enforce_allowed_rules` | `false` | When `true`, walks `.claude/skills/coding-standards/rules/` in the caller's workspace, writes a deduplicated allowlist to `$RUNNER_TEMP/allowed-rules.txt`, and exports `ALLOWED_RULES_FILE` so `createInlineComment.sh` enforces a per-comment rule-ID gate. Today only `Expensify/App` opts in; `Auth` and `Web-Expensify` run without the gate. | +Caller repos must ship a `.claude/skills/coding-standards/rules/` directory with at least one `.md` rule file whose YAML frontmatter declares a `ruleId:` tag matching `[A-Z]+(-[A-Z]+)*-[0-9]+` (e.g. `PERF-1`, `GEN-01`, `CLEAN-REACT-PATTERNS-0`). The action's extract step builds an allowlist from those tags and fails the workflow if the directory is missing or yields no tags. ## Outputs @@ -41,7 +35,7 @@ It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violat ## Side effects - Prepends `/scripts` to `GITHUB_PATH`, so the helper scripts below are callable by bare name in later steps. -- When `enforce_allowed_rules: 'true'`, exports `ALLOWED_RULES_FILE` to `$GITHUB_ENV`. +- Exports `ALLOWED_RULES_FILE` to `$GITHUB_ENV`, pointing at the deduplicated allowlist extracted from the caller's rules directory. ## Scripts on `PATH` @@ -49,8 +43,8 @@ It places a set of helper scripts on `GITHUB_PATH`, exposes the canonical violat | --- | --- | --- | | `addPrReaction.sh` | ` ` | Adds a reaction (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) to the PR. | | `removePrReaction.sh` | ` ` | Removes the matching reaction authored by `` (typically `github-actions[bot]`). Idempotent. | -| `createInlineComment.sh` | ` ` | Posts an inline review comment. Requires `GITHUB_REPOSITORY` and `GH_TOKEN` in env. When `ALLOWED_RULES_FILE` is set and non-empty, 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. When unset, validation is skipped. | -| `extractAllowedRules.sh` | ` ` | Walks `` for `.md` rule files and writes their rule-ID tags to ``. Invoked automatically by the action when `enforce_allowed_rules: 'true'`; rarely called directly. | +| `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. | +| `extractAllowedRules.sh` | ` ` | Walks `` for `.md` rule files and writes their `ruleId:` tags to ``. Invoked automatically by the action; rarely called directly. | ## Schema extension diff --git a/.github/actions/claude-review-toolkit/action.yml b/.github/actions/claude-review-toolkit/action.yml index 1f38b04..18741c3 100644 --- a/.github/actions/claude-review-toolkit/action.yml +++ b/.github/actions/claude-review-toolkit/action.yml @@ -1,10 +1,5 @@ name: 'Claude Review Toolkit' description: 'Sets up scripts and schema for the Claude PR review pipeline' -inputs: - enforce_allowed_rules: - description: 'When true, walks .claude/skills/coding-standards/rules/ in the caller workspace, writes an allowlist, and exports ALLOWED_RULES_FILE so createInlineComment.sh enforces a per-comment rule-ID gate.' - required: false - default: 'false' outputs: schema_path: description: 'Filesystem path to the canonical schema JSON' @@ -24,8 +19,7 @@ runs: run: | echo "schema_path=$GITHUB_ACTION_PATH/schemas/code-review-output.json" >> "$GITHUB_OUTPUT" echo "schema_json=$(jq -c . "$GITHUB_ACTION_PATH/schemas/code-review-output.json")" >> "$GITHUB_OUTPUT" - - name: Extract allowed rules (when enforce_allowed_rules=true) - if: inputs.enforce_allowed_rules == 'true' + - name: Extract allowed rules shell: bash run: | "$GITHUB_ACTION_PATH/scripts/extractAllowedRules.sh" \ diff --git a/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh index bd4677f..aba950d 100755 --- a/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh +++ b/.github/actions/claude-review-toolkit/scripts/createInlineComment.sh @@ -1,8 +1,8 @@ #!/bin/bash # Secure proxy script to create an inline comment on a GitHub PR. -# When ALLOWED_RULES_FILE is set and non-empty, the comment body is validated -# against the allowlist. When unset or empty, validation is skipped. +# Validates that the comment body references a rule tag present in +# $ALLOWED_RULES_FILE (exported by the toolkit's extract-rules step). set -eu readonly ALLOWED_RULES_FILE="${ALLOWED_RULES_FILE:-}" @@ -46,13 +46,10 @@ readonly LINE_ARG="${4:-}" [[ -z "$PR_NUMBER" || -z "$PATH_ARG" || -z "$BODY_ARG" || -z "$LINE_ARG" ]] && usage [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || die "PR_NUMBER must be a positive integer" [[ -z "${GITHUB_REPOSITORY:-}" ]] && die "Environment variable GITHUB_REPOSITORY is required" +[[ -n "$ALLOWED_RULES_FILE" ]] || die "Environment variable ALLOWED_RULES_FILE is required" -if [[ -n "$ALLOWED_RULES_FILE" ]]; then - validate_rule "$BODY_ARG" - echo "Comment approved: $COMMENT_STATUS_REASON" -else - echo "Comment approved: ALLOWED_RULES_FILE not set, skipping rule validation" -fi +validate_rule "$BODY_ARG" +echo "Comment approved: $COMMENT_STATUS_REASON" COMMIT_ID=$(gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha') readonly COMMIT_ID