Skip to main content
Glama
claude-comment-gate.ymlβ€’11.2 kB
name: Claude Comment Gate (no-checkout) on: issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request_review: types: [submitted] concurrency: group: claude-comment-gate-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} cancel-in-progress: false permissions: contents: read pull-requests: write issues: write actions: write jobs: gate: # Only when someone actually tags @claude on a PR thread/comment/review if: | ( github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@claude') ) || ( github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') ) || ( github.event_name == 'pull_request_review' && github.event.review.body && contains(github.event.review.body, '@claude') ) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: sparse-checkout: | scripts/workflows/claude/ sparse-checkout-cone-mode: false - name: Detect review command id: classify run: | node scripts/workflows/claude/is-review-command.mjs > classify.json cat classify.json is_review=$(jq -r '.isReview // false' classify.json) mode=$(jq -r '.mode // ""' classify.json) rerun=$(jq -r '.rerun // false' classify.json) echo "isReview=$is_review" >> "$GITHUB_OUTPUT" echo "mode=$mode" >> "$GITHUB_OUTPUT" echo "rerun=$rerun" >> "$GITHUB_OUTPUT" env: CLAUDE_TRIGGER_PHRASE: '@claude' - name: Exit if not review if: steps.classify.outputs.isReview != 'true' run: | echo 'Not a review trigger; leaving for command workflow.' - name: Validate CLAUDE_GATE_TOKEN if: steps.classify.outputs.isReview == 'true' run: | if [ -z "${{ secrets.CLAUDE_GATE_TOKEN }}" ]; then echo "::error::CLAUDE_GATE_TOKEN not set (repo β†’ Settings β†’ Secrets and variables β†’ Actions)" exit 1 fi - name: Who am I (should be your PAT user) if: steps.classify.outputs.isReview == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const me = await github.rest.users.getAuthenticated(); core.info(`Using token for @${me.data.login} (type=${me.data.type})`); if (me.data.type === 'Bot') { core.setFailed(`❌ Token is for bot account: ${me.data.login}`); } - name: Extract PR number + intent if: steps.classify.outputs.isReview == 'true' id: info uses: actions/github-script@v7 env: CLAUDE_MODE_HINT: ${{ steps.classify.outputs.mode }} CLAUDE_RERUN_HINT: ${{ steps.classify.outputs.rerun }} with: script: | const prNumber = (context.payload.pull_request?.number) || context.issue.number; if (!prNumber) { core.setFailed('No PR number found'); return; } const raw = (context.payload.comment?.body || context.payload.review?.body || '').toString(); const lc = raw.toLowerCase(); let modeHint = (process.env.CLAUDE_MODE_HINT || '').toLowerCase(); let rerunHint = (process.env.CLAUDE_RERUN_HINT || '').toLowerCase() === 'true'; const tokens = (lc.match(/@claude\s*:?\s*([^\n]*)/)?.[1] || '') .trim() .split(/\s+/) .filter(Boolean); let mode = modeHint === 'ultra' ? 'ultra' : modeHint === 'review' ? 'review' : null; let rerun = rerunHint; for (const token of tokens) { if (!mode && ['ultra', 'review'].includes(token)) mode = token; if (['re-review', 'rereview', 'rerun', 're-run', 'again', 'refresh', 'recheck'].includes(token)) rerun = true; } if (!mode) mode = 'review'; core.setOutput('pr', String(prNumber)); core.setOutput('mode', mode === 'ultra' ? 'ultra' : 'review'); core.setOutput('label', mode === 'ultra' ? 'claude:ultra' : 'claude:review'); core.setOutput('rerun', String(rerun)); - name: Guard - only maintainers may trigger Claude reviews if: steps.classify.outputs.isReview == 'true' id: guard uses: actions/github-script@v7 env: CLAUDE_ALLOWED_USERS: kesslerio with: github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const user = context.actor; const assoc = context.payload.comment?.author_association || context.payload.review?.author_association || 'NONE'; const maintainer = ['OWNER','MEMBER','COLLABORATOR'].includes(assoc); // Optional allowlist (comma-separated), e.g. "kesslerio,othermaintainer" const allow = (process.env.CLAUDE_ALLOWED_USERS || '').split(',').map(s=>s.trim()).filter(Boolean); const allowHit = allow.length === 0 || allow.includes(user); if (!maintainer && !allowHit) { core.notice(`@claude trigger ignored for ${user} (assoc=${assoc}). Only maintainers may trigger reviews.`); core.setOutput('blocked', 'true'); } else { core.setOutput('blocked', 'false'); } - name: Ensure label exists if: steps.classify.outputs.isReview == 'true' && steps.guard.outputs.blocked != 'true' uses: actions/github-script@v7 with: # Use validated PAT to trigger downstream workflows github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const name = '${{ steps.info.outputs.label }}'; const palette = { 'claude:ultra': { color: 'a371f7', description: 'Run deep Opus review' }, 'claude:review': { color: '0e8a16', description: 'Run Sonnet review' }, }; const def = palette[name] || { color: '0366d6', description: 'Claude PR review trigger' }; try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name }); } catch (e) { if (e.status === 404) { await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name, color: def.color, description: def.description }); } else { throw e; } } - name: Add or toggle label (handles re-review) if: steps.classify.outputs.isReview == 'true' && steps.guard.outputs.blocked != 'true' id: relabel uses: actions/github-script@v7 with: # Use validated PAT to trigger downstream workflows github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const issue_number = Number('${{ steps.info.outputs.pr }}'); const requested = '${{ steps.info.outputs.label }}'; const rerun = '${{ steps.info.outputs.rerun }}' === 'true'; // Current labels const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number }); const names = new Set(labels.map(l => l.name)); const hasReview = names.has('claude:review'); const hasUltra = names.has('claude:ultra'); const hasRequested = names.has(requested); async function remove(name) { try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number, name }); } catch (e) { if (e.status !== 404) throw e; } } async function add() { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number, labels: [requested] }); } if (rerun) { if (hasReview) await remove('claude:review'); if (hasUltra) await remove('claude:ultra'); await add(); core.setOutput('action', 're-run'); return; } if (!hasRequested) { if (requested === 'claude:ultra' && hasReview) await remove('claude:review'); if (requested === 'claude:review' && hasUltra) await remove('claude:ultra'); await add(); core.setOutput('action', 'queued'); return; } core.setOutput('action', 'already-queued'); - name: Acknowledge if: steps.classify.outputs.isReview == 'true' && steps.guard.outputs.blocked != 'true' uses: actions/github-script@v7 with: # Use validated PAT to trigger downstream workflows github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const issue_number = Number('${{ steps.info.outputs.pr }}'); const mode = '${{ steps.info.outputs.mode }}'; const action = '${{ steps.relabel.outputs.action }}'; const tag = mode === 'ultra' ? '**Opus (ultra)**' : '**Sonnet**'; const body = action === 're-run' ? `πŸ” Re-queued ${tag} review in PR-safe context. I'll post results here shortly.` : action === 'queued' ? `🟒 Queued ${tag} review in PR-safe context. I'll post results here shortly.` : `ℹ️ ${tag} review is already queued. Push new commits or comment \`@claude re-review\` to force a re-run.`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number, body }); - name: Kick off review (dispatch) if: steps.classify.outputs.isReview == 'true' && steps.guard.outputs.blocked != 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.CLAUDE_GATE_TOKEN }} script: | const owner = context.repo.owner; const repo = context.repo.repo; // Resolve default branch dynamically const { data: r } = await github.rest.repos.get({ owner, repo }); const ref = r.default_branch || 'main'; // Dispatch the workflow on the default branch await github.rest.actions.createWorkflowDispatch({ owner, repo, workflow_id: 'claude-pr-review-labeled.yml', ref, inputs: { pr: String('${{ steps.info.outputs.pr }}'), label: '${{ steps.info.outputs.label }}', source: 'gate' } }); core.info(`Dispatched review workflow for PR #${{ steps.info.outputs.pr }} on ref "${ref}" with label ${{ steps.info.outputs.label }}`);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server