diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 0000000000..31321bcf97 --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,90 @@ +name: Code Review + +on: + pull_request: + types: [opened, ready_for_review, synchronize] + +permissions: + contents: read + pull-requests: write + issues: write + id-token: write + +jobs: + code-review: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: anthropics/claude-code-action@v1 + id: claude-review + continue-on-error: true + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + use_sticky_comment: true + prompt: | + Review the changes introduced by this PR. Follow these steps: + + 1. Use `gh pr diff` or read the changed files to understand what changed. + 2. For each changed file, check for: + - Correctness: logic errors, edge cases, off-by-ones + - Security: injection risks, auth bypasses, exposed secrets + - Elixir/Phoenix patterns: proper use of changesets, Ecto queries scoped by project, policy checks via Bodyguard + - React/TypeScript patterns: correct hook usage, prop types, no unnecessary re-renders + - Test coverage: are the changes tested? + 3. Check CLAUDE.md for project-specific conventions. + 4. Post a structured review comment with sections: + - **Summary** of what the PR does + - **Issues** (if any): numbered list with file:line references + - **Suggestions** (optional): non-blocking improvements + - **Verdict**: APPROVE / REQUEST CHANGES / COMMENT + claude_args: --max-turns 50 --model claude-opus-4-7 + + - name: Post status comment if Claude did not comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const { data: comments } = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + const outcome = '${{ steps.claude-review.outcome }}'; + const hasClaudeComment = comments.some(c => + c.body && c.body.includes('Code Review') && c.user.type === 'Bot' + ); + if (!hasClaudeComment) { + const isFailure = outcome === 'failure' || outcome === 'cancelled'; + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: isFailure + ? [ + '## Code Review', + '', + '⚠️ **Automated code review did not complete.**', + '', + 'Claude hit the max-turns limit or encountered an error before posting findings.', + '', + 'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.', + ].join('\n') + : [ + '## Code Review', + '', + '⚠️ The review completed but no findings comment was posted.', + '', + 'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for the raw Claude output.', + ].join('\n'), + }); + } + + - name: Fail the check if review failed + if: steps.claude-review.outcome == 'failure' + run: | + echo "::error::Code review did not complete successfully. See PR comments for details." + exit 1 diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 3c79a3cee0..0ffc017102 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -7,6 +7,7 @@ on: permissions: contents: read pull-requests: write + issues: write id-token: write jobs: @@ -31,40 +32,48 @@ jobs: Read that file first, then follow its instructions exactly. Review only the changes introduced by this PR. Post your findings as a structured review comment. - claude_args: | - --max-turns 50 - --model claude-opus-4-6 + claude_args: --max-turns 50 --model claude-opus-4-7 - - name: Post fallback comment on failure - if: steps.claude-review.outcome == 'failure' + - name: Post status comment if Claude did not comment + if: always() uses: actions/github-script@v7 with: script: | - // Check if there's already a sticky comment from Claude const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); + const outcome = '${{ steps.claude-review.outcome }}'; + // Claude posts its own sticky comment when successful; only intervene if it didn't. const hasClaudeComment = comments.some(c => - c.body && c.body.includes('Security Review') + c.body && c.body.includes('Security Review') && c.user.type === 'Bot' ); if (!hasClaudeComment) { + const isFailure = outcome === 'failure' || outcome === 'cancelled'; await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: [ - '## Security Review', - '', - '⚠️ **Automated security review did not complete.**', - '', - 'Claude hit the max-turns limit or encountered an error before posting findings.', - 'A manual review of S0 (project-scoped data access), S1 (authorization policies),', - 'and S2 (audit trail coverage) is recommended for this PR.', - '', - 'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.', - ].join('\n'), + body: isFailure + ? [ + '## Security Review', + '', + '⚠️ **Automated security review did not complete.**', + '', + 'Claude hit the max-turns limit or encountered an error before posting findings.', + 'A manual review of S0 (project-scoped data access), S1 (authorization policies),', + 'and S2 (audit trail coverage) is recommended for this PR.', + '', + 'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.', + ].join('\n') + : [ + '## Security Review', + '', + '⚠️ The review completed but no findings comment was posted.', + '', + 'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for the raw Claude output.', + ].join('\n'), }); }