Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/claude-review-toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Caller repos must ship a `.claude/skills/coding-standards/rules/` directory with
| `addPrReaction.sh` | `<PR_NUMBER> <REACTION>` | Adds a reaction (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) to the PR. |
| `removePrReaction.sh` | `<PR_NUMBER> <REACTION> <USER>` | Removes the matching reaction authored by `<USER>` (typically `github-actions[bot]`). Idempotent. |
| `createInlineComment.sh` | `<PR_NUMBER> <path> <body> <line>` | 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` | `<PR_NUMBER>` | 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`, and `ALLOWED_RULES_FILE` in env. Individual comment failures are swallowed so one rejected comment does not kill the loop. |
| `extractAllowedRules.sh` | `<rules-dir> <output-file>` | Walks `<rules-dir>` for `.md` rule files and writes their `ruleId:` tags to `<output-file>`. Invoked automatically by the action; rarely called directly. |

## Schema extension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

# Post 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 parsed from $STRUCTURED_OUTPUT.
# Usage: postCodeReviewResults.sh <PR_NUMBER>
# Env: STRUCTURED_OUTPUT (JSON from claude-code-action), GH_TOKEN, GITHUB_REPOSITORY, ALLOWED_RULES_FILE
set -eu

if [[ $# -lt 1 ]]; then
echo "Usage: $0 <PR_NUMBER>" >&2
exit 1
fi

if ! [[ "$1" =~ ^[0-9]+$ ]]; then
echo "Error: PR_NUMBER must be a positive integer" >&2
exit 1
fi

if [[ -z "${STRUCTURED_OUTPUT:-}" ]]; then
echo "::error::Claude Code Action returned empty structured output" >&2
exit 1
fi

readonly PR_NUMBER="$1"

COUNT=$(echo "$STRUCTURED_OUTPUT" | jq '.violations | length')
readonly COUNT

if [[ "$COUNT" -eq 0 ]]; then
addPrReaction.sh "$PR_NUMBER" "+1"
else
echo "$STRUCTURED_OUTPUT" | jq -c '.violations[]' | while IFS= read -r violation; do
PATH_ARG=$(echo "$violation" | jq -r '.path')
BODY_ARG=$(echo "$violation" | jq -r '.body')
LINE_ARG=$(echo "$violation" | jq -r '.line')
createInlineComment.sh "$PR_NUMBER" "$PATH_ARG" "$BODY_ARG" "$LINE_ARG" || true
done
fi
47 changes: 47 additions & 0 deletions .github/actions/run-claude-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Run Claude Code Action

Composite action that pins [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) to a single SHA shared across `Expensify/App`, `Expensify/Auth`, and `Expensify/Web-Expensify`. Bump the pin here once instead of editing every client `claude-review.yml`.

The composite intentionally stays thin: it sets the common defaults (`display_report`, `allowed_non_write_users`) and exposes the upstream `structured_output`. The model, allowed tools, and JSON schema vary per caller and live in `claude_args`. Caller-specific concerns - the `claude-review-toolkit` setup, the eyes-reaction lifecycle, the post-violations loop, and any schema extension - remain in the caller workflow.

## Usage

```yaml
- name: Setup Claude review toolkit
id: toolkit
uses: Expensify/GitHub-Actions/.github/actions/claude-review-toolkit@<sha>

- name: Run Claude Code
id: code-review
uses: Expensify/GitHub-Actions/.github/actions/run-claude-action@<sha>
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
prompt: "/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}"
claude_args: |
--model claude-opus-4-6
--allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*)" --json-schema '${{ steps.toolkit.outputs.schema_json }}'
```

If you omit `--model`, `claude-code-action` uses its own default (currently Sonnet). Pass `--model claude-opus-4-6` (or whichever model you want) explicitly to lock the choice.

## Inputs

| Name | Required | Default | Description |
| --- | --- | --- | --- |
| `anthropic_api_key` | yes | - | Anthropic API key. Pass via a caller secret. |
| `github_token` | yes | - | GitHub token used by `claude-code-action`. |
| `prompt` | yes | - | Prompt passed to Claude. Typically the slash command plus PR context (`REPO:`, `PR_NUMBER:`). |
| `claude_args` | no | `''` | Forwarded verbatim to `claude-code-action.claude_args`. Put `--model`, `--allowedTools`, `--json-schema`, etc. here. Model selection lives caller-side because today's three client repos disagree (App: Opus; Auth/Web: CLI default). |
| `allowed_non_write_users` | no | `*` | Passthrough to `claude-code-action`. |
| `display_report` | no | `true` | Passthrough to `claude-code-action`. |

## Outputs

| Name | Description |
| --- | --- |
| `structured_output` | Passthrough of `claude-code-action.outputs.structured_output`. Callers parse this for the post-violations loop. |

## Bumping the pin

Update the `uses:` line in `action.yml` to the new SHA and version tag comment. Client workflows automatically pick up the new pin on the next merge that floats their `Expensify/GitHub-Actions/...@<sha>` reference forward.
41 changes: 41 additions & 0 deletions .github/actions/run-claude-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: 'Run Claude Code Action'
description: 'Pins anthropics/claude-code-action to a centralised SHA with the defaults used across Expensify client repos.'
inputs:
anthropic_api_key:
description: 'Anthropic API key. Pass via secret from the caller workflow.'
required: true
github_token:
description: 'GitHub token used by claude-code-action for repo interactions.'
required: true
prompt:
description: 'Prompt passed to Claude (typically a slash command plus PR context).'
required: true
claude_args:
description: 'Arguments forwarded to claude-code-action.claude_args (e.g. --model, --allowedTools, --json-schema). Pass --model X if you want a specific model; otherwise the underlying CLI default is used.'
required: false
default: ''
allowed_non_write_users:
description: 'Passthrough to claude-code-action allowed_non_write_users. Defaults to "*".'
required: false
default: '*'
display_report:
description: 'Passthrough to claude-code-action display_report. Defaults to "true".'
required: false
default: 'true'
outputs:
structured_output:
description: 'Passthrough of the structured_output produced by claude-code-action.'
value: ${{ steps.claude.outputs.structured_output }}
runs:
using: 'composite'
steps:
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@ba026a3e56b9f646ae3b1be02dd9c0812aa2f8ae # v1.0.86
with:
anthropic_api_key: ${{ inputs.anthropic_api_key }}
github_token: ${{ inputs.github_token }}
prompt: ${{ inputs.prompt }}
allowed_non_write_users: ${{ inputs.allowed_non_write_users }}
display_report: ${{ inputs.display_report }}
claude_args: ${{ inputs.claude_args }}
Loading