Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/converters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function extractUsersAndTeams(orgName, reviewers) {
const separator = reviewers.includes(",") ? "," : " ";
const split = reviewers.split(separator);

return {
teams: split.filter((reviewer) => reviewer.includes("/")),
users: split.filter((reviewer) => !reviewer.includes("/")),
};
}
47 changes: 47 additions & 0 deletions app/converters.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { extractUsersAndTeams } from "./converters.js";

describe("converters", () => {
describe("extractUsersAndTeams", () => {
test("single user", () => {
const converted = extractUsersAndTeams("test-org", "@reviewer1");

expect(converted).toEqual({
users: ["@reviewer1"],
teams: [],
});
});
test("user and team", () => {
const converted = extractUsersAndTeams(
"test-org",
"@reviewer1,@test-org/team-1"
);

expect(converted).toEqual({
users: ["@reviewer1"],
teams: ["@test-org/team-1"],
});
});
test("multiple users and teams", () => {
const converted = extractUsersAndTeams(
"test-org",
"@reviewer1,@test-org/team-1,@reviewer2,@test-org/team-2"
);

expect(converted).toEqual({
users: ["@reviewer1", "@reviewer2"],
teams: ["@test-org/team-1", "@test-org/team-2"],
});
});
test("space separated users and teams", () => {
const converted = extractUsersAndTeams(
"test-org",
"@reviewer1 @test-org/team-1 @reviewer2 @test-org/team-2"
);

expect(converted).toEqual({
users: ["@reviewer1", "@reviewer2"],
teams: ["@test-org/team-1", "@test-org/team-2"],
});
});
});
});
23 changes: 23 additions & 0 deletions app/matchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function transferMatcher(text) {
return text.match(/\/transfer ([a-zA-Z\d-]+)/);
}

export function closeMatcher(text) {
return text.match(/\/close (not-planned)|\/close/);
}

export function reopenMatcher(text) {
return text.match(/\/reopen/);
}

export function labelMatcher(text) {
return text.match(/\/label ([a-zA-Z\d-, ]+)/);
}

export function removeLabelMatcher(text) {
return text.match(/\/remove-label ([a-zA-Z\d-, ]+)/);
}

export function reviewerMatcher(text) {
return text.match(/\/reviewers? ([@a-z/A-Z\d-, ]+)/);
}
183 changes: 183 additions & 0 deletions app/matchers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {
closeMatcher,
labelMatcher,
removeLabelMatcher,
reopenMatcher,
reviewerMatcher,
transferMatcher,
} from "./matchers.js";

describe("matchers", () => {
describe("transfer", () => {
test("matches /transfer and extracts the repo name", () => {
const result = transferMatcher("/transfer github-comment-ops");

expect(result).toBeTruthy();
expect(result[1]).toEqual("github-comment-ops");
});
test("does not match input without /transfer", () => {
const result = transferMatcher("transfer github-comment-ops");

expect(result).toBeFalsy();
});
test("does not match without a repository name", () => {
const result = transferMatcher("/transfer");

expect(result).toBeFalsy();
});
});

describe("close", () => {
test("matches /close", () => {
const result = closeMatcher("/close");

expect(result).toBeTruthy();
expect(result[1]).toBeUndefined();
});
test("does not match input without /close", () => {
const result = closeMatcher("close something");

expect(result).toBeFalsy();
});
test("does not match /closenot-planned", () => {
const result = closeMatcher("/closenot-planned");

expect(result).toBeTruthy();
expect(result[1]).toBeUndefined();
});

test("matches /close not-planned", () => {
const result = closeMatcher("/close not-planned");

expect(result).toBeTruthy();
expect(result[1]).toEqual("not-planned");
});
});

describe("reopen", () => {
test("matches /reopen", () => {
const result = reopenMatcher("/reopen");

expect(result).toBeTruthy();
});
test("does not match input without /reopen", () => {
const result = reopenMatcher("reopen blah");

expect(result).toBeFalsy();
});
});

describe("label", () => {
test("matches /label label1", () => {
const result = labelMatcher("/label label1");

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1");
});
test("does not match input without /label", () => {
const result = labelMatcher("label label1");

expect(result).toBeFalsy();
});
test("does not match /labellabel1", () => {
const result = labelMatcher("/labellabel1");

expect(result).toBeFalsy();
});

test("matches /label label1,label2", () => {
const result = labelMatcher("/label label1,label2");

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1,label2");
});
test("matches /label label1,label2 with spaces,label3", () => {
const result = labelMatcher("/label label1,label 2 with spaces,label3");

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1,label 2 with spaces,label3");
});
});

describe("remove-label", () => {
test("matches /remove-label label1", () => {
const result = removeLabelMatcher("/remove-label label1");

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1");
});
test("does not match input without /remove-label", () => {
const result = removeLabelMatcher("remove-label label1");

expect(result).toBeFalsy();
});
test("does not match /remove-labellabel1", () => {
const result = removeLabelMatcher("/remove-labellabel1");

expect(result).toBeFalsy();
});

test("matches /remove-label label1,label2", () => {
const result = removeLabelMatcher("/remove-label label1,label2");

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1,label2");
});
test("matches /remove-label label1,label2 with spaces,label3", () => {
const result = removeLabelMatcher(
"/remove-label label1,label 2 with spaces,label3"
);

expect(result).toBeTruthy();
expect(result[1]).toEqual("label1,label 2 with spaces,label3");
});
});

describe("reviewer", () => {
test("matches /reviewer reviewer1", () => {
const result = reviewerMatcher("/reviewer reviewer1");

expect(result).toBeTruthy();
expect(result[1]).toEqual("reviewer1");
});
test("matches /reviewers reviewer1,reviewer2", () => {
const result = reviewerMatcher("/reviewers reviewer1,reviewer2");

expect(result).toBeTruthy();
expect(result[1]).toEqual("reviewer1,reviewer2");
});
test("does not match input without /reviewer", () => {
const result = reviewerMatcher("reviewer reviewer1");

expect(result).toBeFalsy();
});
test("does not match /reviewerreviewer1", () => {
const result = reviewerMatcher("/reviewerreviewer1");

expect(result).toBeFalsy();
});

test("matches /reviewer reviewer1,reviewer2", () => {
const result = reviewerMatcher("/reviewer reviewer1,reviewer2");

expect(result).toBeTruthy();
expect(result[1]).toEqual("reviewer1,reviewer2");
});
test("matches /reviewer reviewer1,@reviewer2,@org/team", () => {
const result = reviewerMatcher(
"/reviewer reviewer1,@reviewer2,@org/team"
);

expect(result).toBeTruthy();
expect(result[1]).toEqual("reviewer1,@reviewer2,@org/team");
});
test("matches with space separator /reviewer reviewer1 @reviewer2 @org/team", () => {
const result = reviewerMatcher(
"/reviewer reviewer1 @reviewer2 @org/team"
);

expect(result).toBeTruthy();
expect(result[1]).toEqual("reviewer1 @reviewer2 @org/team");
});
});
});
127 changes: 127 additions & 0 deletions app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
closeMatcher,
labelMatcher,
removeLabelMatcher,
reopenMatcher,
reviewerMatcher,
transferMatcher,
} from "./matchers.js";
import {
addLabel,
closeIssue,
removeLabel,
reopenIssue,
requestReviewers,
transferIssue,
} from "./github.js";
import { getAuthToken } from "./auth.js";
import { extractUsersAndTeams } from "./converters.js";

export async function router(auth, id, payload, verbose) {
const sourceRepo = payload.repository.name;
const transferMatches = transferMatcher(payload.comment.body);
const actorRequest = `as requested by ${payload.sender.login}`;
if (transferMatches) {
const targetRepo = transferMatches[1];
console.log(
`${id} Transferring issue ${payload.issue.html_url} to repo ${targetRepo} ${actorRequest}`
);
await transferIssue(
await getAuthToken(auth, payload.installation.id),
payload.repository.owner.login,
sourceRepo,
targetRepo,
payload.issue.node_id
);
return;
}

const closeMatches = closeMatcher(payload.comment.body);
if (closeMatches) {
const reason =
closeMatches.length > 1 && closeMatches[1] === "not-planned"
? "NOT_PLANNED"
: "COMPLETED";
console.log(
`${id} Closing issue ${payload.issue.html_url}, reason: ${reason} ${actorRequest}`
);
await closeIssue(
await getAuthToken(auth, payload.installation.id),
sourceRepo,
payload.issue.node_id,
reason
);
return;
}

const reopenMatches = reopenMatcher(payload.comment.body);
if (reopenMatches) {
console.log(
`${id} Re-opening issue ${payload.issue.html_url} ${actorRequest}`
);
await reopenIssue(
await getAuthToken(auth, payload.installation.id),
sourceRepo,
payload.issue.node_id
);
return;
}

const labelMatches = labelMatcher(payload.comment.body);
if (labelMatches) {
const labels = labelMatches[1].split(",");

console.log(
`${id} Labeling issue ${payload.issue.html_url} with labels ${labels} ${actorRequest}`
);
await addLabel(
await getAuthToken(auth, payload.installation.id),
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
labels
);
return;
}

const removeLabelMatches = removeLabelMatcher(payload.comment.body);
if (removeLabelMatches) {
const labels = removeLabelMatches[1].split(",");

console.log(
`${id} Removing label(s) from issue ${payload.issue.html_url}, labels ${labels} ${actorRequest}`
);
await removeLabel(
await getAuthToken(auth, payload.installation.id),
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
labels
);
return;
}

const reviewerMatches = reviewerMatcher(payload.comment.body);
if (reviewerMatches) {
console.log(
`${id} Requesting review for ${reviewerMatches[1]} at ${payload.issue.html_url} ${actorRequest}`
);
const reviewers = extractUsersAndTeams(
payload.repository.owner.login,
reviewerMatches[1]
);
await requestReviewers(
await getAuthToken(auth, payload.installation.id),
payload.repository.owner.login,
sourceRepo,
payload.issue.node_id,
reviewers.users,
reviewers.teams
);
return;
}

if (verbose) {
console.log("No match for", payload.comment.body);
}
}
Loading