refactor(lisp): split SpecValidator into Parser + execution facade (#899) #1221
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |