PR Command Bot #158
This file contains 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: PR Command Bot | |
on: | |
issue_comment: | |
types: [created] | |
pull_request_review_comment: | |
types: [created] | |
pull_request: | |
types: [opened, closed, reopened, edited] | |
permissions: | |
issues: write | |
pull-requests: write | |
jobs: | |
process-command: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Process PR Commands | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const eventName = context.eventName; | |
let comment, commenter, action, prNumber; | |
if (eventName === 'issue_comment') { | |
if (!context.payload.issue.pull_request) { | |
return; | |
} | |
comment = context.payload.comment?.body?.trim().toLowerCase(); | |
commenter = context.payload.comment?.user?.login; | |
prNumber = context.payload.issue.number; | |
action = context.payload.action; | |
} else if (eventName === 'pull_request_review_comment') { | |
comment = context.payload.comment?.body?.trim().toLowerCase(); | |
commenter = context.payload.comment?.user?.login; | |
prNumber = context.payload.pull_request.number; | |
action = context.payload.action; | |
} else if (eventName === 'pull_request') { | |
prNumber = context.payload.pull_request.number; | |
action = context.payload.action; | |
} | |
const repoOwner = context.repo.owner; | |
const repoName = context.repo.repo; | |
let prAuthor; | |
if (eventName === 'issue_comment') { | |
const { data: pr } = await github.rest.pulls.get({ | |
owner: repoOwner, | |
repo: repoName, | |
pull_number: prNumber | |
}); | |
prAuthor = pr.user.login; | |
} else if (eventName === 'pull_request' || eventName === 'pull_request_review_comment') { | |
prAuthor = context.payload.pull_request.user.login; | |
} | |
async function isMaintainer(username) { | |
try { | |
const { data: collaborators } = await github.rest.repos.listCollaborators({ | |
owner: repoOwner, | |
repo: repoName, | |
affiliation: 'direct' | |
}); | |
return collaborators.some(collab => | |
collab.login === username && | |
['admin', 'write'].includes(collab.permissions?.pull) | |
); | |
} catch (error) { | |
return false; | |
} | |
} | |
async function handleLabel(labelName, color, description) { | |
try { | |
await github.rest.issues.addLabels({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
labels: [labelName] | |
}); | |
} catch (error) { | |
if (error.status === 404) { | |
await github.rest.issues.createLabel({ | |
owner: repoOwner, | |
repo: repoName, | |
name: labelName, | |
color: color || "0e8a16", | |
description: description || "" | |
}); | |
await github.rest.issues.addLabels({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
labels: [labelName] | |
}); | |
} else { | |
throw error; | |
} | |
} | |
} | |
async function requestReviews(reviewers) { | |
const validReviewers = reviewers | |
.map(r => r.replace(/^@/, '').trim()) | |
.filter(r => r); | |
if (validReviewers.length === 0) { | |
throw new Error('No valid reviewers specified'); | |
} | |
await github.rest.pulls.requestReviewers({ | |
owner: repoOwner, | |
repo: repoName, | |
pull_number: prNumber, | |
reviewers: validReviewers | |
}); | |
return validReviewers; | |
} | |
const isPR = !!context.payload.pull_request || !!context.payload.issue?.pull_request; | |
try { | |
if (comment?.includes("/cc") && action === "created") { | |
try { | |
const reviewers = comment.substring(3).trim().split(/[\s,]+/); | |
await requestReviews(reviewers); | |
} catch (error) { | |
await github.rest.issues.createComment({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
body: `❌ ${error.message} !` | |
}); | |
} | |
} | |
if (comment?.includes("/lgtm")) { | |
try { | |
if (commenter === prAuthor) { | |
throw new Error("You cannot LGTM your own pull request "); | |
} | |
await handleLabel("LGTM", "0e8a16", "Looks Good To Me"); | |
} catch (error) { | |
await github.rest.issues.createComment({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
body: `❌ ${error.message} !` | |
}); | |
} | |
} | |
if (comment?.includes("/assign") && isPR && action === "created") { | |
await github.rest.pulls.requestReviewers({ | |
owner: repoOwner, | |
repo: repoName, | |
pull_number: prNumber, | |
reviewers: [commenter] | |
}); | |
} | |
if (comment?.includes("/close") && action === "created") { | |
await github.rest.pulls.update({ | |
owner: repoOwner, | |
repo: repoName, | |
pull_number: prNumber, | |
state: "closed" | |
}); | |
} | |
if (comment?.includes("/reopen") && action === "created") { | |
await github.rest.pulls.update({ | |
owner: repoOwner, | |
repo: repoName, | |
pull_number: prNumber, | |
state: "open" | |
}); | |
} | |
if (comment?.includes("/label") && action === "created") { | |
const labelName = comment.split("/label")[1]?.trim(); | |
if (labelName) { | |
await handleLabel(labelName, "0e8a16"); | |
} | |
} | |
if ((action === "opened" || action === "edited") && eventName === 'pull_request') { | |
const description = context.payload.pull_request?.body?.trim() || ""; | |
const kindMatch = description.match(/\/kind\s*:\s*(\S+)|\/kind\s+(\S+)/i); | |
if (kindMatch) { | |
const kindValue = (kindMatch[1] || kindMatch[2]).toLowerCase(); | |
const kindLabel = `kind/${kindValue}`; | |
try { | |
await handleLabel(kindLabel, "696969", `PR type: ${kindValue}`); | |
} catch (error) { | |
await github.rest.issues.createComment({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
body: `❌ Failed to add kind label: ${kindLabel}. ${error.message} !` | |
}); | |
} | |
} | |
} | |
} catch (error) { | |
await github.rest.issues.createComment({ | |
owner: repoOwner, | |
repo: repoName, | |
issue_number: prNumber, | |
body: `❌ ${error.message} !` | |
}); | |
} |