Skip to content

PR Command Bot

PR Command Bot #158

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} !`
});
}