-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
7,086 additions
and
859 deletions.
There are no files selected for viewing
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
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("/")), | ||
}; | ||
} |
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
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"], | ||
}); | ||
}); | ||
}); | ||
}); |
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
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-, ]+)/); | ||
} |
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
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"); | ||
}); | ||
}); | ||
}); |
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
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); | ||
} | ||
} |
Oops, something went wrong.