Skip to content

refactor(lisp): split SpecValidator into Parser + execution facade (#899) #1221

refactor(lisp): split SpecValidator into Parser + execution facade (#899)

refactor(lisp): split SpecValidator into Parser + execution facade (#899) #1221

name: Claude Code Review
on:
pull_request:
types: [opened, labeled, synchronize]
jobs:
claude-review:
# Run when:
# - PR opened/synced by claude[bot]
# - PR opened/synced from claude/* branch
# - claude-review label is added
# - PR synced and has claude-review label
if: |
github.event.pull_request.changed_files > 0 &&
(
(github.event.action == 'opened' && github.event.pull_request.user.login == 'claude[bot]') ||
(github.event.action == 'synchronize' && github.event.pull_request.user.login == 'claude[bot]') ||
(github.event.action == 'opened' && startsWith(github.event.pull_request.head.ref, 'claude/')) ||
(github.event.action == 'synchronize' && startsWith(github.event.pull_request.head.ref, 'claude/')) ||
(github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
(github.event.action == 'synchronize' && contains(toJSON(github.event.pull_request.labels), 'claude-review'))
)
runs-on: ubuntu-latest
concurrency:
group: claude-automation
cancel-in-progress: false
permissions:
contents: read
pull-requests: write # Need write to add labels
issues: read
id-token: write
env:
MIX_ENV: test
FILE_SIZE_THRESHOLD: 800
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for merge
ref: ${{ github.event.pull_request.head.sha }}
- name: Attempt merge with main
id: merge-check
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Fetch the base branch
git fetch origin ${{ github.event.pull_request.base.ref }}
# Attempt merge (don't commit, just check for conflicts)
if git merge origin/${{ github.event.pull_request.base.ref }} --no-commit --no-ff; then
echo "merge_ok=true" >> $GITHUB_OUTPUT
echo "✅ Merge with ${{ github.event.pull_request.base.ref }} successful"
else
echo "merge_ok=false" >> $GITHUB_OUTPUT
echo "❌ Merge conflict detected"
# Add merge-conflict label
gh pr edit ${{ github.event.pull_request.number }} --add-label "merge-conflict"
# Post a comment explaining the situation
gh pr comment ${{ github.event.pull_request.number }} --body "$(cat <<'EOF'
⚠️ **Merge Conflict Detected**
This PR has conflicts with the base branch and cannot be reviewed until they are resolved.
Please merge or rebase with the base branch and push the changes. The review will run automatically once the conflicts are resolved.
EOF
)"
fi
- name: Skip review if merge conflict
if: steps.merge-check.outputs.merge_ok == 'false'
run: |
echo "::notice::Skipping review due to merge conflict"
exit 0
- name: Remove merge-conflict label if present
if: steps.merge-check.outputs.merge_ok == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Remove stale merge-conflict label from previous failed merges
gh pr edit ${{ github.event.pull_request.number }} \
--remove-label "merge-conflict" 2>/dev/null || true
- name: Setup Elixir environment
if: steps.merge-check.outputs.merge_ok == 'true'
uses: ./.github/actions/setup-elixir
- name: Setup Claude Code
if: steps.merge-check.outputs.merge_ok == 'true'
id: setup-claude
uses: ./.github/actions/setup-claude-code
- name: Check protected files
if: steps.merge-check.outputs.merge_ok == 'true'
id: protected-files
env:
GH_TOKEN: ${{ github.token }}
run: |
# Define protected file patterns
PROTECTED_PATTERNS=(
"docs/specs/"
"docs/guidelines/"
".github/workflows/"
".credo.exs"
".formatter.exs"
)
# Get list of changed files
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only)
PROTECTED_CHANGED=""
for pattern in "${PROTECTED_PATTERNS[@]}"; do
MATCHES=$(echo "$CHANGED_FILES" | grep "^$pattern" || true)
if [ -n "$MATCHES" ]; then
PROTECTED_CHANGED="$PROTECTED_CHANGED$MATCHES"$'\n'
fi
done
if [ -n "$PROTECTED_CHANGED" ]; then
echo "has_protected=true" >> $GITHUB_OUTPUT
echo "Protected files changed:"
echo "$PROTECTED_CHANGED"
# Store list for later
{
echo 'files<<EOF'
echo "$PROTECTED_CHANGED"
echo 'EOF'
} >> $GITHUB_OUTPUT
# Check if spec files specifically changed
if echo "$PROTECTED_CHANGED" | grep -q "docs/specs/"; then
echo "has_spec_changes=true" >> $GITHUB_OUTPUT
else
echo "has_spec_changes=false" >> $GITHUB_OUTPUT
fi
else
echo "has_protected=false" >> $GITHUB_OUTPUT
echo "has_spec_changes=false" >> $GITHUB_OUTPUT
echo "No protected files changed"
fi
- name: Check file sizes
if: steps.merge-check.outputs.merge_ok == 'true'
id: file-sizes
env:
GH_TOKEN: ${{ github.token }}
run: |
threshold=$FILE_SIZE_THRESHOLD
# Get files with additions and their added line counts
# Output format: "added_lines<TAB>filename" for each file with additions
# Using TAB delimiter and substr($0, 7) to handle filenames with spaces
gh pr diff ${{ github.event.pull_request.number }} | \
awk '
/^\+\+\+ b\// {
if (file && added > 0) print added "\t" file
file = substr($0, 7) # Everything after "+++ b/"
added = 0
}
/^\+[^+]/ && file { added++ }
END { if (file && added > 0) print added "\t" file }
' > /tmp/files_with_additions.txt
report="## File Size Analysis\n\n"
found_large=false
while IFS=$'\t' read -r added_lines file; do
# Only check .ex and .exs files that exist
if [[ -f "$file" && "$file" =~ \.(ex|exs)$ ]]; then
total_lines=$(wc -l < "$file" | tr -d ' ')
if [[ $total_lines -gt $threshold ]]; then
report+="- ⚠️ **$file**: $total_lines lines total (+$added_lines in this PR, exceeds $threshold threshold)\n"
found_large=true
fi
fi
done < /tmp/files_with_additions.txt
if [[ "$found_large" == "false" ]]; then
report+="No files with additions exceed $threshold lines.\n"
fi
# Make available to next step via GITHUB_OUTPUT
{
echo 'FILE_SIZE_REPORT<<EOF'
echo -e "$report"
echo 'EOF'
} >> $GITHUB_OUTPUT
- name: Run Claude Code Review
if: steps.merge-check.outputs.merge_ok == 'true'
id: claude-review
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ github.token }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
with:
allowed_bots: "claude"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
path_to_claude_code_executable: ${{ steps.setup-claude.outputs.executable-path }}
prompt: |
# PR Review Task
**Repository**: ${{ github.repository }}
**PR Number**: ${{ github.event.pull_request.number }}
${{ steps.file-sizes.outputs.FILE_SIZE_REPORT }}
${{ steps.protected-files.outputs.has_protected == 'true' && format('## ⚠️ Protected Files Changed
The following protected files were modified:
```
{0}
```
**Review these changes carefully:**
- Spec changes (`docs/specs/`): Is this a legitimate spec update or implementation avoidance?
- Guideline changes: Are these process improvements or shortcuts?
- Workflow changes: Are these automation improvements?
- Linter config changes: Are these necessary or disabling valid checks?
', steps.protected-files.outputs.files) || '' }}
## Instructions
1. Read `docs/guidelines/pr-review-guidelines.md` for complete review guidelines
2. Use `gh pr view ${{ github.event.pull_request.number }}` to see the PR description
3. Use `gh pr diff ${{ github.event.pull_request.number }}` to see changes
4. If the PR references an issue (e.g., "Fixes #N"), read it with comments: `gh issue view N --comments`
5. **Check spec documents**: If the issue references a spec/plan document (e.g., `docs/*-plan.md`),
read it and verify the implementation matches the spec
6. Follow the review structure and severity language from the guidelines
7. Post your review using `gh pr comment ${{ github.event.pull_request.number }} --body "..."`
**CRITICAL**: Check for in-scope completeness - if the PR establishes a pattern,
verify it's applied consistently. Flag incomplete work as "MUST FIX", not "consider".
**CRITICAL**: If the issue references a specification document, verify the implementation
matches the documented structure (file paths, module names, etc.).
**CRITICAL**: Check for code duplication:
- Flag duplicated production code that should be extracted into helpers
- Flag duplicated test code that should use setup blocks or helper functions
- See `docs/guidelines/development-guidelines.md` (DRY section) and `docs/guidelines/testing-guidelines.md` (Avoid Duplication section)
**CRITICAL**: If the File Size Analysis above shows files exceeding the threshold:
- Flag as "Suggestion (Optional)" with recommendation to split into smaller modules
- Only flag if the PR added significant lines to the file (not just minor edits to already-large files)
**CRITICAL - Spec Change Detection (Anti-Cheating)**:
If spec files (`docs/specs/*.md`) are modified:
1. Read the ORIGINAL spec (use `git show origin/main:path/to/spec.md`)
2. Compare with the new version
3. Determine if this is:
- **LEGITIMATE**: Discovered gap/error in spec, necessary clarification
- **AVOIDANCE**: Weakening requirements to match implementation
4. Signs of avoidance:
- Spec change removes or weakens requirements
- Spec change adds exceptions for edge cases
- Implementation doesn't match original spec, but new spec matches implementation
5. If AVOIDANCE detected: Flag as "MUST FIX" - revert spec, fix implementation
6. If LEGITIMATE: Note in review that spec update is justified
**CRITICAL - TODO/Skip Tag Audit**:
Check the diff for any new TODOs, FIXMEs, or skipped tests:
```bash
gh pr diff ${{ github.event.pull_request.number }} | grep -E '^\+.*(TODO|FIXME|@tag :skip)'
```
For each match, verify it has a proper issue reference:
- `TODO(#123)` or `FIXME(#123)` - must have `(#N)` format
- `@tag :skip` - must have `#N` in a comment on same or adjacent line
- `credo:disable` - must have `#N` reference
**Flag as "MUST FIX" if found without issue reference:**
- "TODO without issue reference at line X. Use format: `# TODO(#123): description`"
- "@tag :skip without issue reference. Add comment: `# Skipped: #123 - reason`"
This prevents untracked technical debt from accumulating in the codebase.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
claude_args: '--model claude-opus-4-6 --allowed-tools "Read,Write,Edit,MultiEdit,Glob,Grep,LS,Bash,WebSearch,WebFetch,Task,TodoWrite,TodoRead"'
# Enable full output for debugging
show_full_output: true
- name: Label spec changes
if: steps.merge-check.outputs.merge_ok == 'true' && steps.protected-files.outputs.has_spec_changes == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --add-label "spec-change-detected"
echo "::notice::Spec changes detected - added label for tracking"