From 1aa530b0c8a6709e63f79f19eaeae4a6929ed1ff Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:57:08 +0100 Subject: [PATCH 01/18] chore: teamate assignment --- src/handlers/shared/start.ts | 7 +++---- src/handlers/user-start-stop.ts | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index fbe77ba..f8c8891 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -6,7 +6,7 @@ import { generateAssignmentComment } from "./generate-assignment-comment"; import structuredMetadata from "./structured-metadata"; import { assignTableComment } from "./table"; -export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"]) { +export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]) { const { logger, config } = context; const { maxConcurrentTasks } = config.miscellaneous; const { taskStaleTimeoutDuration } = config.timers; @@ -35,9 +35,8 @@ export async function start(context: Context, issue: Context["payload"]["issue"] } // check max assigned issues - const openedPullRequests = await getAvailableOpenedPullRequests(context, sender.login); - logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: ${JSON.stringify(openedPullRequests)}`); + logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: `, { openedPullRequests }); const assignedIssues = await getAssignedIssues(context, sender.login); logger.info("Max issue allowed is", { maxConcurrentTasks }); @@ -83,7 +82,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const duration: number = calculateDurations(labels).shift() ?? 0; const { id, login } = sender; - const logMessage = logger.info("Task assigned successfully", { duration, priceLabel, revision: commitHash?.substring(0, 7) }); + const logMessage = logger.info("Task assigned successfully", { duration, priceLabel, revision: commitHash?.substring(0, 7), teammate: teammates, assignee: login, issue: issue.number }); const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration); const metadata = structuredMetadata.create("Assignment", logMessage); diff --git a/src/handlers/user-start-stop.ts b/src/handlers/user-start-stop.ts index 46148f3..23b961f 100644 --- a/src/handlers/user-start-stop.ts +++ b/src/handlers/user-start-stop.ts @@ -6,11 +6,12 @@ export async function userStartStop(context: Context): Promise<{ output: string const { payload } = context; const { issue, comment, sender, repository } = payload; const slashCommand = comment.body.split(" ")[0].replace("/", ""); + const teamMates = comment.body.split("@").slice(1).map((teamMate) => teamMate.split(" ")[0]); if (slashCommand === "stop") { return await stop(context, issue, sender, repository); } else if (slashCommand === "start") { - return await start(context, issue, sender); + return await start(context, issue, sender, teamMates); } return { output: null }; From 7281a72e960d05436293116684438e842c7fdb00 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:14:11 +0100 Subject: [PATCH 02/18] chore: unassign command user --- src/utils/get-linked-prs.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/get-linked-prs.ts b/src/utils/get-linked-prs.ts index 187f808..205afae 100644 --- a/src/utils/get-linked-prs.ts +++ b/src/utils/get-linked-prs.ts @@ -43,6 +43,5 @@ export async function getLinkedPullRequests(context: Context, { owner, repositor body: pr.body, }; }) - .filter((pr) => pr !== null) - .filter((pr) => pr.state === "open") as GetLinkedResults[]; + .filter((pr) => pr !== null && pr.state === "open") as GetLinkedResults[]; } From e486effa0465fe12e2fa5cbff686549b91ea9c51 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:51:24 +0100 Subject: [PATCH 03/18] chore: teams test --- src/handlers/shared/start.ts | 9 +++++++- tests/main.test.ts | 41 ++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index f8c8891..9e4b16f 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -82,7 +82,14 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const duration: number = calculateDurations(labels).shift() ?? 0; const { id, login } = sender; - const logMessage = logger.info("Task assigned successfully", { duration, priceLabel, revision: commitHash?.substring(0, 7), teammate: teammates, assignee: login, issue: issue.number }); + const logMessage = logger.info("Task assigned successfully", { + duration, + priceLabel, + revision: commitHash?.substring(0, 7), + teammate: teammates, + assignee: login, + issue: issue.number, + }); const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration); const metadata = structuredMetadata.create("Assignment", logMessage); diff --git a/tests/main.test.ts b/tests/main.test.ts index 15181f1..2543d89 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -52,6 +52,25 @@ describe("User start/stop", () => { expect(output).toEqual("Task assigned successfully"); }); + test("User can start an issue with teammates", async () => { + const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; + const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; + + const context = createContext(issue, sender, "/start @user2"); + + context.adapters = createAdapters(getSupabase(), context as unknown as Context); + + const { output } = await userStartStop(context as unknown as Context); + + expect(output).toEqual("Task assigned successfully"); + + const issue2 = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; + + expect(issue2.assignees).toHaveLength(2); + + expect(issue2.assignees).toEqual(expect.arrayContaining(["ubiquity", "user2"])); + }); + test("User can stop an issue", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 2 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as Sender; @@ -66,7 +85,7 @@ describe("User start/stop", () => { }); test("Stopping an issue should close the author's linked PR", async () => { - const infoSpy = jest.spyOn(console, "info").mockImplementation(() => {}); + const infoSpy = jest.spyOn(console, "info").mockImplementation(() => { }); const issue = db.issue.findFirst({ where: { id: { equals: 2 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as Sender; const context = createContext(issue, sender, "/stop"); @@ -416,7 +435,6 @@ async function setupTests() { }, body: "Pull request body", owner: "ubiquity", - repo: "test-repo", state: "open", closed_at: null, @@ -454,6 +472,7 @@ async function setupTests() { state: "open", body: `Resolves #2`, html_url: "https://github.com/ubiquity/test-repo/pull/10", + state: "open", repository: { full_name: TEST_REPO, }, @@ -566,17 +585,17 @@ function getSupabase(withData = true) { single: jest.fn().mockResolvedValue({ data: withData ? { - id: 1, - wallets: { - address: "0x123", - }, - } + id: 1, + wallets: { + address: "0x123", + }, + } : { - id: 1, - wallets: { - address: undefined, - }, + id: 1, + wallets: { + address: undefined, }, + }, }), }), }), From 790926780fc0cfad8029a700fd21fbb7bda6cedd Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 16 Jul 2024 00:39:54 +0100 Subject: [PATCH 04/18] chore: correct comment --- src/handlers/shared/start.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 9e4b16f..e2cbe71 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -63,13 +63,14 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const assignees = (issue?.assignees ?? []).filter(Boolean); if (assignees.length !== 0) { - const log = logger.error("The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error("Issue is already assigned"); + // const log = logger.error("The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); + // await addCommentToIssue(context, log?.logMessage.diff as string); + const currentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); + const log = logger.error(currentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); + return await addCommentToIssue(context, log?.logMessage.diff as string); } // get labels - const labels = issue.labels; const priceLabel = labels.find((label: Label) => label.name.startsWith("Price: ")); From deaf33edb6ece4f1d748338c30df3704ed132ef3 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:14:25 +0100 Subject: [PATCH 05/18] chore: max assigned check all users --- src/handlers/shared/start.ts | 32 +++++++++++++++++++++++++++++--- src/utils/issue.ts | 15 ++++----------- tests/main.test.ts | 2 +- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index e2cbe71..8e51e73 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -61,13 +61,23 @@ export async function start(context: Context, issue: Context["payload"]["issue"] throw new Error("Issue is closed"); } - const assignees = (issue?.assignees ?? []).filter(Boolean); + const assignees = issue?.assignees ?? [] + if (assignees.length !== 0) { // const log = logger.error("The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); // await addCommentToIssue(context, log?.logMessage.diff as string); const currentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); - const log = logger.error(currentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); - return await addCommentToIssue(context, log?.logMessage.diff as string); + const comment = currentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task."; + await addCommentToIssue(context, `\`\`\`diff\n! ${comment}`); + throw new Error(comment); + } + + teammates.push(sender.login) + + // check max assigned issues + for (const user of teammates) { + if (!user) continue; + await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login); } // get labels @@ -118,3 +128,19 @@ export async function start(context: Context, issue: Context["payload"]["issue"] return { output: "Task assigned successfully" }; } + +async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: number, logger: Context["logger"], sender: string) { + const openedPullRequests = await getAvailableOpenedPullRequests(context, username); + logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: `, { openedPullRequests }); + + const assignedIssues = await getAssignedIssues(context, username); + logger.info("Max issue allowed is", { maxConcurrentTasks, assignedIssues: assignedIssues.map((issue) => `${issue.url}`) }); + + // check for max and enforce max + if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { + const isSender = username === sender; + const comment = (isSender ? "You have" : `${username} has`) + ` reached the max limit of ${maxConcurrentTasks} assigned issues.`; + await addCommentToIssue(context, `\`\`\`diff\n! ${comment}\n\`\`\``); + throw new Error(`Too many assigned issues, you have reached your max limit of ${maxConcurrentTasks} issues.`); + } +} \ No newline at end of file diff --git a/src/utils/issue.ts b/src/utils/issue.ts index e6746e8..b7d5288 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -8,19 +8,12 @@ export function isParentIssue(body: string) { } export async function getAssignedIssues(context: Context, username: string): Promise { - const payload = context.payload; + const { payload } = context try { - return await context.octokit.paginate( - context.octokit.issues.listForRepo, - { - owner: payload.repository.owner.login, - repo: payload.repository.name, - state: ISSUE_TYPE.OPEN, - per_page: 100, - }, - ({ data: issues }) => issues.filter((issue: Issue) => !issue.pull_request && issue.assignee && issue.assignee.login === username) - ); + return await context.octokit.search.issuesAndPullRequests({ + q: `is:open assignee:${username} org:${payload.repository.owner.login}`, + }).then((response) => response.data.items) as Issue[]; } catch (err: unknown) { context.logger.error("Fetching assigned issues failed!", { error: err as Error }); return []; diff --git a/tests/main.test.ts b/tests/main.test.ts index 2543d89..ca74162 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -138,7 +138,7 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const err = "Issue is already assigned"; + const err = "The issue is already assigned. Please choose another unassigned task."; try { await userStartStop(context as unknown as Context); From aafc15e76ae4ec4dfa90b76fc3e4cb16696f39f2 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:16:40 +0100 Subject: [PATCH 06/18] chore: fix eslint naming convention --- src/handlers/shared/start.ts | 15 ++++++--------- src/utils/issue.ts | 12 +++++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 8e51e73..e29481a 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -61,18 +61,15 @@ export async function start(context: Context, issue: Context["payload"]["issue"] throw new Error("Issue is closed"); } - const assignees = issue?.assignees ?? [] + const assignees = issue?.assignees ?? []; if (assignees.length !== 0) { - // const log = logger.error("The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); - // await addCommentToIssue(context, log?.logMessage.diff as string); - const currentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); - const comment = currentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task."; - await addCommentToIssue(context, `\`\`\`diff\n! ${comment}`); - throw new Error(comment); + const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); + const log = logger.error(isCurrentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); + return await addCommentToIssue(context, log?.logMessage.diff as string); } - teammates.push(sender.login) + teammates.push(sender.login); // check max assigned issues for (const user of teammates) { @@ -143,4 +140,4 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc await addCommentToIssue(context, `\`\`\`diff\n! ${comment}\n\`\`\``); throw new Error(`Too many assigned issues, you have reached your max limit of ${maxConcurrentTasks} issues.`); } -} \ No newline at end of file +} diff --git a/src/utils/issue.ts b/src/utils/issue.ts index b7d5288..eb8442f 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,5 +1,5 @@ import { Context } from "../types/context"; -import { Issue, ISSUE_TYPE, PullRequest, Review } from "../types/payload"; +import { Issue, PullRequest, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; export function isParentIssue(body: string) { @@ -8,12 +8,14 @@ export function isParentIssue(body: string) { } export async function getAssignedIssues(context: Context, username: string): Promise { - const { payload } = context + const { payload } = context; try { - return await context.octokit.search.issuesAndPullRequests({ - q: `is:open assignee:${username} org:${payload.repository.owner.login}`, - }).then((response) => response.data.items) as Issue[]; + return (await context.octokit.search + .issuesAndPullRequests({ + q: `is:open assignee:${username} org:${payload.repository.owner.login}`, + }) + .then((response) => response.data.items)) as Issue[]; } catch (err: unknown) { context.logger.error("Fetching assigned issues failed!", { error: err as Error }); return []; From ed2e2ddfbfddef1603a7fc48091c72baa1cf79c8 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:50:07 +0100 Subject: [PATCH 07/18] chore: update logs and test --- src/handlers/shared/start.ts | 38 +++++++++++++++++++++------------ src/handlers/user-start-stop.ts | 5 ++++- src/utils/get-linked-prs.ts | 3 +-- tests/__mocks__/handlers.ts | 7 +++++- tests/main.test.ts | 25 ++++++++++------------ 5 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index e29481a..b086fa7 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -39,7 +39,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: `, { openedPullRequests }); const assignedIssues = await getAssignedIssues(context, sender.login); - logger.info("Max issue allowed is", { maxConcurrentTasks }); + logger.info("Max issue allowed is", { maxConcurrentTasks, assignedIssues: assignedIssues?.map((issue) => `${issue.url}`) }); // check for max and enforce max @@ -65,8 +65,12 @@ export async function start(context: Context, issue: Context["payload"]["issue"] if (assignees.length !== 0) { const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); - const log = logger.error(isCurrentUserAssigned ? "You are already assigned to this task." : "The issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number }); - return await addCommentToIssue(context, log?.logMessage.diff as string); + const log = logger.error( + isCurrentUserAssigned ? "You are already assigned to this task." : "This issue is already assigned. Please choose another unassigned task.", + { issueNumber: issue.number } + ); + await addCommentToIssue(context, log?.logMessage.diff as string); + throw new Error(log?.logMessage.diff); } teammates.push(sender.login); @@ -94,7 +98,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] duration, priceLabel, revision: commitHash?.substring(0, 7), - teammate: teammates, + teammates: teammates, assignee: login, issue: issue.number, }); @@ -102,11 +106,17 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration); const metadata = structuredMetadata.create("Assignment", logMessage); - // add assignee - if (!assignees.map((i: Partial) => i?.login).includes(login)) { - await addAssignees(context, issue.number, [login]); + const toAssign = []; + + for (const teammate of teammates) { + if (!assignees.find((assignee: Partial) => assignee?.login?.toLowerCase() === teammate.toLowerCase())) { + toAssign.push(teammate); + } } + // assign the issue + await addAssignees(context, issue.number, toAssign); + const isTaskStale = checkTaskStale(taskStaleTimeoutDuration, issue.created_at); await addCommentToIssue( @@ -128,16 +138,16 @@ export async function start(context: Context, issue: Context["payload"]["issue"] async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: number, logger: Context["logger"], sender: string) { const openedPullRequests = await getAvailableOpenedPullRequests(context, username); - logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: `, { openedPullRequests }); - const assignedIssues = await getAssignedIssues(context, username); - logger.info("Max issue allowed is", { maxConcurrentTasks, assignedIssues: assignedIssues.map((issue) => `${issue.url}`) }); // check for max and enforce max if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { - const isSender = username === sender; - const comment = (isSender ? "You have" : `${username} has`) + ` reached the max limit of ${maxConcurrentTasks} assigned issues.`; - await addCommentToIssue(context, `\`\`\`diff\n! ${comment}\n\`\`\``); - throw new Error(`Too many assigned issues, you have reached your max limit of ${maxConcurrentTasks} issues.`); + const log = logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, { + assignedIssues: assignedIssues.length, + openedPullRequests: openedPullRequests.length, + maxConcurrentTasks, + }); + await addCommentToIssue(context, log?.logMessage.diff as string); + throw new Error(log?.logMessage.diff); } } diff --git a/src/handlers/user-start-stop.ts b/src/handlers/user-start-stop.ts index 23b961f..a7c8815 100644 --- a/src/handlers/user-start-stop.ts +++ b/src/handlers/user-start-stop.ts @@ -6,7 +6,10 @@ export async function userStartStop(context: Context): Promise<{ output: string const { payload } = context; const { issue, comment, sender, repository } = payload; const slashCommand = comment.body.split(" ")[0].replace("/", ""); - const teamMates = comment.body.split("@").slice(1).map((teamMate) => teamMate.split(" ")[0]); + const teamMates = comment.body + .split("@") + .slice(1) + .map((teamMate) => teamMate.split(" ")[0]); if (slashCommand === "stop") { return await stop(context, issue, sender, repository); diff --git a/src/utils/get-linked-prs.ts b/src/utils/get-linked-prs.ts index 205afae..0ffd852 100644 --- a/src/utils/get-linked-prs.ts +++ b/src/utils/get-linked-prs.ts @@ -42,6 +42,5 @@ export async function getLinkedPullRequests(context: Context, { owner, repositor state: pr.state, body: pr.body, }; - }) - .filter((pr) => pr !== null && pr.state === "open") as GetLinkedResults[]; + }).filter((pr) => pr !== null && pr.state === "open") as GetLinkedResults[]; } diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index d975b95..24a882b 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -81,7 +81,7 @@ export const handlers = [ db.issue.update({ where: { id: { equals: issue.id } }, data: { - assignees, + assignees: [...issue.assignees, ...assignees], }, }); } @@ -107,4 +107,9 @@ export const handlers = [ http.delete("https://api.github.com/repos/:owner/:repo/issues/:issue_number/assignees", ({ params: { owner, repo, issue_number: issueNumber } }) => HttpResponse.json({ owner, repo, issueNumber }) ), + // search issues + http.get("https://api.github.com/search/issues", () => { + const issues = [db.issue.findFirst({ where: { number: { equals: 1 } } })]; + return HttpResponse.json({ items: issues }); + }), ]; diff --git a/tests/main.test.ts b/tests/main.test.ts index ca74162..40d5cb7 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -65,9 +65,7 @@ describe("User start/stop", () => { expect(output).toEqual("Task assigned successfully"); const issue2 = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - expect(issue2.assignees).toHaveLength(2); - expect(issue2.assignees).toEqual(expect.arrayContaining(["ubiquity", "user2"])); }); @@ -85,7 +83,7 @@ describe("User start/stop", () => { }); test("Stopping an issue should close the author's linked PR", async () => { - const infoSpy = jest.spyOn(console, "info").mockImplementation(() => { }); + const infoSpy = jest.spyOn(console, "info").mockImplementation(() => {}); const issue = db.issue.findFirst({ where: { id: { equals: 2 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as Sender; const context = createContext(issue, sender, "/stop"); @@ -138,7 +136,7 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const err = "The issue is already assigned. Please choose another unassigned task."; + const err = "```diff\n! This issue is already assigned. Please choose another unassigned task.\n```"; try { await userStartStop(context as unknown as Context); @@ -472,7 +470,6 @@ async function setupTests() { state: "open", body: `Resolves #2`, html_url: "https://github.com/ubiquity/test-repo/pull/10", - state: "open", repository: { full_name: TEST_REPO, }, @@ -585,17 +582,17 @@ function getSupabase(withData = true) { single: jest.fn().mockResolvedValue({ data: withData ? { - id: 1, - wallets: { - address: "0x123", - }, - } + id: 1, + wallets: { + address: "0x123", + }, + } : { - id: 1, - wallets: { - address: undefined, + id: 1, + wallets: { + address: undefined, + }, }, - }, }), }), }), From 5b3c4d90a633cc138eff4dabf1c2f1a3929d5bcf Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:55:44 +0100 Subject: [PATCH 08/18] chore: remove redunant logic --- src/handlers/shared/start.ts | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index b086fa7..0adbc28 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,4 +1,4 @@ -import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; +import { Context, ISSUE_TYPE, Label } from "../../types"; import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue } from "../../utils/issue"; import { calculateDurations } from "../../utils/shared"; import { checkTaskStale } from "./check-task-stale"; @@ -34,25 +34,6 @@ export async function start(context: Context, issue: Context["payload"]["issue"] logger.error("Error while getting commit hash", { error: e as Error }); } - // check max assigned issues - const openedPullRequests = await getAvailableOpenedPullRequests(context, sender.login); - logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: `, { openedPullRequests }); - - const assignedIssues = await getAssignedIssues(context, sender.login); - logger.info("Max issue allowed is", { maxConcurrentTasks, assignedIssues: assignedIssues?.map((issue) => `${issue.url}`) }); - - // check for max and enforce max - - if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { - const log = logger.error("Too many assigned issues, you have reached your max limit", { - assignedIssues: assignedIssues.length, - openedPullRequests: openedPullRequests.length, - maxConcurrentTasks, - }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error(`Too many assigned issues, you have reached your max limit of ${maxConcurrentTasks} issues.`); - } - // is it assignable? if (issue.state === ISSUE_TYPE.CLOSED) { @@ -63,6 +44,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const assignees = issue?.assignees ?? []; + // find out if the issue is already assigned if (assignees.length !== 0) { const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); const log = logger.error( @@ -77,7 +59,6 @@ export async function start(context: Context, issue: Context["payload"]["issue"] // check max assigned issues for (const user of teammates) { - if (!user) continue; await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login); } @@ -106,16 +87,8 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration); const metadata = structuredMetadata.create("Assignment", logMessage); - const toAssign = []; - - for (const teammate of teammates) { - if (!assignees.find((assignee: Partial) => assignee?.login?.toLowerCase() === teammate.toLowerCase())) { - toAssign.push(teammate); - } - } - // assign the issue - await addAssignees(context, issue.number, toAssign); + await addAssignees(context, issue.number, teammates); const isTaskStale = checkTaskStale(taskStaleTimeoutDuration, issue.created_at); From 570f60e87d5ee26e44fea8d9b0dcb1ea0202de45 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:44:08 +0100 Subject: [PATCH 09/18] chore: throw errors and paginate --- src/handlers/shared/start.ts | 5 ++--- src/handlers/shared/stop.ts | 18 +++++++++++------- src/utils/issue.ts | 23 +++++++++-------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 0adbc28..80537ea 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -74,13 +74,12 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const duration: number = calculateDurations(labels).shift() ?? 0; - const { id, login } = sender; + const { id } = sender; const logMessage = logger.info("Task assigned successfully", { duration, priceLabel, revision: commitHash?.substring(0, 7), - teammates: teammates, - assignee: login, + assignees: teammates, issue: issue.number, }); diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 4f10186..5bce080 100644 --- a/src/handlers/shared/stop.ts +++ b/src/handlers/shared/stop.ts @@ -26,16 +26,20 @@ export async function stop(context: Context, issue: Context["payload"]["issue"], // remove assignee - await context.octokit.rest.issues.removeAssignees({ - owner: login, - repo: name, - issue_number: issueNumber, - assignees: [sender.login], - }); + try { + await context.octokit.rest.issues.removeAssignees({ + owner: login, + repo: name, + issue_number: issueNumber, + assignees: [userToUnassign.login], + }); + } catch (err) { + throw new Error(`Error while removing ${userToUnassign.login} from the issue: ${err}`); + } const unassignedLog = logger.info("You have been unassigned from the task", { issueNumber, - user: sender.login, + user: userToUnassign.login, }); await addCommentToIssue(context, unassignedLog?.logMessage.diff as string); diff --git a/src/utils/issue.ts b/src/utils/issue.ts index eb8442f..2990630 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -11,14 +11,11 @@ export async function getAssignedIssues(context: Context, username: string): Pro const { payload } = context; try { - return (await context.octokit.search - .issuesAndPullRequests({ - q: `is:open assignee:${username} org:${payload.repository.owner.login}`, - }) - .then((response) => response.data.items)) as Issue[]; + return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, { + q: `is:issue is:open assignee:${username} org:${payload.repository.owner.login}`, + })) as Issue[]; } catch (err: unknown) { - context.logger.error("Fetching assigned issues failed!", { error: err as Error }); - return []; + throw context.logger.error("Fetching assigned issues failed!", { error: err as Error }); } } @@ -36,7 +33,7 @@ export async function addCommentToIssue(context: Context, message: string | null body: comment, }); } catch (err: unknown) { - context.logger.error("Adding a comment failed!", { error: err as Error }); + throw context.logger.error("Adding a comment failed!", { error: err as Error }); } } @@ -52,7 +49,7 @@ export async function closePullRequest(context: Context, results: GetLinkedResul state: "closed", }); } catch (err: unknown) { - context.logger.error("Closing pull requests failed!", { error: err as Error }); + throw context.logger.error("Closing pull requests failed!", { error: err as Error }); } } @@ -111,7 +108,7 @@ export async function addAssignees(context: Context, issueNo: number, assignees: const payload = context.payload; try { - await context.octokit.rest.issues.addAssignees({ + await context.octokit.issues.addAssignees({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNo, @@ -133,8 +130,7 @@ export async function getAllPullRequests(context: Context, state: "open" | "clos per_page: 100, })) as PullRequest[]; } catch (err: unknown) { - context.logger.error("Fetching all pull requests failed!", { error: err as Error }); - return []; + throw context.logger.error("Fetching all pull requests failed!", { error: err as Error }); } } @@ -155,8 +151,7 @@ export async function getAllPullRequestReviews(context: Context, pullNumber: num }, })) as Review[]; } catch (err: unknown) { - context.logger.error("Fetching all pull request reviews failed!", { error: err as Error }); - return []; + throw context.logger.error("Fetching all pull request reviews failed!", { error: err as Error }); } } From 4342e5f24abcfa10cb3e261c7db4130e54d20361 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 7 Aug 2024 00:41:31 +0100 Subject: [PATCH 10/18] feat: custom message for private issues without plan --- src/utils/issue.ts | 33 +++++++++++++++++++++++++++++++++ tests/__mocks__/handlers.ts | 8 ++++++++ 2 files changed, 41 insertions(+) diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 2990630..8af20d5 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -104,6 +104,37 @@ export async function closePullRequestForAnIssue(context: Context, issueNumber: return logger.info(comment); } +async function confirmMultiAssignment(context: Context, issueNumber: number, usernames: string[]) { + const { logger, payload, octokit } = context; + + if (usernames.length < 2) { + return; + } + + const { private: isPrivate } = payload.repository; + + const { + data: { assignees }, + } = await octokit.issues.get({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + }); + + if (!assignees?.length) { + const log = logger.error("We detected that this task was not assigned to anyone. Please report this to the maintainers.", { issueNumber, usernames }); + await addCommentToIssue(context, log?.logMessage.diff as string); + throw new Error(log?.logMessage.raw); + } + + if (isPrivate && assignees?.length <= 1) { + const log = logger.error("This task belongs to a private repo and can only be assigned to one user without an official paid GitHub subscription.", { + issueNumber, + }); + await addCommentToIssue(context, log?.logMessage.diff as string); + } +} + export async function addAssignees(context: Context, issueNo: number, assignees: string[]) { const payload = context.payload; @@ -117,6 +148,8 @@ export async function addAssignees(context: Context, issueNo: number, assignees: } catch (e: unknown) { throw context.logger.error("Adding the assignee failed", { assignee: assignees, issueNo, error: e as Error }); } + + await confirmMultiAssignment(context, issueNo, assignees); } export async function getAllPullRequests(context: Context, state: "open" | "closed" | "all" = "open") { diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index 24a882b..f247b08 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -112,4 +112,12 @@ export const handlers = [ const issues = [db.issue.findFirst({ where: { number: { equals: 1 } } })]; return HttpResponse.json({ items: issues }); }), + // get issue by number + http.get("https://api.github.com/repos/:owner/:repo/issues/:issue_number", ({ params: { owner, repo, issue_number: issueNumber } }) => + HttpResponse.json( + db.issue.findFirst({ + where: { owner: { equals: owner as string }, repo: { equals: repo as string }, number: { equals: Number(issueNumber) } }, + }) + ) + ), ]; From 23f8a775ad03aa7c4b0553eca318a08d16737d5c Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:37:20 +0100 Subject: [PATCH 11/18] chore: use octokit.rest --- src/handlers/shared/start.ts | 2 +- src/utils/get-linked-prs.ts | 2 +- src/utils/issue.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 80537ea..eeb38d1 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -24,7 +24,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] let commitHash: string | null = null; try { - const hashResponse = await context.octokit.repos.getCommit({ + const hashResponse = await context.octokit.rest.repos.getCommit({ owner: context.payload.repository.owner.login, repo: context.payload.repository.name, ref: context.payload.repository.default_branch, diff --git a/src/utils/get-linked-prs.ts b/src/utils/get-linked-prs.ts index 0ffd852..5614ce5 100644 --- a/src/utils/get-linked-prs.ts +++ b/src/utils/get-linked-prs.ts @@ -22,7 +22,7 @@ export async function getLinkedPullRequests(context: Context, { owner, repositor throw new Error("Issue is not defined"); } - const { data: timeline } = (await context.octokit.issues.listEventsForTimeline({ + const { data: timeline } = (await context.octokit.rest.issues.listEventsForTimeline({ owner, repo: repository, issue_number: issue, diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 8af20d5..fe43239 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -11,7 +11,7 @@ export async function getAssignedIssues(context: Context, username: string): Pro const { payload } = context; try { - return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, { + return (await context.octokit.paginate(context.octokit.rest.search.issuesAndPullRequests, { q: `is:issue is:open assignee:${username} org:${payload.repository.owner.login}`, })) as Issue[]; } catch (err: unknown) { @@ -26,7 +26,7 @@ export async function addCommentToIssue(context: Context, message: string | null const issueNumber = payload.issue.number; try { - await context.octokit.issues.createComment({ + await context.octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, @@ -115,7 +115,7 @@ async function confirmMultiAssignment(context: Context, issueNumber: number, use const { data: { assignees }, - } = await octokit.issues.get({ + } = await octokit.rest.issues.get({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, @@ -139,7 +139,7 @@ export async function addAssignees(context: Context, issueNo: number, assignees: const payload = context.payload; try { - await context.octokit.issues.addAssignees({ + await context.octokit.rest.issues.addAssignees({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNo, From 26fdd0e9dbd1a29e5dcf62eecd842f1e251aba4a Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:28:29 +0100 Subject: [PATCH 12/18] chore: throw logReturn and catch-all error comment --- src/handlers/shared/start.ts | 18 +++++------------- src/handlers/shared/stop.ts | 10 ++++++---- src/plugin.ts | 12 ++++++++++-- src/utils/issue.ts | 12 ++++++------ 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index eeb38d1..e8262c7 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -18,7 +18,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] "```diff\n# Please select a child issue from the specification checklist to work on. The '/start' command is disabled on parent issues.\n```" ); logger.error(`Skipping '/start' since the issue is a parent issue`); - throw new Error("Issue is a parent issue"); + return { output: "Parent issue detected" }; } let commitHash: string | null = null; @@ -37,9 +37,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] // is it assignable? if (issue.state === ISSUE_TYPE.CLOSED) { - const log = logger.error("This issue is closed, please choose another.", { issueNumber: issue.number }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error("Issue is closed"); + throw logger.error("This issue is closed, please choose another.", { issueNumber: issue.number }); } const assignees = issue?.assignees ?? []; @@ -47,12 +45,10 @@ export async function start(context: Context, issue: Context["payload"]["issue"] // find out if the issue is already assigned if (assignees.length !== 0) { const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); - const log = logger.error( + throw logger.error( isCurrentUserAssigned ? "You are already assigned to this task." : "This issue is already assigned. Please choose another unassigned task.", { issueNumber: issue.number } ); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error(log?.logMessage.diff); } teammates.push(sender.login); @@ -67,9 +63,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const priceLabel = labels.find((label: Label) => label.name.startsWith("Price: ")); if (!priceLabel) { - const log = logger.error("No price label is set to calculate the duration", { issueNumber: issue.number }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error("No price label is set to calculate the duration"); + throw logger.error("No price label is set to calculate the duration", { issueNumber: issue.number }); } const duration: number = calculateDurations(labels).shift() ?? 0; @@ -114,12 +108,10 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc // check for max and enforce max if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { - const log = logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, { + throw logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, { assignedIssues: assignedIssues.length, openedPullRequests: openedPullRequests.length, maxConcurrentTasks, }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error(log?.logMessage.diff); } } diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 5bce080..633b099 100644 --- a/src/handlers/shared/stop.ts +++ b/src/handlers/shared/stop.ts @@ -11,9 +11,7 @@ export async function stop(context: Context, issue: Context["payload"]["issue"], const userToUnassign = assignees.find((assignee: Partial) => assignee?.login?.toLowerCase() === sender.login.toLowerCase()); if (!userToUnassign) { - const log = logger.error("You are not assigned to this task", { issueNumber, user: sender.login }); - await addCommentToIssue(context, log?.logMessage.diff as string); - return { output: "You are not assigned to this task" }; + throw logger.error("You are not assigned to this task", { issueNumber, user: sender.login }); } // close PR @@ -34,7 +32,11 @@ export async function stop(context: Context, issue: Context["payload"]["issue"], assignees: [userToUnassign.login], }); } catch (err) { - throw new Error(`Error while removing ${userToUnassign.login} from the issue: ${err}`); + throw logger.error(`Error while removing ${userToUnassign.login} from the issue: `, { + err, + issueNumber, + user: userToUnassign.login, + }); } const unassignedLog = logger.info("You have been unassigned from the task", { diff --git a/src/plugin.ts b/src/plugin.ts index ec2bf09..64ae621 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,9 +1,10 @@ import { Octokit } from "@octokit/rest"; import { createClient } from "@supabase/supabase-js"; -import { Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; import { createAdapters } from "./adapters"; import { userStartStop } from "./handlers/user-start-stop"; import { Context, Env, PluginInputs } from "./types"; +import { addCommentToIssue } from "./utils/issue"; export async function startStopTask(inputs: PluginInputs, env: Env) { const octokit = new Octokit({ auth: inputs.authToken }); @@ -22,7 +23,14 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { context.adapters = createAdapters(supabase, context); if (context.eventName === "issue_comment.created") { - await userStartStop(context); + try { + return await userStartStop(context); + } catch (err) { + if (err instanceof LogReturn) { + const errorMessage = context.logger.error(`Failed to run comment evaluation. ${err.logMessage?.raw || err}`, { err }); + await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); + } + } } else { context.logger.error(`Unsupported event: ${context.eventName}`); } diff --git a/src/utils/issue.ts b/src/utils/issue.ts index fe43239..d019d37 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -56,8 +56,10 @@ export async function closePullRequest(context: Context, results: GetLinkedResul export async function closePullRequestForAnIssue(context: Context, issueNumber: number, repository: Context["payload"]["repository"], author: string) { const { logger } = context; if (!issueNumber) { - logger.error("Issue is not defined"); - return; + throw logger.error("Issue is not defined", { + issueNumber, + repository: repository.name, + }); } const linkedPullRequests = await getLinkedPullRequests(context, { @@ -122,13 +124,11 @@ async function confirmMultiAssignment(context: Context, issueNumber: number, use }); if (!assignees?.length) { - const log = logger.error("We detected that this task was not assigned to anyone. Please report this to the maintainers.", { issueNumber, usernames }); - await addCommentToIssue(context, log?.logMessage.diff as string); - throw new Error(log?.logMessage.raw); + throw logger.error("We detected that this task was not assigned to anyone. Please report this to the maintainers.", { issueNumber, usernames }); } if (isPrivate && assignees?.length <= 1) { - const log = logger.error("This task belongs to a private repo and can only be assigned to one user without an official paid GitHub subscription.", { + const log = logger.info("This task belongs to a private repo and can only be assigned to one user without an official paid GitHub subscription.", { issueNumber, }); await addCommentToIssue(context, log?.logMessage.diff as string); From 3f4fa4ee0ad85b6051987f39dbdd4624810fc499 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:30:19 +0100 Subject: [PATCH 13/18] chore: catch other errors --- src/plugin.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 64ae621..7c9b365 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -26,10 +26,13 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { try { return await userStartStop(context); } catch (err) { + let errorMessage; if (err instanceof LogReturn) { - const errorMessage = context.logger.error(`Failed to run comment evaluation. ${err.logMessage?.raw || err}`, { err }); - await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); + errorMessage = context.logger.error(`Failed to run comment evaluation. ${err.logMessage?.raw || err}`, { err }); + } else { + errorMessage = context.logger.error(`Failed to run comment evaluation. ${err}`, { err }); } + await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); } } else { context.logger.error(`Unsupported event: ${context.eventName}`); From 1f9d69365d3bbda58526781a40951303397a00bb Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:47:12 +0100 Subject: [PATCH 14/18] chore: update tests --- tests/main.test.ts | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index 40d5cb7..dffa859 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -9,7 +9,7 @@ import issueTemplate from "./__mocks__/issue-template"; import { createAdapters } from "../src/adapters"; import { createClient } from "@supabase/supabase-js"; import dotenv from "dotenv"; -import { Logs, cleanLogString } from "@ubiquity-dao/ubiquibot-logger"; +import { LogReturn, Logs, cleanLogString } from "@ubiquity-dao/ubiquibot-logger"; dotenv.config(); type Issue = Context["payload"]["issue"]; @@ -110,9 +110,25 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const output = await userStartStop(context as unknown as Context); + const logReturn: LogReturn = { + logMessage: { + diff: "```diff\n! You are not assigned to this task\n```", + level: "error", + raw: "You are not assigned to this task", + type: "error", + }, + metadata: { + caller: "error", + issueNumber: 2, + user: "ubiquity", + }, + }; - expect(output).toEqual({ output: "You are not assigned to this task" }); + try { + expect(await userStartStop(context as unknown as Context)).toThrow(logReturn); + } catch (error) { + expect(error).toEqual(logReturn); + } }); test("User can't stop an issue without assignees", async () => { @@ -120,12 +136,27 @@ describe("User start/stop", () => { const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; const context = createContext(issue, sender, "/stop"); - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const output = await userStartStop(context as unknown as Context); + const logReturn: LogReturn = { + logMessage: { + diff: "```diff\n! You are not assigned to this task\n```", + level: "error", + raw: "You are not assigned to this task", + type: "error", + }, + metadata: { + caller: "error", + issueNumber: 5, + user: "ubiquity", + }, + }; - expect(output).toEqual({ output: "You are not assigned to this task" }); + try { + expect(await userStartStop(context as unknown as Context)).toThrow(logReturn); + } catch (error) { + expect(error).toEqual(logReturn); + } }); test("User can't start an issue that's already assigned", async () => { From 8db028c5ba4a1350879d0dff5ea4262f4b50b950 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:12:46 +0100 Subject: [PATCH 15/18] chore: fix test by throwing Error --- src/handlers/shared/stop.ts | 2 +- tests/main.test.ts | 76 ++----------------------------------- 2 files changed, 4 insertions(+), 74 deletions(-) diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 633b099..3a2541e 100644 --- a/src/handlers/shared/stop.ts +++ b/src/handlers/shared/stop.ts @@ -11,7 +11,7 @@ export async function stop(context: Context, issue: Context["payload"]["issue"], const userToUnassign = assignees.find((assignee: Partial) => assignee?.login?.toLowerCase() === sender.login.toLowerCase()); if (!userToUnassign) { - throw logger.error("You are not assigned to this task", { issueNumber, user: sender.login }); + throw new Error(logger.error("You are not assigned to this task", { issueNumber, user: sender.login })?.logMessage.diff as string); } // close PR diff --git a/tests/main.test.ts b/tests/main.test.ts index dffa859..8c109d0 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -9,7 +9,7 @@ import issueTemplate from "./__mocks__/issue-template"; import { createAdapters } from "../src/adapters"; import { createClient } from "@supabase/supabase-js"; import dotenv from "dotenv"; -import { LogReturn, Logs, cleanLogString } from "@ubiquity-dao/ubiquibot-logger"; +import { Logs, cleanLogString } from "@ubiquity-dao/ubiquibot-logger"; dotenv.config(); type Issue = Context["payload"]["issue"]; @@ -110,25 +110,7 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const logReturn: LogReturn = { - logMessage: { - diff: "```diff\n! You are not assigned to this task\n```", - level: "error", - raw: "You are not assigned to this task", - type: "error", - }, - metadata: { - caller: "error", - issueNumber: 2, - user: "ubiquity", - }, - }; - - try { - expect(await userStartStop(context as unknown as Context)).toThrow(logReturn); - } catch (error) { - expect(error).toEqual(logReturn); - } + await expect(userStartStop(context as unknown as Context)).rejects.toThrow("```diff\n! You are not assigned to this task\n```"); }); test("User can't stop an issue without assignees", async () => { @@ -138,25 +120,7 @@ describe("User start/stop", () => { const context = createContext(issue, sender, "/stop"); context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const logReturn: LogReturn = { - logMessage: { - diff: "```diff\n! You are not assigned to this task\n```", - level: "error", - raw: "You are not assigned to this task", - type: "error", - }, - metadata: { - caller: "error", - issueNumber: 5, - user: "ubiquity", - }, - }; - - try { - expect(await userStartStop(context as unknown as Context)).toThrow(logReturn); - } catch (error) { - expect(error).toEqual(logReturn); - } + await expect(userStartStop(context as unknown as Context)).rejects.toThrow("```diff\n! You are not assigned to this task\n```"); }); test("User can't start an issue that's already assigned", async () => { @@ -231,40 +195,6 @@ describe("User start/stop", () => { } }); - test("User can't start if command is disabled", async () => { - const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; - - const context = createContext(issue, sender, "/start"); - - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - - try { - await userStartStop(context as unknown as Context); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual("The '/start' command is disabled for this repository."); - } - } - }); - - test("User can't stop if command is disabled", async () => { - const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; - - const context = createContext(issue, sender, "/stop"); - - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - - try { - await userStartStop(context as unknown as Context); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual("The '/stop' command is disabled for this repository."); - } - } - }); - test("User can't start an issue that's a parent issue", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; From 8c9d33c9e1d1cb366ed61982490ef6d9982976c5 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:47:18 +0100 Subject: [PATCH 16/18] chore: sanitize metadata comment --- src/plugin.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/plugin.ts b/src/plugin.ts index 7c9b365..2117cec 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -32,9 +32,42 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { } else { errorMessage = context.logger.error(`Failed to run comment evaluation. ${err}`, { err }); } - await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); + + await addCommentToIssue(context, `${sanitizeDiff(errorMessage?.logMessage.diff)}\n`); } } else { context.logger.error(`Unsupported event: ${context.eventName}`); } } + +function sanitizeDiff(diff?: LogReturn["logMessage"]["diff"]): string { + if (!diff) return ""; + // eslint-disable-next-line no-useless-escape + const backticks = diff.match(/\`\`\`/g); + if (!backticks) return diff; + + // we need two sets at least and one must be at the end + + if (backticks.length < 2 || backticks.length % 2 !== 0) { + return diff; + } + + // does it end with a set of backticks? + if (diff.endsWith("```") || diff.endsWith("```\n")) { + return diff; + } + + return diff + "```"; +} + +function sanitizeMetadata(obj: LogReturn["metadata"]): string { + return JSON.stringify(obj, null, 2) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/--/g, "--") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\\/g, "\") + .replace(/\//g, "/"); +} From 178847011e5651f09c8075b6306f520514cc1261 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:54:36 +0100 Subject: [PATCH 17/18] chore: improve sanity checks --- src/plugin.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 2117cec..3a26800 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -44,20 +44,34 @@ function sanitizeDiff(diff?: LogReturn["logMessage"]["diff"]): string { if (!diff) return ""; // eslint-disable-next-line no-useless-escape const backticks = diff.match(/\`\`\`/g); - if (!backticks) return diff; + if (!backticks) return "```\n" + diff + "\n```"; + + const endsWith = diff.endsWith("```") || diff.endsWith("```\n") || diff.endsWith("``` ") // we need two sets at least and one must be at the end - if (backticks.length < 2 || backticks.length % 2 !== 0) { + if ((backticks.length === 2 || backticks.length % 2 === 0) && endsWith) { return diff; } // does it end with a set of backticks? - if (diff.endsWith("```") || diff.endsWith("```\n")) { - return diff; + if (diff.startsWith("```") && !endsWith) { + return diff + "\n```"; + } + + // does it start with a set of backticks? + + if (!diff.startsWith("```") && endsWith) { + return "```\n" + diff; + } + + // does it have a set of backticks in the middle? + + if (!diff.startsWith("```") && !endsWith) { + return "```\n" + diff + "\n```"; } - return diff + "```"; + return diff; } function sanitizeMetadata(obj: LogReturn["metadata"]): string { From 06127cb5b2739b126858b724add2ad823a6ed4e9 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:04:51 +0100 Subject: [PATCH 18/18] chore: log message --- src/plugin.ts | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 3a26800..617a319 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -33,55 +33,16 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { errorMessage = context.logger.error(`Failed to run comment evaluation. ${err}`, { err }); } - await addCommentToIssue(context, `${sanitizeDiff(errorMessage?.logMessage.diff)}\n`); + await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); } } else { context.logger.error(`Unsupported event: ${context.eventName}`); } } -function sanitizeDiff(diff?: LogReturn["logMessage"]["diff"]): string { - if (!diff) return ""; - // eslint-disable-next-line no-useless-escape - const backticks = diff.match(/\`\`\`/g); - if (!backticks) return "```\n" + diff + "\n```"; - - const endsWith = diff.endsWith("```") || diff.endsWith("```\n") || diff.endsWith("``` ") - - // we need two sets at least and one must be at the end - - if ((backticks.length === 2 || backticks.length % 2 === 0) && endsWith) { - return diff; - } - - // does it end with a set of backticks? - if (diff.startsWith("```") && !endsWith) { - return diff + "\n```"; - } - - // does it start with a set of backticks? - - if (!diff.startsWith("```") && endsWith) { - return "```\n" + diff; - } - - // does it have a set of backticks in the middle? - - if (!diff.startsWith("```") && !endsWith) { - return "```\n" + diff + "\n```"; - } - - return diff; -} - function sanitizeMetadata(obj: LogReturn["metadata"]): string { return JSON.stringify(obj, null, 2) - .replace(/&/g, "&") .replace(//g, ">") .replace(/--/g, "--") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\\/g, "\") - .replace(/\//g, "/"); }