|
11 | 11 | types: [submitted] |
12 | 12 |
|
13 | 13 | jobs: |
14 | | - claude: |
| 14 | + authorize_claude_actor: |
15 | 15 | if: | |
16 | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || |
17 | 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || |
18 | 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || |
19 | 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) |
20 | 20 | runs-on: ubuntu-latest |
| 21 | + permissions: |
| 22 | + contents: read |
| 23 | + outputs: |
| 24 | + authorized: ${{ steps.permission-gate.outputs.authorized }} |
| 25 | + steps: |
| 26 | + - name: Verify actor can run Claude |
| 27 | + id: permission-gate |
| 28 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 |
| 29 | + with: |
| 30 | + script: | |
| 31 | + const owner = context.repo.owner; |
| 32 | + const repo = context.repo.repo; |
| 33 | + const permissionByLogin = new Map(); |
| 34 | +
|
| 35 | + function claudeRequester() { |
| 36 | + if ( |
| 37 | + context.eventName === 'issue_comment' || |
| 38 | + context.eventName === 'pull_request_review_comment' |
| 39 | + ) { |
| 40 | + return context.payload.comment?.user?.login; |
| 41 | + } |
| 42 | +
|
| 43 | + if (context.eventName === 'pull_request_review') { |
| 44 | + return context.payload.review?.user?.login; |
| 45 | + } |
| 46 | +
|
| 47 | + if (context.eventName === 'issues') { |
| 48 | + return context.payload.issue?.user?.login; |
| 49 | + } |
| 50 | +
|
| 51 | + return null; |
| 52 | + } |
| 53 | +
|
| 54 | + async function hasWriteAccessFor(username) { |
| 55 | + if (!username) { |
| 56 | + return false; |
| 57 | + } |
| 58 | +
|
| 59 | + if (permissionByLogin.has(username)) { |
| 60 | + return permissionByLogin.get(username); |
| 61 | + } |
| 62 | +
|
| 63 | + let hasWriteAccess = false; |
| 64 | + try { |
| 65 | + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ |
| 66 | + owner, |
| 67 | + repo, |
| 68 | + username |
| 69 | + }); |
| 70 | + hasWriteAccess = ['admin', 'write'].includes(permission.permission); |
| 71 | + } catch (error) { |
| 72 | + core.warning( |
| 73 | + `Unable to verify ${username} write permission for Claude; ` + |
| 74 | + `treating the Claude request as untrusted: ${error.message || error}` |
| 75 | + ); |
| 76 | + } |
| 77 | +
|
| 78 | + permissionByLogin.set(username, hasWriteAccess); |
| 79 | + return hasWriteAccess; |
| 80 | + } |
| 81 | +
|
| 82 | + const actor = context.actor; |
| 83 | + const requester = claudeRequester(); |
| 84 | +
|
| 85 | + if (!requester) { |
| 86 | + core.setOutput('authorized', 'false'); |
| 87 | + core.setFailed('Unable to run Claude because the requester could not be determined.'); |
| 88 | + return; |
| 89 | + } |
| 90 | +
|
| 91 | + if (!(await hasWriteAccessFor(actor))) { |
| 92 | + core.setOutput('authorized', 'false'); |
| 93 | + core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`); |
| 94 | + return; |
| 95 | + } |
| 96 | +
|
| 97 | + if (!(await hasWriteAccessFor(requester))) { |
| 98 | + core.setOutput('authorized', 'false'); |
| 99 | + core.setFailed( |
| 100 | + `Unable to run Claude because requester ${requester} does not have write/admin repository permission.` |
| 101 | + ); |
| 102 | + return; |
| 103 | + } |
| 104 | +
|
| 105 | + core.setOutput('authorized', 'true'); |
| 106 | + core.info(`Authorized ${actor} and Claude requester ${requester} to run Claude.`); |
| 107 | +
|
| 108 | + claude: |
| 109 | + needs: authorize_claude_actor |
| 110 | + if: needs.authorize_claude_actor.outputs.authorized == 'true' |
| 111 | + runs-on: ubuntu-latest |
21 | 112 | permissions: |
22 | 113 | contents: read |
23 | 114 | pull-requests: write |
|
0 commit comments