Skip to content

feat(ui): add two-factor authentication (TOTP) support #858

feat(ui): add two-factor authentication (TOTP) support

feat(ui): add two-factor authentication (TOTP) support #858

name: Claude Code Review
on:
# Automatic review only on PR opened (no synchronize — author drives re-review)
# Using pull_request_target because shellhub is public (fork PRs need secrets)
pull_request_target:
types: [opened]
branches: [master]
# Manual review via /review command in comments
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
concurrency:
group: claude-review-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
jobs:
review:
name: Claude Code Review
if: |
(github.event_name == 'pull_request_target' && !github.event.pull_request.draft && github.event.pull_request.user.type != 'Bot') ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/review') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/review') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/review') && github.event.review.user.type != 'Bot')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Generate cross-repo token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.CLOUD_DISPATCH_APP_ID }}
private-key: ${{ secrets.CLOUD_DISPATCH_APP_PRIVATE_KEY }}
owner: shellhub-io
repositories: cloud,claude
# For issue_comment events, github.event.pull_request.head.sha is
# unavailable (the payload only has github.event.issue). Resolve the
# PR head SHA via the REST API so no git checkout is needed yet.
- name: Resolve PR head ref
id: pr-ref
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
if [[ -n "$PR_SHA" ]]; then
echo "sha=$PR_SHA" >> "$GITHUB_OUTPUT"
else
SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/pulls/$PR_NUMBER" | jq -r '.head.sha')
if [[ -z "$SHA" || "$SHA" == "null" ]]; then
echo "::error::Failed to resolve PR head SHA for PR #$PR_NUMBER"
exit 1
fi
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
fi
- name: Checkout shellhub (PR branch)
uses: actions/checkout@v6
with:
ref: ${{ steps.pr-ref.outputs.sha }}
- name: Check /review authorization
id: auth-check
if: github.event_name != 'pull_request_target'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
EVENT_NAME: ${{ github.event_name }}
REVIEW_USER: ${{ github.event.review.user.login }}
COMMENT_USER: ${{ github.event.comment.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
COMMENT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "$EVENT_NAME" == "pull_request_review" ]]; then
USERNAME="$REVIEW_USER"
else
USERNAME="$COMMENT_USER"
fi
HTTP_STATUS=$(gh api orgs/shellhub-io/teams/admin/memberships/"$USERNAME" \
--silent -i 2>/dev/null | head -1 | awk '{print $2}') || true
[[ "$HTTP_STATUS" =~ ^[0-9]+$ ]] || HTTP_STATUS="000"
if [[ "$HTTP_STATUS" == "200" ]]; then
echo "authorized=true" >> "$GITHUB_OUTPUT"
elif [[ "$HTTP_STATUS" == "404" ]]; then
echo "authorized=false" >> "$GITHUB_OUTPUT"
GH_TOKEN="$COMMENT_TOKEN" gh pr comment "$PR_NUMBER" \
--body "@$USERNAME You are not authorized to request an explicit review. If you believe this PR needs a new automated review round, please tag the \`@shellhub-io/admin\` team and a team member can trigger it." || true
else
echo "::warning::Team membership check returned HTTP $HTTP_STATUS for $USERNAME"
echo "authorized=false" >> "$GITHUB_OUTPUT"
fi
- name: Acknowledge /review command
if: github.event_name != 'pull_request_target' && steps.auth-check.outputs.authorized == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REVIEW_ID: ${{ github.event.review.id }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
if [[ "$EVENT_NAME" == "pull_request_review" ]]; then
gh api "repos/$REPO/pulls/$PR_NUMBER/reviews/$REVIEW_ID/reactions" -f content=eyes
elif [[ "$EVENT_NAME" == "pull_request_review_comment" ]]; then
gh api "repos/$REPO/pulls/comments/$COMMENT_ID/reactions" -f content=eyes
elif [[ "$EVENT_NAME" == "issue_comment" ]]; then
gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" -f content=eyes
else
echo "::warning::Unexpected event type for acknowledge: $EVENT_NAME"
fi
- name: Authorization gate
id: gate
if: github.event_name == 'pull_request_target' || steps.auth-check.outputs.authorized == 'true'
run: echo "proceed=true" >> "$GITHUB_OUTPUT"
- name: Clean up previous review comment
if: steps.gate.outputs.proceed == 'true' && github.event_name != 'pull_request_target'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
MARKER="<!-- claude-code-review -->"
gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
--jq ".[] | select(.body | contains(\"$MARKER\")) | .id" \
| while read -r id; do
gh api "repos/$REPO/issues/comments/$id" -X DELETE > /dev/null || true
done
- name: Determine cloud branch
id: cloud-branch
if: steps.gate.outputs.proceed == 'true'
env:
HEAD_REF: ${{ github.head_ref || github.event.pull_request.head.ref }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
if [[ -n "$HEAD_REF" ]]; then
BRANCH="$HEAD_REF"
else
BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName') || {
echo "::warning::Failed to resolve PR head ref name, falling back to master"
true
}
BRANCH="${BRANCH:-master}"
fi
if curl -sf -H "Authorization: Bearer $APP_TOKEN" \
"https://api.github.com/repos/shellhub-io/cloud/branches/$BRANCH" > /dev/null 2>&1; then
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
else
echo "ref=master" >> "$GITHUB_OUTPUT"
fi
- name: Checkout cloud (context)
if: steps.gate.outputs.proceed == 'true'
uses: actions/checkout@v6
with:
repository: shellhub-io/cloud
token: ${{ steps.app-token.outputs.token }}
ref: ${{ steps.cloud-branch.outputs.ref }}
fetch-depth: 1
path: cloud
- name: Checkout claude config
if: steps.gate.outputs.proceed == 'true'
uses: actions/checkout@v6
with:
repository: shellhub-io/claude
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 1
path: claude
- name: Setup workspace context
if: steps.gate.outputs.proceed == 'true'
run: |
"$GITHUB_WORKSPACE/claude/workspace.sh" sync -w "$GITHUB_WORKSPACE" --project shellhub
- name: Check PR author team membership
id: author-check
if: steps.gate.outputs.proceed == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.event.issue.user.login }}
run: |
# Default to non-admin so continue-on-error failures are fail-closed
echo "is_admin=false" >> "$GITHUB_OUTPUT"
HTTP_STATUS=$(gh api orgs/shellhub-io/teams/admin/memberships/"$PR_AUTHOR" \
--silent -i 2>/dev/null | head -1 | awk '{print $2}') || true
[[ "$HTTP_STATUS" =~ ^[0-9]+$ ]] || HTTP_STATUS="000"
if [[ "$HTTP_STATUS" == "200" ]]; then
echo "is_admin=true" >> "$GITHUB_OUTPUT"
elif [[ "$HTTP_STATUS" == "404" ]]; then
echo "is_admin=false" >> "$GITHUB_OUTPUT"
else
echo "::warning::Author team membership check returned HTTP $HTTP_STATUS for $PR_AUTHOR"
echo "is_admin=false" >> "$GITHUB_OUTPUT"
fi
- name: Run Claude Code Review
if: steps.gate.outputs.proceed == 'true'
timeout-minutes: 30
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
track_progress: true
trigger_phrase: "/review"
settings: |
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
prompt: |
You are the lead reviewer for this PR in shellhub-io/shellhub (Community Edition).
The cloud/ (enterprise) repo is checked out at $GITHUB_WORKSPACE/cloud/ for context.
Focus the review on the shellhub diff. Use cloud/ only to understand cross-repo impact.
## Step 1: Check for existing reviews
Fetch existing review comments to avoid duplicating feedback:
gh api repos/shellhub-io/shellhub/pulls/${{ github.event.pull_request.number || github.event.issue.number }}/comments
gh api repos/shellhub-io/shellhub/pulls/${{ github.event.pull_request.number || github.event.issue.number }}/reviews
Update the tracking comment via mcp__github_comment__update_claude_comment:
<!-- claude-code-review -->
## Code Review
- [x] Gathered PR context
- [ ] Reviewing with 5 specialized agents
- [ ] Posting feedback
## Step 2: Spawn 5 reviewer agents IN PARALLEL
Launch exactly 5 Task agents in a SINGLE message (so they run in parallel).
Each agent MUST use model: "opus". Each agent gets:
- The PR diff (pass it in the prompt)
- The list of changed files
- Read access to the full codebase (current directory for shellhub, $GITHUB_WORKSPACE/cloud/ for cloud)
Agent assignments:
1. **Code Quality**: Project conventions (CLAUDE.md), dead code, single responsibility, error handling, commit hygiene
2. **Security**: OWASP Top 10 (injection, XSS, CSRF, SSRF), hardcoded secrets, crypto/rand usage, access control, input validation
3. **Testing**: Missing tests for new/changed behavior, edge cases, test determinism, untested error paths
4. **Go/TypeScript Patterns**: Error wrapping (%w), goroutine leaks, context propagation, minimal interfaces, React patterns (no `any`, no unnecessary re-renders, Zustand)
5. **Architecture & Cross-repo**: If PR changes pkg/, check $GITHUB_WORKSPACE/cloud/ for impact. API contract changes, interface compatibility, breaking changes.
Each agent must return findings as a structured list:
- file_path: exact path relative to repo root
- line_start and line_end: exact line numbers in the NEW file
- severity: critical | high | medium | low
- description: what's wrong and why
- suggestion: corrected code (if applicable), or empty string
General instructions for ALL agents:
- Be exhaustive. Report every issue you find regardless of severity. Do not
stop after finding the most obvious problems.
- Do at least two passes: first for structural/logic issues, then for edge
cases and defensive coding.
- For each suggestion you make, verify it is correct: check that all
referenced variables exist, all event fields are available for every
trigger type, token permissions are sufficient, and your fix does not
introduce new issues.
- Check consistency: if you find an issue in one place, scan for the same
pattern elsewhere in the diff and report all occurrences.
- Consider all GitHub Actions trigger types this workflow handles
(pull_request_target, issue_comment, pull_request_review,
pull_request_review_comment) and verify each code path works for all of
them.
Agents must NOT post any GitHub comments. They only return findings.
After all agents complete, update the tracking comment via mcp__github_comment__update_claude_comment:
<!-- claude-code-review -->
## Code Review
- [x] Gathered PR context
- [x] Code review complete
- [ ] Posting feedback
## Step 3: Aggregate and deduplicate
After all 5 agents complete, collect their findings. Then:
1. Remove duplicate findings (same file + same line range + same issue)
2. If the same pattern repeats across multiple locations, keep only the first occurrence and note "Same issue also at: file:line, file:line, ..."
3. Compare against the existing review comments fetched in Step 1. Skip any finding already reported in a previous review thread on the same file and line.
## Step 3.5: Cross-validate findings
Before posting, do a final review pass:
1. For each finding with a suggestion, verify the suggested fix is correct
and complete — does it handle all trigger types? Does it use the right
token? Does it introduce any new issues?
2. For each finding, check if the same pattern exists elsewhere in the diff
that wasn't reported. If so, add those locations.
3. Read the full diff one more time specifically looking for: missing error
handling, missing fallbacks for optional event fields, inconsistent
patterns between similar code blocks, and defensive coding gaps
(continue-on-error, || true, fail-closed defaults).
## Step 4: Post inline comments
For each remaining finding, post an inline comment using
mcp__github_inline_comment__create_inline_comment on the specific file and line.
When a fix is available, include a GitHub suggestion block:
```suggestion
corrected code here
```
Do NOT re-report issues that already exist in previous review threads.
After posting all inline comments, update the tracking comment via mcp__github_comment__update_claude_comment:
<!-- claude-code-review -->
## Code Review
- [x] Gathered PR context
- [x] Code review complete
- [x] Posted inline comments
- [ ] Finalizing summary
## Step 5: Update final summary
After Step 4 is complete, update the tracking comment one final time via
mcp__github_comment__update_claude_comment to become the closing summary.
When mentioning `/review` in any comment, always wrap it in backticks (`` `/review` ``).
The PR author ${{ steps.author-check.outputs.is_admin == 'true' && 'IS' || 'is NOT' }} a member of the shellhub-io/admin team.
### If there are findings:
Update the tracking comment to:
<!-- claude-code-review -->
## Code Review Complete
Reviewed N files. **X inline issues** posted (breakdown by severity).
### Additional notes
(Only if there are non-inlinable findings — architectural concerns,
missing files, cross-cutting issues not tied to specific lines.
Omit this section entirely if all findings were posted inline.)
---
{For admin authors:}
To request another review round, comment `/review`.
{For non-admin authors:}
If you've addressed the feedback and want a new review, tag
`@shellhub-io/admin` and a team member can trigger it.
### If there are NO findings:
Update the tracking comment to:
<!-- claude-code-review -->
## Code Review Complete
Reviewed N files across code quality, security, testing, language patterns,
and architecture — no issues found. The code looks good as-is.
{For non-admin authors only:}
If you push additional changes and want a new review, tag
`@shellhub-io/admin` and a team member can trigger it.
Do NOT mention inline comments or `/review` follow-up when there are no findings
(unless the author is non-admin, in which case include the tag guidance).
In both cases, always update the tracking comment. Never skip Step 5.
claude_args: |
--max-turns 50
--model claude-opus-4-6
--allowedTools "mcp__github_inline_comment__create_inline_comment"