feat(ui): add two-factor authentication (TOTP) support #902
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: | |
| # 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" |