From 5b176736d8678051779ee5e9a2c7822bfe4625fa Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 15 Aug 2024 01:37:35 +0100 Subject: [PATCH 01/76] fix: add validation to check for duplicate roles --- src/types/plugin-input.ts | 10 +++-- .../validate-schema-for-duplicate-roles.ts | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/utils/validate-schema-for-duplicate-roles.ts diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index c390fd69..55e0dfca 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,6 +1,7 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { StaticDecode, Type as T } from "@sinclair/typebox"; +import { Static, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; +import { validateSchemaForDuplicateRoles } from "../utils/validate-schema-for-duplicate-roles"; export interface PluginInputs { stateId: string; @@ -55,5 +56,8 @@ export const startStopSchema = T.Object({ ), }); -export type StartStopSettings = StaticDecode; -export const startStopSettingsValidator = new StandardValidator(startStopSchema); + +export const validatedStartStopSchema = validateSchemaForDuplicateRoles(startStopSchema); + +export type StartStopSettings = Static; +export const startStopSettingsValidator = new StandardValidator(validatedStartStopSchema); \ No newline at end of file diff --git a/src/utils/validate-schema-for-duplicate-roles.ts b/src/utils/validate-schema-for-duplicate-roles.ts new file mode 100644 index 00000000..11f64874 --- /dev/null +++ b/src/utils/validate-schema-for-duplicate-roles.ts @@ -0,0 +1,37 @@ +import { Static, TSchema } from "@sinclair/typebox"; +import { startStopSchema } from "../types"; + +class DuplicateRoleError extends Error { + constructor(message: string) { + super(message); + this.name = "DuplicateRoleError"; + } + } + + export function validateSchemaForDuplicateRoles(schema: T): T { + return { + ...schema, + decode(value: unknown) { + try { + const decodedValue = value as Static; + + const taskRoles = decodedValue.miscellaneous.maxConcurrentTasks.map((task) => task.role); + const uniqueRoles = new Set(taskRoles); + + if (taskRoles.length !== uniqueRoles.size) { + throw new DuplicateRoleError("Duplicate roles found in maxConcurrentTasks."); + } + + return decodedValue; + } catch (error) { + if (error instanceof DuplicateRoleError) { + console.error(error.message); + throw error; + } else { + console.error("An unexpected error occurred during decoding:", error); + throw error; + } + } + }, + } as T; + } \ No newline at end of file From 61424ec37e91dcdc40592e21e87729cdc6c672c2 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 15 Aug 2024 01:49:14 +0100 Subject: [PATCH 02/76] chore: update readme to match changes --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf3264f8..0300ac89 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,13 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu reviewDelayTolerance: 86000 taskStaleTimeoutDuration: 2580000 miscellaneous: - maxConcurrentTasks: 3 + maxConcurrentTasks: # default concurrent task limits per role + - role: Admin + limit: 10 + - role: Member + limit: 5 + - role: Collaborator + limit: 3 startRequiresWallet: true # default is true ``` From 07734d45426dcd5f5fc59a27fdb299561b4d0d47 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 15 Aug 2024 15:54:09 +0100 Subject: [PATCH 03/76] fix: user t.record and loop by object.entries --- README.md | 10 ++--- .../shared/get-user-task-limit-and-role.ts | 13 ++++++- src/types/plugin-input.ts | 31 +++++----------- .../validate-schema-for-duplicate-roles.ts | 37 ------------------- tests/main.test.ts | 21 +++-------- 5 files changed, 30 insertions(+), 82 deletions(-) delete mode 100644 src/utils/validate-schema-for-duplicate-roles.ts diff --git a/README.md b/README.md index 0300ac89..3856d910 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu reviewDelayTolerance: 86000 taskStaleTimeoutDuration: 2580000 miscellaneous: - maxConcurrentTasks: # default concurrent task limits per role - - role: Admin + maxConcurrentTasks: # Default concurrent task limits per role. + Admin: limit: 10 - - role: Member + Member: limit: 5 - - role: Collaborator + Contributor: limit: 3 - startRequiresWallet: true # default is true + startRequiresWallet: true # default is true ``` # Testing diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 4be9b5fa..b99e7a76 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -9,7 +9,11 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P const orgLogin = context.payload.organization?.login; const { config, logger } = context; const { maxConcurrentTasks } = config.miscellaneous; - const smallestTask = maxConcurrentTasks.reduce((minTask, currentTask) => (currentTask.limit < minTask.limit ? currentTask : minTask)); + + const smallestTask = Object.entries(maxConcurrentTasks).reduce( + (minTask, [role, { limit }]) => (limit < minTask.limit ? { role, limit } : minTask), + { role: "", limit: Infinity } as MatchingUserProps + ); try { const response = await context.octokit.orgs.getMembershipForUser({ @@ -17,7 +21,12 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P username: user, }); - return maxConcurrentTasks.find(({ role }) => role.toLowerCase() === response.data.role) ?? smallestTask; + const matchedTask = Object.entries(maxConcurrentTasks).find( + ([role]) => role.toLowerCase() === response.data.role.toLowerCase() + ); + + return matchedTask ? { role: matchedTask[0], limit: matchedTask[1].limit } : smallestTask; + } catch (err) { logger.error("Could not get user role", { err }); return smallestTask; diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 55e0dfca..c02517ca 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,7 +1,6 @@ import { SupportedEvents, SupportedEventsU } from "./context"; import { Static, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; -import { validateSchemaForDuplicateRoles } from "../utils/validate-schema-for-duplicate-roles"; export interface PluginInputs { stateId: string; @@ -27,37 +26,25 @@ export const startStopSchema = T.Object({ miscellaneous: T.Object( { startRequiresWallet: T.Boolean(), - maxConcurrentTasks: T.Array( + maxConcurrentTasks: T.Record( + T.String(), T.Object({ - role: userRoleSchema, limit: T.Integer(), }) ), }, { default: { - maxConcurrentTasks: [ - { - role: "Admin", - limit: 20, - }, - { - role: "Member", - limit: 10, - }, - { - role: "Contributor", - limit: 2, - }, - ], + maxConcurrentTasks: { + Admin: { limit: 20 }, + Member: { limit: 20 }, + Contributor: { limit: 2 }, + }, startRequiresWallet: true, }, } ), }); - -export const validatedStartStopSchema = validateSchemaForDuplicateRoles(startStopSchema); - -export type StartStopSettings = Static; -export const startStopSettingsValidator = new StandardValidator(validatedStartStopSchema); \ No newline at end of file +export type StartStopSettings = Static; +export const startStopSettingsValidator = new StandardValidator(startStopSchema); \ No newline at end of file diff --git a/src/utils/validate-schema-for-duplicate-roles.ts b/src/utils/validate-schema-for-duplicate-roles.ts deleted file mode 100644 index 11f64874..00000000 --- a/src/utils/validate-schema-for-duplicate-roles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Static, TSchema } from "@sinclair/typebox"; -import { startStopSchema } from "../types"; - -class DuplicateRoleError extends Error { - constructor(message: string) { - super(message); - this.name = "DuplicateRoleError"; - } - } - - export function validateSchemaForDuplicateRoles(schema: T): T { - return { - ...schema, - decode(value: unknown) { - try { - const decodedValue = value as Static; - - const taskRoles = decodedValue.miscellaneous.maxConcurrentTasks.map((task) => task.role); - const uniqueRoles = new Set(taskRoles); - - if (taskRoles.length !== uniqueRoles.size) { - throw new DuplicateRoleError("Duplicate roles found in maxConcurrentTasks."); - } - - return decodedValue; - } catch (error) { - if (error instanceof DuplicateRoleError) { - console.error(error.message); - throw error; - } else { - console.error("An unexpected error occurred during decoding:", error); - throw error; - } - } - }, - } as T; - } \ No newline at end of file diff --git a/tests/main.test.ts b/tests/main.test.ts index 325818b8..2b6b0781 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -560,21 +560,6 @@ const maxConcurrentDefaults = { contributor: 2, }; -const maxConcurrentTasks = [ - { - role: "Admin", - limit: 6, - }, - { - role: "Member", - limit: 4, - }, - { - role: "Collaborator", - limit: 2, - }, -]; - function createContext(issue: Record, sender: Record, body = "/start"): Context { return { adapters: {} as ReturnType, @@ -594,7 +579,11 @@ function createContext(issue: Record, sender: Record Date: Thu, 15 Aug 2024 17:55:10 +0100 Subject: [PATCH 04/76] fix: use role as key and limit as value --- README.md | 9 +++------ src/handlers/shared/get-user-task-limit-and-role.ts | 9 ++++----- src/types/plugin-input.ts | 13 ++++--------- tests/main.test.ts | 6 +++--- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3856d910..03947812 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,9 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu taskStaleTimeoutDuration: 2580000 miscellaneous: maxConcurrentTasks: # Default concurrent task limits per role. - Admin: - limit: 10 - Member: - limit: 5 - Contributor: - limit: 3 + Admin: 10 + Member: 5 + Contributor: 3 startRequiresWallet: true # default is true ``` diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index b99e7a76..871c69d8 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -11,7 +11,7 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P const { maxConcurrentTasks } = config.miscellaneous; const smallestTask = Object.entries(maxConcurrentTasks).reduce( - (minTask, [role, { limit }]) => (limit < minTask.limit ? { role, limit } : minTask), + (minTask, [role, limit]) => (limit < minTask.limit ? { role, limit } : minTask), { role: "", limit: Infinity } as MatchingUserProps ); @@ -21,11 +21,10 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P username: user, }); - const matchedTask = Object.entries(maxConcurrentTasks).find( - ([role]) => role.toLowerCase() === response.data.role.toLowerCase() - ); + const role = response.data.role.toLowerCase(); + const limit = maxConcurrentTasks[role.charAt(0).toUpperCase() + role.slice(1)]; - return matchedTask ? { role: matchedTask[0], limit: matchedTask[1].limit } : smallestTask; + return limit ? { role, limit } : smallestTask; } catch (err) { logger.error("Could not get user role", { err }); diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index c02517ca..4a552778 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -26,19 +26,14 @@ export const startStopSchema = T.Object({ miscellaneous: T.Object( { startRequiresWallet: T.Boolean(), - maxConcurrentTasks: T.Record( - T.String(), - T.Object({ - limit: T.Integer(), - }) - ), + maxConcurrentTasks: T.Record(T.String(), T.Integer()), }, { default: { maxConcurrentTasks: { - Admin: { limit: 20 }, - Member: { limit: 20 }, - Contributor: { limit: 2 }, + Admin: 20, + Member: 10, + Contributor: 2, }, startRequiresWallet: true, }, diff --git a/tests/main.test.ts b/tests/main.test.ts index 2b6b0781..052421ba 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -580,9 +580,9 @@ function createContext(issue: Record, sender: Record Date: Thu, 15 Aug 2024 22:24:31 +0100 Subject: [PATCH 05/76] fix: get rid of unnecesary operstions on the role --- src/handlers/shared/get-user-task-limit-and-role.ts | 4 ++-- tests/main.test.ts | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 871c69d8..8628c5d0 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -21,8 +21,8 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P username: user, }); - const role = response.data.role.toLowerCase(); - const limit = maxConcurrentTasks[role.charAt(0).toUpperCase() + role.slice(1)]; + const role = response.data.role + const limit = maxConcurrentTasks[role]; return limit ? { role, limit } : smallestTask; diff --git a/tests/main.test.ts b/tests/main.test.ts index 052421ba..8957ae89 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -579,11 +579,7 @@ function createContext(issue: Record, sender: Record Date: Sun, 18 Aug 2024 16:54:29 +0100 Subject: [PATCH 06/76] chore: use correct cases --- README.md | 6 +++--- src/handlers/shared/get-user-task-limit-and-role.ts | 2 +- src/types/plugin-input.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 03947812..a31d7db2 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu taskStaleTimeoutDuration: 2580000 miscellaneous: maxConcurrentTasks: # Default concurrent task limits per role. - Admin: 10 - Member: 5 - Contributor: 3 + admin: 10 + member: 5 + contributor: 3 startRequiresWallet: true # default is true ``` diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 8628c5d0..49800e68 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -21,7 +21,7 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P username: user, }); - const role = response.data.role + const role = response.data.role.toLowerCase() const limit = maxConcurrentTasks[role]; return limit ? { role, limit } : smallestTask; diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 4a552778..b6d363e4 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -31,9 +31,9 @@ export const startStopSchema = T.Object({ { default: { maxConcurrentTasks: { - Admin: 20, - Member: 10, - Contributor: 2, + admin: 20, + member: 10, + contributor: 2, }, startRequiresWallet: true, }, From 3fd93c19f886acfe658a9650fdeddace9ceb5ef6 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sun, 25 Aug 2024 00:50:31 +0100 Subject: [PATCH 07/76] Update src/types/plugin-input.ts Co-authored-by: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> --- src/types/plugin-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index b6d363e4..a1753ece 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -26,7 +26,7 @@ export const startStopSchema = T.Object({ miscellaneous: T.Object( { startRequiresWallet: T.Boolean(), - maxConcurrentTasks: T.Record(T.String(), T.Integer()), + maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: 20, member: 10, contributor: 2 } }), }, { default: { From 20fb5031e43f35c1747cee2fde058d3bd9a9e45b Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 27 Aug 2024 09:53:01 +0100 Subject: [PATCH 08/76] Update src/types/plugin-input.ts Co-authored-by: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> --- src/types/plugin-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index a1753ece..a3583431 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -25,7 +25,7 @@ export const startStopSchema = T.Object({ ), miscellaneous: T.Object( { - startRequiresWallet: T.Boolean(), + startRequiresWallet: T.Boolean({ default: true }}), maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: 20, member: 10, contributor: 2 } }), }, { From 2d559ed7c8d5c45e57754eba07ee795586889258 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 27 Aug 2024 10:29:46 +0100 Subject: [PATCH 09/76] Update src/types/plugin-input.ts Co-authored-by: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> --- src/types/plugin-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index a3583431..730b8ea6 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -29,7 +29,7 @@ export const startStopSchema = T.Object({ maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: 20, member: 10, contributor: 2 } }), }, { - default: { + default: {} maxConcurrentTasks: { admin: 20, member: 10, From 38978335977bccaaa806d6db957b241e71c772aa Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Wed, 28 Aug 2024 12:21:00 +0100 Subject: [PATCH 10/76] chore: revert to staticDecode --- src/types/plugin-input.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 730b8ea6..d3be12ba 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,5 +1,5 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { Static, Type as T } from "@sinclair/typebox"; +import { Static, StaticDecode, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; export interface PluginInputs { @@ -41,5 +41,5 @@ export const startStopSchema = T.Object({ ), }); -export type StartStopSettings = Static; +export type StartStopSettings = StaticDecode; export const startStopSettingsValidator = new StandardValidator(startStopSchema); \ No newline at end of file From 5faa1a0d82281585f8c0bdec7aa7c003274fa289 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Wed, 28 Aug 2024 12:52:02 +0100 Subject: [PATCH 11/76] chore: cleanup --- src/types/plugin-input.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index d3be12ba..87a4a7a3 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -25,11 +25,11 @@ export const startStopSchema = T.Object({ ), miscellaneous: T.Object( { - startRequiresWallet: T.Boolean({ default: true }}), + startRequiresWallet: T.Boolean({ default: true }), maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: 20, member: 10, contributor: 2 } }), }, { - default: {} + default: {}, maxConcurrentTasks: { admin: 20, member: 10, @@ -37,7 +37,6 @@ export const startStopSchema = T.Object({ }, startRequiresWallet: true, }, - } ), }); From 57d0089077207f6f2016deb6ef7d40959f196fd5 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 29 Aug 2024 17:37:51 +0100 Subject: [PATCH 12/76] chore: rebase to development --- src/adapters/supabase/helpers/user.ts | 2 +- .../shared/get-user-task-limit-and-role.ts | 2 +- src/handlers/shared/start.ts | 6 +-- src/types/plugin-input.ts | 42 +++++---------- src/utils/issue.ts | 17 +++++-- tests/main.test.ts | 51 ++++++++----------- 6 files changed, 51 insertions(+), 69 deletions(-) diff --git a/src/adapters/supabase/helpers/user.ts b/src/adapters/supabase/helpers/user.ts index 3396ad24..635e4ff9 100644 --- a/src/adapters/supabase/helpers/user.ts +++ b/src/adapters/supabase/helpers/user.ts @@ -16,7 +16,7 @@ export class User extends Super { const { data, error } = (await this.supabase.from("users").select("wallets(*)").eq("id", userId).single()) as { data: { wallets: Wallet }; error: unknown }; if ((error && !data) || !data.wallets?.address) { this.context.logger.error("No wallet address found", { userId, issueNumber }); - if (this.context.config.miscellaneous.startRequiresWallet) { + if (this.context.config.startRequiresWallet) { await addCommentToIssue(this.context, "```diff\n! Please set your wallet address with the /wallet command first and try again.\n```"); throw new Error("No wallet address found"); } else { diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 49800e68..98ba3e8a 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -8,7 +8,7 @@ interface MatchingUserProps { export async function getUserRoleAndTaskLimit(context: Context, user: string): Promise { const orgLogin = context.payload.organization?.login; const { config, logger } = context; - const { maxConcurrentTasks } = config.miscellaneous; + const { maxConcurrentTasks } = config; const smallestTask = Object.entries(maxConcurrentTasks).reduce( (minTask, [role, limit]) => (limit < minTask.limit ? { role, limit } : minTask), diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 8ee05887..de1d4d72 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,5 +1,5 @@ import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; -import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue } from "../../utils/issue"; +import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue"; import { calculateDurations } from "../../utils/shared"; import { checkTaskStale } from "./check-task-stale"; import { generateAssignmentComment } from "./generate-assignment-comment"; @@ -9,7 +9,7 @@ import { assignTableComment } from "./table"; export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"]) { const { logger, config } = context; - const { taskStaleTimeoutDuration } = config.timers; + const { taskStaleTimeoutDuration } = config; const maxTask = await getUserRoleAndTaskLimit(context, sender.login); // is it a child issue? @@ -92,7 +92,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] await addAssignees(context, issue.number, [login]); } - const isTaskStale = checkTaskStale(taskStaleTimeoutDuration, issue.created_at); + const isTaskStale = checkTaskStale(getTimeValue(taskStaleTimeoutDuration), issue.created_at); await addCommentToIssue( context, diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 87a4a7a3..48d77b31 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,5 +1,5 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { Static, StaticDecode, Type as T } from "@sinclair/typebox"; +import { StaticDecode, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; export interface PluginInputs { @@ -10,35 +10,17 @@ export interface PluginInputs; export const startStopSettingsValidator = new StandardValidator(startStopSchema); \ No newline at end of file diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 3caf1360..c99dbc98 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,3 +1,4 @@ +import ms from "ms"; import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; @@ -167,7 +168,7 @@ export async function getAllPullRequestReviews(context: Context, pullNumber: num } export async function getAvailableOpenedPullRequests(context: Context, username: string) { - const { reviewDelayTolerance } = context.config.timers; + const { reviewDelayTolerance } = context.config; if (!reviewDelayTolerance) return []; const openedPullRequests = await getOpenedPullRequests(context, username); @@ -184,14 +185,24 @@ export async function getAvailableOpenedPullRequests(context: Context, username: result.push(openedPullRequest); } } - - if (reviews.length === 0 && (new Date().getTime() - new Date(openedPullRequest.created_at).getTime()) / (1000 * 60 * 60) >= reviewDelayTolerance) { + + if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.created_at).getTime() >= getTimeValue(reviewDelayTolerance)) { result.push(openedPullRequest); } } return result; } +export function getTimeValue(timeString: string): number { + const timeValue = ms(timeString); + + if (!timeValue || timeValue <= 0 || isNaN(timeValue)) { + throw new Error("Invalid config time value"); + } + + return timeValue; +} + async function getOpenedPullRequests(context: Context, username: string): Promise> { const prs = await getAllPullRequests(context, "open", username); return prs.filter((pr) => pr.pull_request && pr.state === "open"); diff --git a/tests/main.test.ts b/tests/main.test.ts index 8957ae89..cf3b54ee 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -59,7 +59,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"); @@ -85,7 +85,6 @@ describe("User start/stop", () => { const context = createContext(issue, sender, "/stop"); context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const output = await userStartStop(context as unknown as Context); expect(output).toEqual({ output: "You are not assigned to this task" }); @@ -98,7 +97,6 @@ describe("User start/stop", () => { const context = createContext(issue, sender, "/stop"); context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const output = await userStartStop(context as unknown as Context); expect(output).toEqual({ output: "You are not assigned to this task" }); @@ -109,9 +107,7 @@ describe("User start/stop", () => { 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); - const err = "Issue is already assigned"; try { @@ -126,13 +122,9 @@ describe("User start/stop", () => { test("User can't start an issue without a price label", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 3 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; - const context = createContext(issue, sender); - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const err = "No price label is set to calculate the duration"; - try { await userStartStop(context as unknown as Context); } catch (error) { @@ -249,10 +241,10 @@ describe("User start/stop", () => { const sender = db.users.findFirst({ where: { id: { equals: 5 } } }) as unknown as Sender; const memberLimit = maxConcurrentDefaults.member; - // (+ 4) because we have 4 open pull requests being pulled too - // ``Math.abs(assignedIssues.length - openedPullRequests.length) >= maxTask.limit)`` + createIssuesForMaxAssignment(memberLimit + 4, sender.id); const context = createContext(issue, sender) as unknown as Context; + context.adapters = createAdapters(getSupabase(), context as unknown as Context); await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${memberLimit} issues.`); @@ -264,11 +256,12 @@ describe("User start/stop", () => { const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Sender; const adminLimit = maxConcurrentDefaults.admin; - // (+ 4) because we have 4 open pull requests being pulled too - // ``Math.abs(assignedIssues.length - openedPullRequests.length) >= maxTask.limit)`` + createIssuesForMaxAssignment(adminLimit + 4, sender.id); const context = createContext(issue, sender) as unknown as Context; + context.adapters = createAdapters(getSupabase(), context as unknown as Context); + await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${adminLimit} issues.`); expect(adminLimit).toEqual(6); @@ -412,8 +405,8 @@ async function setupTests() { }, body: "Pull request body", owner: "ubiquity", - pull_request: {}, repo: "test-repo", + pull_request: {}, state: "open", closed_at: null, }); @@ -574,14 +567,10 @@ function createContext(issue: Record, sender: Record Date: Tue, 13 Aug 2024 07:33:20 +0100 Subject: [PATCH 13/76] chore: refactor isTaskStale using ms --- src/handlers/shared/check-task-stale.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/handlers/shared/check-task-stale.ts b/src/handlers/shared/check-task-stale.ts index b043bee2..47e79a9e 100644 --- a/src/handlers/shared/check-task-stale.ts +++ b/src/handlers/shared/check-task-stale.ts @@ -1,9 +1,19 @@ -export function checkTaskStale(staleTask: number, createdAt: string) { - if (staleTask !== 0) { - const days = Math.floor((new Date().getTime() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24)); - const staleToDays = Math.floor(staleTask / (1000 * 60 * 60 * 24)); - return days >= staleToDays && staleToDays > 0; - } +import ms from 'ms'; - return false; +function calculateDaysDifference(date1: Date, date2: Date): number { + const millisecondsPerDay = ms('1d'); + return Math.floor((date1.getTime() - date2.getTime()) / millisecondsPerDay); } + +export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string): boolean { + if (staleTaskMilliseconds === 0) { + return false; + } + + const currentDate = new Date(); + const createdDate = new Date(createdAt); + const daysSinceCreation = calculateDaysDifference(currentDate, createdDate); + const staleDaysThreshold = Math.floor(staleTaskMilliseconds / ms('1d')); + + return daysSinceCreation >= staleDaysThreshold && staleDaysThreshold > 0; +} \ No newline at end of file From 1b4d50961e369cb496ec59a23d8f090498d943aa Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 13 Aug 2024 07:44:58 +0100 Subject: [PATCH 14/76] chore: eslint --- src/handlers/shared/check-task-stale.ts | 8 ++++---- tests/main.test.ts | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/handlers/shared/check-task-stale.ts b/src/handlers/shared/check-task-stale.ts index 47e79a9e..ae8a33e9 100644 --- a/src/handlers/shared/check-task-stale.ts +++ b/src/handlers/shared/check-task-stale.ts @@ -1,7 +1,7 @@ -import ms from 'ms'; +import ms from "ms"; function calculateDaysDifference(date1: Date, date2: Date): number { - const millisecondsPerDay = ms('1d'); + const millisecondsPerDay = ms("1d"); return Math.floor((date1.getTime() - date2.getTime()) / millisecondsPerDay); } @@ -13,7 +13,7 @@ export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string) const currentDate = new Date(); const createdDate = new Date(createdAt); const daysSinceCreation = calculateDaysDifference(currentDate, createdDate); - const staleDaysThreshold = Math.floor(staleTaskMilliseconds / ms('1d')); + const staleDaysThreshold = Math.floor(staleTaskMilliseconds / ms("1d")); return daysSinceCreation >= staleDaysThreshold && staleDaysThreshold > 0; -} \ No newline at end of file +} diff --git a/tests/main.test.ts b/tests/main.test.ts index cf3b54ee..105656c6 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -59,7 +59,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"); @@ -588,17 +588,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 b7fc11b07df40849503c3a0f8d22f5fef07c001c Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:19:54 +0100 Subject: [PATCH 15/76] chore: fix default, refactor isTaskStale --- src/handlers/shared/check-task-stale.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/handlers/shared/check-task-stale.ts b/src/handlers/shared/check-task-stale.ts index ae8a33e9..71b1ee03 100644 --- a/src/handlers/shared/check-task-stale.ts +++ b/src/handlers/shared/check-task-stale.ts @@ -1,10 +1,3 @@ -import ms from "ms"; - -function calculateDaysDifference(date1: Date, date2: Date): number { - const millisecondsPerDay = ms("1d"); - return Math.floor((date1.getTime() - date2.getTime()) / millisecondsPerDay); -} - export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string): boolean { if (staleTaskMilliseconds === 0) { return false; @@ -12,8 +5,7 @@ export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string) const currentDate = new Date(); const createdDate = new Date(createdAt); - const daysSinceCreation = calculateDaysDifference(currentDate, createdDate); - const staleDaysThreshold = Math.floor(staleTaskMilliseconds / ms("1d")); + const millisecondsSinceCreation = currentDate.getTime() - createdDate.getTime(); - return daysSinceCreation >= staleDaysThreshold && staleDaysThreshold > 0; + return millisecondsSinceCreation >= staleTaskMilliseconds && staleTaskMilliseconds > 0; } From e834a7f54031f8bcdde139474947b8cb8fa2d860 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:36:40 +0100 Subject: [PATCH 16/76] chore: remove redundant check --- src/handlers/shared/check-task-stale.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/shared/check-task-stale.ts b/src/handlers/shared/check-task-stale.ts index 71b1ee03..d9103548 100644 --- a/src/handlers/shared/check-task-stale.ts +++ b/src/handlers/shared/check-task-stale.ts @@ -7,5 +7,5 @@ export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string) const createdDate = new Date(createdAt); const millisecondsSinceCreation = currentDate.getTime() - createdDate.getTime(); - return millisecondsSinceCreation >= staleTaskMilliseconds && staleTaskMilliseconds > 0; + return millisecondsSinceCreation >= staleTaskMilliseconds } From 874fa10a07129a12bb15b1f1fce40cdb2cd03014 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 17/76] chore: teamate assignment --- src/handlers/shared/start.ts | 5 ++--- src/handlers/user-start-stop.ts | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index de1d4d72..855e8a10 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -7,7 +7,7 @@ import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; 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 { taskStaleTimeoutDuration } = config; const maxTask = await getUserRoleAndTaskLimit(context, sender.login); @@ -36,7 +36,6 @@ 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`, { openedPullRequests: openedPullRequests.map((pr) => pr.html_url), @@ -82,7 +81,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 46148f3e..23b961f8 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 3e7a5861cf10edc8eb5cc2bed00062590ecca8f5 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 18/76] 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 187f8087..205afaef 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 7a34e68d99b830e2fc3f827fdd9e53c8859326b4 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 19/76] chore: teams test --- src/handlers/shared/start.ts | 9 +++++++- tests/main.test.ts | 40 +++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 855e8a10..bad7e255 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -81,7 +81,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 105656c6..ab318cb3 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -45,6 +45,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; @@ -59,7 +78,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"); @@ -466,6 +485,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, }, @@ -588,17 +608,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 b6f24900899994ffdda9b44c2b2371e75ab43437 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 20/76] 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 bad7e255..72493e11 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -62,13 +62,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 535b4f5efe9eae2487aefc6c68ac3815cb98836d 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 21/76] chore: max assigned check all users --- src/handlers/shared/start.ts | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 72493e11..b27bd6f6 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -60,13 +60,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 @@ -117,3 +127,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 From cb28796696a2e1e4c0d15e59e3c29d87515c2259 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 22/76] chore: fix eslint naming convention --- src/handlers/shared/start.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index b27bd6f6..01014391 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -60,18 +60,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) { @@ -142,4 +139,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 +} From 3826f9ca1d77a71e0ec611a8f45284e81d6e6037 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 23/76] chore: update logs and test --- src/handlers/shared/start.ts | 36 +++++++++++++++++++++------------ src/handlers/user-start-stop.ts | 5 ++++- src/utils/get-linked-prs.ts | 3 +-- tests/__mocks__/handlers.ts | 2 +- tests/main.test.ts | 23 +++++++++------------ 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 01014391..7ccb00a5 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -64,8 +64,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); @@ -93,7 +97,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, }); @@ -101,11 +105,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(getTimeValue(taskStaleTimeoutDuration), issue.created_at); await addCommentToIssue( @@ -127,16 +137,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 23b961f8..a7c8815b 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 205afaef..0ffd852a 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 f4d7db30..54bc1d9d 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -80,7 +80,7 @@ export const handlers = [ db.issue.update({ where: { id: { equals: issue.id } }, data: { - assignees, + assignees: [...issue.assignees, ...assignees], }, }); } diff --git a/tests/main.test.ts b/tests/main.test.ts index ab318cb3..09584619 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -58,9 +58,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"])); }); @@ -78,7 +76,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"); @@ -485,7 +483,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, }, @@ -608,17 +605,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 a9236a892da5ebf5155cd95e481ac45a4efe749e 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 24/76] chore: remove redunant logic --- src/handlers/shared/start.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 7ccb00a5..57084cf2 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -62,6 +62,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( @@ -76,7 +77,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); } @@ -105,16 +105,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(getTimeValue(taskStaleTimeoutDuration), issue.created_at); From 402666c673b97c0fcd421c8051d777ed250c85b9 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 25/76] chore: throw errors and paginate --- src/handlers/shared/start.ts | 5 ++--- src/handlers/shared/stop.ts | 18 +++++++++++------- src/utils/issue.ts | 15 ++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 57084cf2..77175b25 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -92,13 +92,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 4f101867..5bce080e 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 c99dbc98..03312b30 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -28,8 +28,7 @@ export async function getAssignedIssues(context: Context, username: string): Pro }) ); } 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 }); } } @@ -48,7 +47,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 }); } } @@ -64,7 +63,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 }); } } @@ -123,7 +122,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, @@ -149,8 +148,7 @@ export async function getAllPullRequests( sort: "created", })) as GitHubIssueSearch["items"]; } 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 }); } } @@ -162,8 +160,7 @@ export async function getAllPullRequestReviews(context: Context, pullNumber: num pull_number: pullNumber, })) 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 829a1342ed716c062ec25d4d010142cc4c285c1d 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 26/76] 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 03312b30..0b5d793e 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -118,6 +118,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; @@ -131,6 +162,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( diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index 54bc1d9d..e06c6808 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -110,4 +110,12 @@ export const handlers = [ return HttpResponse.json(db.pull.getAll()); } }), + // 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 9a00cd02ac32ab85acd7c2e3ef1ce619c16867a8 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 27/76] chore: use octokit.rest --- src/handlers/shared/start.ts | 2 +- src/utils/get-linked-prs.ts | 2 +- src/utils/issue.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 77175b25..352ca426 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -25,7 +25,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 0ffd852a..5614ce54 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 0b5d793e..c7c322f4 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -40,7 +40,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, @@ -129,7 +129,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, @@ -153,7 +153,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 53fa77b8cf57a50d503e942eb2b0d039ae5607d4 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 28/76] 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 352ca426..307fd74e 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -19,7 +19,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; @@ -55,9 +55,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 ?? []; @@ -65,12 +63,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); @@ -85,9 +81,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; @@ -132,12 +126,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 5bce080e..633b0997 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 ec2bf094..64ae6214 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 c7c322f4..e1365e9a 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -70,8 +70,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, { @@ -136,13 +138,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 792627a59c5076d6a858554c410ef52aa242337d 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 29/76] 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 64ae6214..7c9b3650 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 ebff871b800dfbc679a6b4b06e5de5d209a43d52 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 30/76] chore: update tests --- tests/main.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index 09584619..6251bdd1 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"]; @@ -112,7 +112,6 @@ 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); From 087f088eb58d6973fab216841c86482daf764a83 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 31/76] chore: fix test by throwing Error --- src/handlers/shared/stop.ts | 2 +- tests/main.test.ts | 36 +----------------------------------- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 633b0997..3a2541e4 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 6251bdd1..67c652fc 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"]; @@ -184,40 +184,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 0708d75c3a3680490094fa84735e93b7c667d7d6 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 32/76] 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 7c9b3650..2117cec1 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 6405b1aa3d020adbaf905534644019c8d65fdd0c 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 33/76] 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 2117cec1..3a26800b 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 fc97c1db01a505086e58420243fa9c5f5f91ed39 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 34/76] 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 3a26800b..617a319f 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, "/"); } From 059010ca35b67e4bce2d0c184cae5e68bb4a914a Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:12:37 +0100 Subject: [PATCH 35/76] chore: bump logger and fix bubble-up error comment --- package.json | 2 +- src/handlers/shared/check-task-stale.ts | 2 +- src/plugin.ts | 7 ++++--- yarn.lock | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 879fce61..a52f1cf5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@octokit/webhooks": "13.2.7", "@sinclair/typebox": "^0.32.5", "@supabase/supabase-js": "2.42.0", - "@ubiquity-dao/ubiquibot-logger": "^1.3.0", + "@ubiquity-dao/ubiquibot-logger": "^1.3.1", "dotenv": "^16.4.4", "ms": "^2.1.3", "typebox-validators": "^0.3.5" diff --git a/src/handlers/shared/check-task-stale.ts b/src/handlers/shared/check-task-stale.ts index d9103548..55be3f76 100644 --- a/src/handlers/shared/check-task-stale.ts +++ b/src/handlers/shared/check-task-stale.ts @@ -7,5 +7,5 @@ export function checkTaskStale(staleTaskMilliseconds: number, createdAt: string) const createdDate = new Date(createdAt); const millisecondsSinceCreation = currentDate.getTime() - createdDate.getTime(); - return millisecondsSinceCreation >= staleTaskMilliseconds + return millisecondsSinceCreation >= staleTaskMilliseconds; } diff --git a/src/plugin.ts b/src/plugin.ts index 617a319f..90d3918b 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -28,11 +28,12 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { } catch (err) { let errorMessage; if (err instanceof LogReturn) { - errorMessage = context.logger.error(`Failed to run comment evaluation. ${err.logMessage?.raw || err}`, { err }); + errorMessage = err; + } else if (err instanceof Error) { + errorMessage = context.logger.error(err.message, { error: err }); } else { - errorMessage = context.logger.error(`Failed to run comment evaluation. ${err}`, { err }); + errorMessage = context.logger.error("An error occurred", { err }); } - await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n`); } } else { diff --git a/yarn.lock b/yarn.lock index 3027fb95..41474912 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2787,7 +2787,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/ms": "npm:^0.7.34" "@types/node": "npm:20.14.5" - "@ubiquity-dao/ubiquibot-logger": "npm:^1.3.0" + "@ubiquity-dao/ubiquibot-logger": "npm:^1.3.1" cspell: "npm:8.9.0" dotenv: "npm:^16.4.4" eslint: "npm:9.5.0" @@ -2813,10 +2813,10 @@ __metadata: languageName: unknown linkType: soft -"@ubiquity-dao/ubiquibot-logger@npm:^1.3.0": - version: 1.3.0 - resolution: "@ubiquity-dao/ubiquibot-logger@npm:1.3.0" - checksum: 10c0/9150b4a633c4f49b9a5d87dce8ada521ca1a98b4cb5129209abf8bfb0a99e2a035cf42705792b453081d9476931167c5cc352be7fc70ece8e94d2cfba9a00408 +"@ubiquity-dao/ubiquibot-logger@npm:^1.3.1": + version: 1.3.1 + resolution: "@ubiquity-dao/ubiquibot-logger@npm:1.3.1" + checksum: 10c0/b6a4f2171e70126c12f1f7c0f18fe670597bf31e9c0dc19ecfe442a9118401c799a35be32e1d4785f23d0e9c1c6c0be2548769b958a5ed198d849f11520d0c57 languageName: node linkType: hard From 1ab9112bcd9789babbfdc5111c603f0726148532 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:18:10 +0100 Subject: [PATCH 36/76] fix: hide deadline if no time label --- src/handlers/shared/generate-assignment-comment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/shared/generate-assignment-comment.ts b/src/handlers/shared/generate-assignment-comment.ts index a38477bd..194e73df 100644 --- a/src/handlers/shared/generate-assignment-comment.ts +++ b/src/handlers/shared/generate-assignment-comment.ts @@ -15,11 +15,11 @@ export async function generateAssignmentComment(context: Context, issueCreatedAt let endTime: null | Date = null; let deadline: null | string = null; endTime = new Date(startTime + duration * 1000); - deadline = endTime.toLocaleString("en-US", options); + deadline = endTime.toLocaleString("en-US", options) return { daysElapsedSinceTaskCreation: Math.floor((startTime - new Date(issueCreatedAt).getTime()) / 1000 / 60 / 60 / 24), - deadline, + deadline: duration > 0 ? deadline : null, registeredWallet: (await context.adapters.supabase.user.getWalletByUserId(senderId, issueNumber)) || "Register your wallet address using the following slash command: `/wallet 0x0000...0000`", From 84a6d26b702e35ae3dad471fd054d44bfa3f8716 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:10:47 +0100 Subject: [PATCH 37/76] chore: formatting --- src/handlers/shared/generate-assignment-comment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/shared/generate-assignment-comment.ts b/src/handlers/shared/generate-assignment-comment.ts index 194e73df..7c35c6e9 100644 --- a/src/handlers/shared/generate-assignment-comment.ts +++ b/src/handlers/shared/generate-assignment-comment.ts @@ -15,7 +15,7 @@ export async function generateAssignmentComment(context: Context, issueCreatedAt let endTime: null | Date = null; let deadline: null | string = null; endTime = new Date(startTime + duration * 1000); - deadline = endTime.toLocaleString("en-US", options) + deadline = endTime.toLocaleString("en-US", options); return { daysElapsedSinceTaskCreation: Math.floor((startTime - new Date(issueCreatedAt).getTime()) / 1000 / 60 / 60 / 24), From 13e0b4cc9235138b442a2ff0a3b1900646f4e8c5 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 28 Aug 2024 21:28:41 +0900 Subject: [PATCH 38/76] feat: added worker deploy / delete capabilities --- .github/workflows/worker-delete.yml | 44 +++++++++++++++++++++++++++++ .github/workflows/worker-deploy.yml | 27 +++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/worker-delete.yml diff --git a/.github/workflows/worker-delete.yml b/.github/workflows/worker-delete.yml new file mode 100644 index 00000000..89d3e23f --- /dev/null +++ b/.github/workflows/worker-delete.yml @@ -0,0 +1,44 @@ +name: Delete Deployment + +on: + delete: + +jobs: + delete: + runs-on: ubuntu-latest + name: Delete Deployment + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20.10.0" + + - name: Enable corepack + run: corepack enable + + - uses: actions/checkout@v4 + + - name: Get Deleted Branch Name + id: get_branch + run: | + branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g') + echo "branch_name=$branch_name" >> $GITHUB_ENV + - name: Retrieve and Construct Full Worker Name + id: construct_worker_name + run: | + base_name=$(grep '^name = ' wrangler.toml | sed 's/^name = "\(.*\)"$/\1/') + full_worker_name="${base_name}-${{ env.branch_name }}" + # Make sure that it doesnt exceed 63 characters or it will break RFC 1035 + full_worker_name=$(echo "${full_worker_name}" | cut -c 1-63) + echo "full_worker_name=$full_worker_name" >> $GITHUB_ENV + - name: Delete Deployment with Wrangler + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: delete --name ${{ env.full_worker_name }} + + - name: Output Deletion Result + run: | + echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY + echo 'Deployment `${{ env.full_worker_name }}` has been deleted.' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/worker-deploy.yml b/.github/workflows/worker-deploy.yml index 37d64231..0a3942c1 100644 --- a/.github/workflows/worker-deploy.yml +++ b/.github/workflows/worker-deploy.yml @@ -1,9 +1,8 @@ -name: Deploy +name: Deploy Worker on: push: - branches: - - main + workflow_dispatch: jobs: deploy: @@ -19,7 +18,22 @@ jobs: run: corepack enable - uses: actions/checkout@v4 - - uses: cloudflare/wrangler-action@v3 + + - name: Update wrangler.toml Name Field + run: | + branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g') + # Extract base name from wrangler.toml + base_name=$(grep '^name = ' wrangler.toml | sed 's/^name = "\(.*\)"$/\1/') + # Concatenate branch name with base name + new_name="${base_name}-${branch_name}" + # Truncate the new name to 63 characters for RFC 1035 + new_name=$(echo "$new_name" | cut -c 1-63) + # Update the wrangler.toml file + sed -i "s/^name = .*/name = \"$new_name\"/" wrangler.toml + echo "Updated wrangler.toml name to: $new_name" + - name: Deploy with Wrangler + id: wrangler_deploy + uses: cloudflare/wrangler-action@v3 with: wranglerVersion: "3.57.0" apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} @@ -30,3 +44,8 @@ jobs: env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + + - name: Write Deployment URL to Summary + run: | + echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.wrangler_deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY From 8d5c56a65c6f1fb586fd1d1d518c876db5516f47 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Sun, 14 Jul 2024 22:53:00 +0100 Subject: [PATCH 39/76] feat: max task assignment for collaborators --- src/handlers/shared/check-org-member.ts | 9 +++++++++ src/handlers/shared/start.ts | 1 + 2 files changed, 10 insertions(+) create mode 100644 src/handlers/shared/check-org-member.ts diff --git a/src/handlers/shared/check-org-member.ts b/src/handlers/shared/check-org-member.ts new file mode 100644 index 00000000..8b7edfab --- /dev/null +++ b/src/handlers/shared/check-org-member.ts @@ -0,0 +1,9 @@ +import { Context } from "../../types"; + +export async function isUserMember(context: Context, user: string) { + const response = await context.octokit.orgs.listMembers({ + org: context.payload.organization?.login as string, + }); + const members = response.data.map((member) => member.login); + return members.includes(user); +} diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 307fd74e..7b6e6fe0 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,6 +1,7 @@ import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue"; import { calculateDurations } from "../../utils/shared"; +import { isUserMember } from "./check-org-member"; import { checkTaskStale } from "./check-task-stale"; import { generateAssignmentComment } from "./generate-assignment-comment"; import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; From 9b7bbdf618cd39fd6337b44dfa9221745a57e5f2 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Mon, 15 Jul 2024 21:37:07 +0100 Subject: [PATCH 40/76] feat: make task limits configurable --- src/handlers/shared/check-org-member.ts | 9 -------- src/handlers/shared/get-user-role.ts | 28 +++++++++++++++++++++++++ src/handlers/shared/start.ts | 1 - 3 files changed, 28 insertions(+), 10 deletions(-) delete mode 100644 src/handlers/shared/check-org-member.ts create mode 100644 src/handlers/shared/get-user-role.ts diff --git a/src/handlers/shared/check-org-member.ts b/src/handlers/shared/check-org-member.ts deleted file mode 100644 index 8b7edfab..00000000 --- a/src/handlers/shared/check-org-member.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Context } from "../../types"; - -export async function isUserMember(context: Context, user: string) { - const response = await context.octokit.orgs.listMembers({ - org: context.payload.organization?.login as string, - }); - const members = response.data.map((member) => member.login); - return members.includes(user); -} diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts new file mode 100644 index 00000000..acac83ef --- /dev/null +++ b/src/handlers/shared/get-user-role.ts @@ -0,0 +1,28 @@ +import { Context } from "../../types"; + +export async function getUserRole(context: Context, user: string): Promise { + const orgLogin = context.payload.organization?.login; + + if (!orgLogin) { + throw new Error("Organization login not found in context payload."); + } + + try { + const response = await context.octokit.orgs.getMembershipForUser({ + org: orgLogin, + username: user, + }); + + const role = response.data.role; + + if (role === "admin") { + return "admin"; + } else if (role === "member") { + return "member"; + } else { + return "contributor"; + } + } catch (error) { + return "contributor"; + } +} diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 7b6e6fe0..307fd74e 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,7 +1,6 @@ import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue"; import { calculateDurations } from "../../utils/shared"; -import { isUserMember } from "./check-org-member"; import { checkTaskStale } from "./check-task-stale"; import { generateAssignmentComment } from "./generate-assignment-comment"; import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; From 32b045672108106c780dff00b3c793461467211b Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Mon, 22 Jul 2024 10:02:49 +0100 Subject: [PATCH 41/76] fix: get contributors from config --- src/handlers/shared/get-user-role.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts index acac83ef..36bd5988 100644 --- a/src/handlers/shared/get-user-role.ts +++ b/src/handlers/shared/get-user-role.ts @@ -13,15 +13,7 @@ export async function getUserRole(context: Context, user: string): Promise Date: Mon, 22 Jul 2024 17:10:37 +0100 Subject: [PATCH 42/76] fix: remove union type --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index a52f1cf5..be4682a2 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,5 @@ "extends": [ "@commitlint/config-conventional" ] - }, - "packageManager": "yarn@4.2.2" + } } From b4696c87462a52f0eb126a376c1fb3357770560a Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 23 Jul 2024 15:50:23 +0100 Subject: [PATCH 43/76] fix: return lowest task limit if user role doesnt match any of those in the config --- package.json | 3 ++- src/handlers/shared/get-user-role.ts | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index be4682a2..a52f1cf5 100644 --- a/package.json +++ b/package.json @@ -83,5 +83,6 @@ "extends": [ "@commitlint/config-conventional" ] - } + }, + "packageManager": "yarn@4.2.2" } diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts index 36bd5988..78011e1e 100644 --- a/src/handlers/shared/get-user-role.ts +++ b/src/handlers/shared/get-user-role.ts @@ -2,19 +2,26 @@ import { Context } from "../../types"; export async function getUserRole(context: Context, user: string): Promise { const orgLogin = context.payload.organization?.login; + const { config } = context; + const { maxConcurrentTasks } = config.miscellaneous; if (!orgLogin) { throw new Error("Organization login not found in context payload."); } - try { - const response = await context.octokit.orgs.getMembershipForUser({ - org: orgLogin, - username: user, - }); + const response = await context.octokit.orgs.getMembershipForUser({ + org: orgLogin, + username: user, + }); - return response.data.role; - } catch (error) { - return "contributor"; + const matchingUser = maxConcurrentTasks.find(({ role }) => role.toLowerCase() === response.data.role); + + if (matchingUser) { + //chech if the current user role matches any of those defined in the config + return matchingUser.role; + } else { + //return the role with the smallest task limit + const minLimitTask = maxConcurrentTasks.reduce((minTask, currentTask) => (currentTask.limit < minTask.limit ? currentTask : minTask)); + return minLimitTask.role; } } From c251f5b75c71fa95ba5e3433ec2215a6513980ae Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 23 Jul 2024 18:01:55 +0100 Subject: [PATCH 44/76] chore: code cleanups --- src/handlers/shared/get-user-role.ts | 40 +++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts index 78011e1e..9af8500d 100644 --- a/src/handlers/shared/get-user-role.ts +++ b/src/handlers/shared/get-user-role.ts @@ -1,27 +1,31 @@ import { Context } from "../../types"; -export async function getUserRole(context: Context, user: string): Promise { +interface MatchingUserProps { + role: string; + limit: number; +} + +export async function getUserRole(context: Context, user: string): Promise { const orgLogin = context.payload.organization?.login; - const { config } = context; + const { config, logger } = context; const { maxConcurrentTasks } = config.miscellaneous; - if (!orgLogin) { - throw new Error("Organization login not found in context payload."); - } - - const response = await context.octokit.orgs.getMembershipForUser({ - org: orgLogin, - username: user, - }); + try { + const response = await context.octokit.orgs.getMembershipForUser({ + org: orgLogin as string, + username: user, + }); - const matchingUser = maxConcurrentTasks.find(({ role }) => role.toLowerCase() === response.data.role); + const matchingUser = maxConcurrentTasks.find(({ role }) => role.toLowerCase() === response.data.role); - if (matchingUser) { - //chech if the current user role matches any of those defined in the config - return matchingUser.role; - } else { - //return the role with the smallest task limit - const minLimitTask = maxConcurrentTasks.reduce((minTask, currentTask) => (currentTask.limit < minTask.limit ? currentTask : minTask)); - return minLimitTask.role; + if (matchingUser) { + //chech if the current user role matches any of those defined in the config + return matchingUser; + } else { + //return the role with the smallest task limit + return maxConcurrentTasks.reduce((minTask, currentTask) => (currentTask.limit < minTask.limit ? currentTask : minTask)); + } + } catch (error) { + logger.error("An error occured", { error: error as Error }); } } From dce85312d6bee0f06cec437ade976d2e104f7ce7 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 23 Jul 2024 19:11:53 +0100 Subject: [PATCH 45/76] test: add test to cover functionality --- src/handlers/shared/get-user-role.ts | 1 + tests/main.test.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts index 9af8500d..2bd1b3d9 100644 --- a/src/handlers/shared/get-user-role.ts +++ b/src/handlers/shared/get-user-role.ts @@ -27,5 +27,6 @@ export async function getUserRole(context: Context, user: string): Promise { context.adapters = createAdapters(getSupabase(), context as unknown as Context); - await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${adminLimit} issues.`); - - expect(adminLimit).toEqual(6); + try { + await userStartStop(context as unknown as Context); + } catch (error) { + if (error instanceof Error) { + expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 3 issues."); + } + } }); }); From 4b9dbaa5bc0bb88a7c413f9d5974b6a2271ccc8b Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Mon, 29 Jul 2024 21:49:09 +0100 Subject: [PATCH 46/76] test: cleanups --- tests/__mocks__/db.ts | 1 + tests/__mocks__/issue-template.ts | 1 + tests/main.test.ts | 21 ++------------------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index 95dc2b3a..86bc6c4b 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -56,6 +56,7 @@ export const db = factory({ subscriptions_url: String, type: String, url: String, + role: String }), }, repo: { diff --git a/tests/__mocks__/issue-template.ts b/tests/__mocks__/issue-template.ts index c8f7dab1..44359721 100644 --- a/tests/__mocks__/issue-template.ts +++ b/tests/__mocks__/issue-template.ts @@ -22,6 +22,7 @@ export default { subscriptions_url: "", type: "", url: "", + role: "" }, author_association: "NONE", closed_at: null, diff --git a/tests/main.test.ts b/tests/main.test.ts index 655f3d37..acf94b4b 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -242,10 +242,10 @@ describe("User start/stop", () => { createIssuesForMaxAssignment(adminLimit + 4, sender.id); const context = createContext(issue, sender) as unknown as Context; - context.adapters = createAdapters(getSupabase(), context as unknown as Context); + context.adapters = createAdapters(getSupabase(), context); try { - await userStartStop(context as unknown as Context); + await userStartStop(context); } catch (error) { if (error instanceof Error) { expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 3 issues."); @@ -502,23 +502,6 @@ async function setupTests() { issue_number: 2, owner: "ubiquity", repo: "test-repo", - source: { - issue: { - number: 3, - state: "open", - body: `Resolves #2`, - html_url: "http://github.com/ubiquity/test-repo/pull/3", - repository: { - full_name: TEST_REPO, - }, - user: { - login: "user2", - }, - pull_request: { - html_url: "http://github.com/ubiquity/test-repo/pull/3", - }, - }, - }, }); } From c10accd7f88cec7c7bc7245a86239a6dd0afe90f Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 30 Jul 2024 07:42:52 +0100 Subject: [PATCH 47/76] test: made requested changes --- tests/__mocks__/db.ts | 1 - tests/__mocks__/issue-template.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index 86bc6c4b..95dc2b3a 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -56,7 +56,6 @@ export const db = factory({ subscriptions_url: String, type: String, url: String, - role: String }), }, repo: { diff --git a/tests/__mocks__/issue-template.ts b/tests/__mocks__/issue-template.ts index 44359721..c8f7dab1 100644 --- a/tests/__mocks__/issue-template.ts +++ b/tests/__mocks__/issue-template.ts @@ -22,7 +22,6 @@ export default { subscriptions_url: "", type: "", url: "", - role: "" }, author_association: "NONE", closed_at: null, From 2a3549672ca158ecefdb582d9bfd2fc5fc6580fe Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 30 Jul 2024 17:47:03 +0100 Subject: [PATCH 48/76] test: cleanpup test --- tests/main.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index acf94b4b..b2a2fb92 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { drop } from "@mswjs/data"; import { Context, SupportedEventsU } from "../src/types"; import { db } from "./__mocks__/db"; @@ -248,7 +249,7 @@ describe("User start/stop", () => { await userStartStop(context); } catch (error) { if (error instanceof Error) { - expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 3 issues."); + expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 2 issues."); } } }); From f308acab4e389d40bee8f68ab6ed47be797fc791 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:04:55 +0100 Subject: [PATCH 49/76] fix: assignee issue and open pr fetching --- src/utils/issue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/issue.ts b/src/utils/issue.ts index e1365e9a..2f4fd470 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -187,7 +187,7 @@ export async function getAllPullRequests( export async function getAllPullRequestReviews(context: Context, pullNumber: number, owner: string, repo: string) { try { - return (await context.octokit.paginate(context.octokit.rest.pulls.listReviews, { + return (await context.octokit.paginate(context.octokit.pulls.listReviews, { owner, repo, pull_number: pullNumber, From 675ccf1506bb5aaf28cb2ea224b53e3812fda038 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:56:23 +0100 Subject: [PATCH 50/76] chore: tests --- tests/main.test.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index b2a2fb92..c60f65a8 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable sonarjs/no-duplicate-string */ import { drop } from "@mswjs/data"; import { Context, SupportedEventsU } from "../src/types"; import { db } from "./__mocks__/db"; @@ -77,7 +76,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"); @@ -558,17 +557,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 a13b2ea551e1748e222019cc0c42de9f6d394c3e Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:08:22 +0100 Subject: [PATCH 51/76] chore: ci --- tests/main.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index c60f65a8..b4c66941 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -76,7 +76,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"); @@ -557,17 +557,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 d62cfe59a8426eea16239af779bceb39afeb8639 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 15 Aug 2024 01:37:35 +0100 Subject: [PATCH 52/76] fix: add validation to check for duplicate roles --- src/types/plugin-input.ts | 3 +- .../validate-schema-for-duplicate-roles.ts | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/utils/validate-schema-for-duplicate-roles.ts diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 48d77b31..ec39bdda 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,6 +1,7 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { StaticDecode, Type as T } from "@sinclair/typebox"; +import { Static, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; +import { validateSchemaForDuplicateRoles } from "../utils/validate-schema-for-duplicate-roles"; export interface PluginInputs { stateId: string; diff --git a/src/utils/validate-schema-for-duplicate-roles.ts b/src/utils/validate-schema-for-duplicate-roles.ts new file mode 100644 index 00000000..11f64874 --- /dev/null +++ b/src/utils/validate-schema-for-duplicate-roles.ts @@ -0,0 +1,37 @@ +import { Static, TSchema } from "@sinclair/typebox"; +import { startStopSchema } from "../types"; + +class DuplicateRoleError extends Error { + constructor(message: string) { + super(message); + this.name = "DuplicateRoleError"; + } + } + + export function validateSchemaForDuplicateRoles(schema: T): T { + return { + ...schema, + decode(value: unknown) { + try { + const decodedValue = value as Static; + + const taskRoles = decodedValue.miscellaneous.maxConcurrentTasks.map((task) => task.role); + const uniqueRoles = new Set(taskRoles); + + if (taskRoles.length !== uniqueRoles.size) { + throw new DuplicateRoleError("Duplicate roles found in maxConcurrentTasks."); + } + + return decodedValue; + } catch (error) { + if (error instanceof DuplicateRoleError) { + console.error(error.message); + throw error; + } else { + console.error("An unexpected error occurred during decoding:", error); + throw error; + } + } + }, + } as T; + } \ No newline at end of file From a5bd14dac37ee612e257e3a2d0e4375cc5f8b373 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 15 Aug 2024 15:54:09 +0100 Subject: [PATCH 53/76] fix: user t.record and loop by object.entries --- src/types/plugin-input.ts | 1 - .../validate-schema-for-duplicate-roles.ts | 37 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 src/utils/validate-schema-for-duplicate-roles.ts diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index ec39bdda..48c9ba91 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,7 +1,6 @@ import { SupportedEvents, SupportedEventsU } from "./context"; import { Static, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; -import { validateSchemaForDuplicateRoles } from "../utils/validate-schema-for-duplicate-roles"; export interface PluginInputs { stateId: string; diff --git a/src/utils/validate-schema-for-duplicate-roles.ts b/src/utils/validate-schema-for-duplicate-roles.ts deleted file mode 100644 index 11f64874..00000000 --- a/src/utils/validate-schema-for-duplicate-roles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Static, TSchema } from "@sinclair/typebox"; -import { startStopSchema } from "../types"; - -class DuplicateRoleError extends Error { - constructor(message: string) { - super(message); - this.name = "DuplicateRoleError"; - } - } - - export function validateSchemaForDuplicateRoles(schema: T): T { - return { - ...schema, - decode(value: unknown) { - try { - const decodedValue = value as Static; - - const taskRoles = decodedValue.miscellaneous.maxConcurrentTasks.map((task) => task.role); - const uniqueRoles = new Set(taskRoles); - - if (taskRoles.length !== uniqueRoles.size) { - throw new DuplicateRoleError("Duplicate roles found in maxConcurrentTasks."); - } - - return decodedValue; - } catch (error) { - if (error instanceof DuplicateRoleError) { - console.error(error.message); - throw error; - } else { - console.error("An unexpected error occurred during decoding:", error); - throw error; - } - } - }, - } as T; - } \ No newline at end of file From 61294ab0c8db85b1475c05031dc8a8da8d7a5bd1 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Wed, 28 Aug 2024 12:21:00 +0100 Subject: [PATCH 54/76] chore: revert to staticDecode --- src/types/plugin-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 48c9ba91..4495f685 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,5 +1,5 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { Static, Type as T } from "@sinclair/typebox"; +import { Static, StaticDecode, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; export interface PluginInputs { From 0cd8a4943c8087b4996c8c5cea913764603035ce Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 29 Aug 2024 17:37:51 +0100 Subject: [PATCH 55/76] chore: rebase to development --- src/handlers/shared/start.ts | 64 ++++++++------------------ src/types/plugin-input.ts | 2 +- src/utils/issue.ts | 52 +++++---------------- tests/main.test.ts | 89 +++++++++++++++++++++++++----------- 4 files changed, 94 insertions(+), 113 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 307fd74e..956b7474 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -7,7 +7,7 @@ import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; import structuredMetadata from "./structured-metadata"; import { assignTableComment } from "./table"; -export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]) { +export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"]) { const { logger, config } = context; const { taskStaleTimeoutDuration } = config; const maxTask = await getUserRoleAndTaskLimit(context, sender.login); @@ -19,13 +19,13 @@ 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`); - return { output: "Parent issue detected" }; + throw new Error("Issue is a parent issue"); } let commitHash: string | null = null; try { - const hashResponse = await context.octokit.rest.repos.getCommit({ + const hashResponse = await context.octokit.repos.getCommit({ owner: context.payload.repository.owner.login, repo: context.payload.repository.name, ref: context.payload.repository.default_branch, @@ -55,51 +55,41 @@ export async function start(context: Context, issue: Context["payload"]["issue"] // is it assignable? if (issue.state === ISSUE_TYPE.CLOSED) { - throw logger.error("This issue is closed, please choose another.", { issueNumber: issue.number }); + 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"); } - const assignees = issue?.assignees ?? []; - - // find out if the issue is already assigned + const assignees = (issue?.assignees ?? []).filter(Boolean); if (assignees.length !== 0) { - const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login); - throw logger.error( - isCurrentUserAssigned ? "You are already assigned to this task." : "This issue is already assigned. Please choose another unassigned task.", - { issueNumber: issue.number } - ); - } - - teammates.push(sender.login); - - // check max assigned issues - for (const user of teammates) { - await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login); + 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"); } // get labels + const labels = issue.labels; const priceLabel = labels.find((label: Label) => label.name.startsWith("Price: ")); if (!priceLabel) { - throw logger.error("No price label is set to calculate the duration", { issueNumber: issue.number }); + 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"); } const duration: number = calculateDurations(labels).shift() ?? 0; - const { id } = sender; - const logMessage = logger.info("Task assigned successfully", { - duration, - priceLabel, - revision: commitHash?.substring(0, 7), - assignees: teammates, - issue: issue.number, - }); + const { id, login } = sender; + const logMessage = logger.info("Task assigned successfully", { duration, priceLabel, revision: commitHash?.substring(0, 7) }); const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration); const metadata = structuredMetadata.create("Assignment", logMessage); - // assign the issue - await addAssignees(context, issue.number, teammates); + // add assignee + if (!assignees.map((i: Partial) => i?.login).includes(login)) { + await addAssignees(context, issue.number, [login]); + } const isTaskStale = checkTaskStale(getTimeValue(taskStaleTimeoutDuration), issue.created_at); @@ -119,17 +109,3 @@ 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); - const assignedIssues = await getAssignedIssues(context, username); - - // check for max and enforce max - if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { - 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, - }); - } -} diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 4495f685..48d77b31 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,5 +1,5 @@ import { SupportedEvents, SupportedEventsU } from "./context"; -import { Static, StaticDecode, Type as T } from "@sinclair/typebox"; +import { StaticDecode, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; export interface PluginInputs { diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 2f4fd470..65112aaf 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -28,7 +28,8 @@ export async function getAssignedIssues(context: Context, username: string): Pro }) ); } catch (err: unknown) { - throw context.logger.error("Fetching assigned issues failed!", { error: err as Error }); + context.logger.error("Fetching assigned issues failed!", { error: err as Error }); + return []; } } @@ -40,14 +41,14 @@ export async function addCommentToIssue(context: Context, message: string | null const issueNumber = payload.issue.number; try { - await context.octokit.rest.issues.createComment({ + await context.octokit.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, body: comment, }); } catch (err: unknown) { - throw context.logger.error("Adding a comment failed!", { error: err as Error }); + context.logger.error("Adding a comment failed!", { error: err as Error }); } } @@ -63,17 +64,15 @@ export async function closePullRequest(context: Context, results: GetLinkedResul state: "closed", }); } catch (err: unknown) { - throw context.logger.error("Closing pull requests failed!", { error: err as Error }); + context.logger.error("Closing pull requests failed!", { error: err as Error }); } } export async function closePullRequestForAnIssue(context: Context, issueNumber: number, repository: Context["payload"]["repository"], author: string) { const { logger } = context; if (!issueNumber) { - throw logger.error("Issue is not defined", { - issueNumber, - repository: repository.name, - }); + logger.error("Issue is not defined"); + return; } const linkedPullRequests = await getLinkedPullRequests(context, { @@ -120,35 +119,6 @@ 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.rest.issues.get({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issueNumber, - }); - - if (!assignees?.length) { - 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.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); - } -} - export async function addAssignees(context: Context, issueNo: number, assignees: string[]) { const payload = context.payload; @@ -162,8 +132,6 @@ 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( @@ -181,7 +149,8 @@ export async function getAllPullRequests( sort: "created", })) as GitHubIssueSearch["items"]; } catch (err: unknown) { - throw context.logger.error("Fetching all pull requests failed!", { error: err as Error }); + context.logger.error("Fetching all pull requests failed!", { error: err as Error }); + return []; } } @@ -193,7 +162,8 @@ export async function getAllPullRequestReviews(context: Context, pullNumber: num pull_number: pullNumber, })) as Review[]; } catch (err: unknown) { - throw context.logger.error("Fetching all pull request reviews failed!", { error: err as Error }); + context.logger.error("Fetching all pull request reviews failed!", { error: err as Error }); + return []; } } diff --git a/tests/main.test.ts b/tests/main.test.ts index b4c66941..5a7556af 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -45,23 +45,6 @@ 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; @@ -76,7 +59,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"); @@ -112,6 +95,7 @@ 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); @@ -184,6 +168,40 @@ 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; @@ -502,6 +520,23 @@ async function setupTests() { issue_number: 2, owner: "ubiquity", repo: "test-repo", + source: { + issue: { + number: 3, + state: "open", + body: `Resolves #2`, + html_url: "http://github.com/ubiquity/test-repo/pull/3", + repository: { + full_name: TEST_REPO, + }, + user: { + login: "user2", + }, + pull_request: { + html_url: "http://github.com/ubiquity/test-repo/pull/3", + }, + }, + }, }); } @@ -557,17 +592,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 ea30c4e2dbc48bb3579122a272eea22811d851ef Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 29 Aug 2024 19:59:57 +0100 Subject: [PATCH 56/76] chore: rebase to development --- src/handlers/shared/start.ts | 2 +- src/handlers/shared/stop.ts | 26 ++++++++++---------------- tests/main.test.ts | 1 - 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 956b7474..d4c6d616 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -7,7 +7,7 @@ import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; 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 { taskStaleTimeoutDuration } = config; const maxTask = await getUserRoleAndTaskLimit(context, sender.login); diff --git a/src/handlers/shared/stop.ts b/src/handlers/shared/stop.ts index 3a2541e4..4f101867 100644 --- a/src/handlers/shared/stop.ts +++ b/src/handlers/shared/stop.ts @@ -11,7 +11,9 @@ 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 new Error(logger.error("You are not assigned to this task", { issueNumber, user: sender.login })?.logMessage.diff as string); + 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" }; } // close PR @@ -24,24 +26,16 @@ export async function stop(context: Context, issue: Context["payload"]["issue"], // remove assignee - try { - await context.octokit.rest.issues.removeAssignees({ - owner: login, - repo: name, - issue_number: issueNumber, - assignees: [userToUnassign.login], - }); - } catch (err) { - throw logger.error(`Error while removing ${userToUnassign.login} from the issue: `, { - err, - issueNumber, - user: userToUnassign.login, - }); - } + await context.octokit.rest.issues.removeAssignees({ + owner: login, + repo: name, + issue_number: issueNumber, + assignees: [sender.login], + }); const unassignedLog = logger.info("You have been unassigned from the task", { issueNumber, - user: userToUnassign.login, + user: sender.login, }); await addCommentToIssue(context, unassignedLog?.logMessage.diff as string); diff --git a/tests/main.test.ts b/tests/main.test.ts index 5a7556af..70daaf15 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -221,7 +221,6 @@ describe("User start/stop", () => { test("should return the role with the smallest task limit if user role is not defined in config", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - // role: new-start const sender = db.users.findFirst({ where: { id: { equals: 4 } } }) as unknown as Sender; const contributorLimit = maxConcurrentDefaults.contributor; From d318b3b192a584dd86a3a0ba84de3edcaddbf295 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 29 Aug 2024 22:38:18 +0100 Subject: [PATCH 57/76] chore: cleanups --- src/handlers/shared/start.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 4d83196d..3283d618 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -8,9 +8,9 @@ import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; import structuredMetadata from "./structured-metadata"; import { assignTableComment } from "./table"; -export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teamMates: string[]) { +export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]) { const { logger, config } = context; - const { taskStaleTimeoutDuration } = config; + const { maxConcurrentTasks, taskStaleTimeoutDuration } = config; const maxTask = await getUserRoleAndTaskLimit(context, sender.login); // is it a child issue? @@ -121,16 +121,20 @@ 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) { +async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: { [role: string]: number }, logger: Context["logger"], sender: string) { const openedPullRequests = await getAvailableOpenedPullRequests(context, username); const assignedIssues = await getAssignedIssues(context, username); // check for max and enforce max - if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) { + const maxTask = await getUserRoleAndTaskLimit(context, sender); + const maxTasksForRole = maxConcurrentTasks[maxTask.limit] ?? 0; + + if (assignedIssues.length - openedPullRequests.length >= maxTasksForRole) { 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, + maxConcurrentTasks: maxTasksForRole, }); } } + From 009735877ed839286bec0626b1530ff4a1189827 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Mon, 2 Sep 2024 09:53:10 +0100 Subject: [PATCH 58/76] clean up merge --- src/handlers/shared/start.ts | 15 +++------------ tests/__mocks__/db.ts | 1 - tests/__mocks__/users-get.json | 1 - tests/main.test.ts | 17 +++++++++-------- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index faeb3937..4bbb5c01 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -77,7 +77,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const toAssign = []; // check max assigned issues for (const user of teammates) { - if (await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login)) { + if (await handleTaskLimitChecks(user, context, maxTask.limit, logger, sender.login)) { toAssign.push(user); } } @@ -136,8 +136,6 @@ export async function start(context: Context, issue: Context["payload"]["issue"] return { output: "Task assigned successfully" }; } -async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: { [role: string]: number }, logger: Context["logger"], sender: string) { - async function fetchUserIds(context: Context, username: string[]) { const ids = []; @@ -157,22 +155,16 @@ async function fetchUserIds(context: Context, username: string[]) { } async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: number, logger: Context["logger"], sender: string) { - const openedPullRequests = await getAvailableOpenedPullRequests(context, username); const assignedIssues = await getAssignedIssues(context, username); // check for max and enforce max - const maxTask = await getUserRoleAndTaskLimit(context, sender); - const maxTasksForRole = maxConcurrentTasks[maxTask.limit] ?? 0; - - if (assignedIssues.length - openedPullRequests.length >= maxTasksForRole) { - throw logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, { if (Math.abs(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`, { assignedIssues: assignedIssues.length, openedPullRequests: openedPullRequests.length, - maxConcurrentTasks: maxTasksForRole, + maxConcurrentTasks, }); await addCommentToIssue(context, log?.logMessage.diff as string); return false; @@ -183,5 +175,4 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc } return true; -} - +} \ No newline at end of file diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index f6489056..d81914dd 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -103,7 +103,6 @@ export const db = factory({ type: String, url: String, }), - pull_request: Object, assignees: Array, requested_reviewers: Array, requested_teams: Array, diff --git a/tests/__mocks__/users-get.json b/tests/__mocks__/users-get.json index f5e72a03..b8b7bda3 100644 --- a/tests/__mocks__/users-get.json +++ b/tests/__mocks__/users-get.json @@ -23,6 +23,5 @@ "id": 5, "login": "user5", "role": "member" - "login": "user2" } ] diff --git a/tests/main.test.ts b/tests/main.test.ts index efa0cd3a..daccbc69 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -105,7 +105,7 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context); - expect(output).toEqual({ output: "You are not assigned to this task" }); + 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 () => { @@ -178,9 +178,9 @@ describe("User start/stop", () => { const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 4 } } }) as unknown as Sender; -// test("User can't start another issue if they have reached the max limit", async () => { -// const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; -// const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as PayloadSender; + // test("User can't start another issue if they have reached the max limit", async () => { + // const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; + // const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as PayloadSender; const contributorLimit = maxConcurrentDefaults.contributor; createIssuesForMaxAssignment(contributorLimit, sender.id); @@ -224,8 +224,9 @@ describe("User start/stop", () => { await userStartStop(context); } catch (error) { if (error instanceof Error) { - expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 2 issues."); - context.config.maxConcurrentTasks = 1; + expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 2 issues.") + } + } context.adapters = createAdapters(getSupabase(), context); @@ -633,7 +634,7 @@ const maxConcurrentDefaults = { member: 4, contributor: 2, }; - + function createContext( issue: Record, sender: Record, @@ -641,7 +642,7 @@ function createContext( appId: string | null = "1", startRequiresWallet = false ): Context { - + return { adapters: {} as ReturnType, payload: { From 1837930f30b1de7b949637bdafb38ca302afa769 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Mon, 2 Sep 2024 10:58:11 +0100 Subject: [PATCH 59/76] test: fixed test to reflect merge --- tests/main.test.ts | 91 ++++++++-------------------------------------- 1 file changed, 16 insertions(+), 75 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index daccbc69..d8c4c54c 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -78,7 +78,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 PayloadSender; const context = createContext(issue, sender, "/stop"); @@ -115,9 +115,7 @@ describe("User start/stop", () => { const context = createContext(issue, sender, "/stop"); context.adapters = createAdapters(getSupabase(), context as unknown as Context); - const output = await userStartStop(context as unknown as Context); - - expect(output).toEqual({ output: "You are not assigned to this task" }); + 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 () => { @@ -174,26 +172,6 @@ describe("User start/stop", () => { await expect(userStartStop(context)).rejects.toThrow("Skipping '/start' since the issue is a parent issue"); }); - test("should return the role with the smallest task limit if user role is not defined in config", async () => { - const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - const sender = db.users.findFirst({ where: { id: { equals: 4 } } }) as unknown as Sender; - - // test("User can't start another issue if they have reached the max limit", async () => { - // const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; - // const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as PayloadSender; - - const contributorLimit = maxConcurrentDefaults.contributor; - createIssuesForMaxAssignment(contributorLimit, sender.id); - const context = createContext(issue, sender); - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - - await expect(userStartStop(context as unknown as Context)).rejects.toThrow( - `Too many assigned issues, you have reached your max limit of ${contributorLimit} issues.` - ); - - expect(contributorLimit).toEqual(2); - }); - test("should set maxLimits to 4 if the user is a member", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 5 } } }) as unknown as Sender; @@ -209,7 +187,7 @@ describe("User start/stop", () => { expect(memberLimit).toEqual(4); }); - test("should set maxLimits to 6 if the user is an admin", async () => { +test("should set maxLimits to 6 if the user is an admin", 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; @@ -218,19 +196,10 @@ describe("User start/stop", () => { createIssuesForMaxAssignment(adminLimit + 4, sender.id); const context = createContext(issue, sender) as unknown as Context; - context.adapters = createAdapters(getSupabase(), context); - - try { - await userStartStop(context); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual("Too many assigned issues, you have reached your max limit of 2 issues.") - } - } - - context.adapters = createAdapters(getSupabase(), context); + context.adapters = createAdapters(getSupabase(), context as unknown as Context); + await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${adminLimit} issues.`); - await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); + expect(adminLimit).toEqual(6); }); test("User can't start an issue if they have previously been unassigned by an admin", async () => { @@ -272,7 +241,6 @@ describe("User start/stop", () => { const errorDetails: string[] = []; for (const error of envConfigValidator.errors(env)) { errorDetails.push(`${error.path}: ${error.message}`); - } expect(errorDetails).toContain("Invalid APP_ID"); @@ -379,7 +347,6 @@ async function setupTests() { owner: "ubiquity", repo: "test-repo", state: "open", - pull_request: {}, closed_at: null, }); @@ -398,7 +365,6 @@ async function setupTests() { body: "Pull request", owner: "ubiquity", repo: "test-repo", - pull_request: {}, state: "open", closed_at: null, }); @@ -418,28 +384,6 @@ async function setupTests() { body: "Pull request body", owner: "ubiquity", repo: "test-repo", - pull_request: {}, - state: "open", - closed_at: null, - }); - - db.pull.create({ - id: 4, - html_url: "https://github.com/ubiquity/test-repo/pull/4", - number: 3, - author: { - id: 1, - name: "ubiquity", - }, - user: { - id: 1, - login: "ubiquity", - }, - body: "Pull request body", - owner: "ubiquity", - draft: true, - pull_request: {}, - repo: "test-repo", state: "open", closed_at: null, }); @@ -447,8 +391,6 @@ async function setupTests() { db.review.create({ id: 1, body: "Review body", - owner: "ubiquity", - repo: "test-repo", commit_id: "123", html_url: "", pull_request_url: "", @@ -642,7 +584,6 @@ function createContext( appId: string | null = "1", startRequiresWallet = false ): Context { - return { adapters: {} as ReturnType, payload: { @@ -679,17 +620,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, + }, }, - }, }), }), }), @@ -700,4 +641,4 @@ function getSupabase(withData = true) { }; return mockedSupabase as unknown as ReturnType; -} +} \ No newline at end of file From 84c8465c815c9f2edfab3b151e109d40504c083b Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 3 Sep 2024 18:56:49 +0100 Subject: [PATCH 60/76] fix: fix failling knip and double logging max limit error message --- src/handlers/shared/get-user-role.ts | 32 ---------------------------- src/handlers/shared/start.ts | 2 -- 2 files changed, 34 deletions(-) delete mode 100644 src/handlers/shared/get-user-role.ts diff --git a/src/handlers/shared/get-user-role.ts b/src/handlers/shared/get-user-role.ts deleted file mode 100644 index 2bd1b3d9..00000000 --- a/src/handlers/shared/get-user-role.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Context } from "../../types"; - -interface MatchingUserProps { - role: string; - limit: number; -} - -export async function getUserRole(context: Context, user: string): Promise { - const orgLogin = context.payload.organization?.login; - const { config, logger } = context; - const { maxConcurrentTasks } = config.miscellaneous; - - try { - const response = await context.octokit.orgs.getMembershipForUser({ - org: orgLogin as string, - username: user, - }); - - const matchingUser = maxConcurrentTasks.find(({ role }) => role.toLowerCase() === response.data.role); - - if (matchingUser) { - //chech if the current user role matches any of those defined in the config - return matchingUser; - } else { - //return the role with the smallest task limit - return maxConcurrentTasks.reduce((minTask, currentTask) => (currentTask.limit < minTask.limit ? currentTask : minTask)); - } - } catch (error) { - logger.error("An error occured", { error: error as Error }); - throw error; - } -} diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 4bbb5c01..067efd16 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -48,8 +48,6 @@ export async function start(context: Context, issue: Context["payload"]["issue"] // check for max and enforce max if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxTask.limit) { - const log = logger.error("Too many assigned issues, you have reached your max limit") - await addCommentToIssue(context, log?.logMessage.diff as string); throw new Error(`Too many assigned issues, you have reached your max limit of ${maxTask.limit} issues.`); } From 14f8e5f2ed01af77d5711eb49501f79696b4c61c Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 3 Sep 2024 20:57:33 +0100 Subject: [PATCH 61/76] fix: remove openedPullRequest checks for maxlimit --- src/handlers/shared/start.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 067efd16..5acccb65 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -36,18 +36,12 @@ 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: openedPullRequests.map((pr) => pr.html_url), - }); - const assignedIssues = await getAssignedIssues(context, sender.login); logger.info("Max issues allowed is", { limit: maxTask.limit, assigned: assignedIssues.length }); // check for max and enforce max - if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxTask.limit) { + if (assignedIssues.length >= maxTask.limit) { throw new Error(`Too many assigned issues, you have reached your max limit of ${maxTask.limit} issues.`); } From 99ccab31345cfce1ddf804999cd9bf3707010c26 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Tue, 3 Sep 2024 22:04:11 +0100 Subject: [PATCH 62/76] fix: remove duplicate maxTask checks --- src/handlers/shared/start.ts | 11 +---------- tests/main.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 5acccb65..318300c5 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -35,16 +35,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] } catch (e) { logger.error("Error while getting commit hash", { error: e as Error }); } - - const assignedIssues = await getAssignedIssues(context, sender.login); - logger.info("Max issues allowed is", { limit: maxTask.limit, assigned: assignedIssues.length }); - - // check for max and enforce max - - if (assignedIssues.length >= maxTask.limit) { - throw new Error(`Too many assigned issues, you have reached your max limit of ${maxTask.limit} issues.`); - } - + // is it assignable? if (issue.state === ISSUE_TYPE.CLOSED) { diff --git a/tests/main.test.ts b/tests/main.test.ts index d8c4c54c..babf8d30 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -182,7 +182,7 @@ describe("User start/stop", () => { const context = createContext(issue, sender) as unknown as Context; context.adapters = createAdapters(getSupabase(), context as unknown as Context); - await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${memberLimit} issues.`); + await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); expect(memberLimit).toEqual(4); }); @@ -197,7 +197,7 @@ test("should set maxLimits to 6 if the user is an admin", async () => { const context = createContext(issue, sender) as unknown as Context; context.adapters = createAdapters(getSupabase(), context as unknown as Context); - await expect(userStartStop(context)).rejects.toThrow(`Too many assigned issues, you have reached your max limit of ${adminLimit} issues.`); + await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); expect(adminLimit).toEqual(6); }); From d0e17bea06ecce10a88ba437052815ce041c741f Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 5 Sep 2024 01:04:13 +0100 Subject: [PATCH 63/76] test: fix failing test --- tests/main.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index babf8d30..5fc1aeec 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -226,7 +226,7 @@ test("should set maxLimits to 6 if the user is an admin", async () => { errorDetails.push(`${error.path}: ${error.message}`); } - expect(errorDetails).toContain("/APP_ID: Required property"); + expect(errorDetails).toContain("/APP_ID: Expected union value"); } }); @@ -574,7 +574,7 @@ function createIssuesForMaxAssignment(n: number, userId: number) { const maxConcurrentDefaults = { admin: 6, member: 4, - contributor: 2, + contributor: 3, }; function createContext( From 0a4ed48587d0033ba950b2565c164dc55b50d63d Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 5 Sep 2024 06:35:30 +0100 Subject: [PATCH 64/76] Update src/types/plugin-input.ts Co-authored-by: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> --- src/types/plugin-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 8dd4d178..646eb7de 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -21,7 +21,7 @@ export const startStopSchema = T.Object( }, { default: {} - }, + } ); export type StartStopSettings = StaticDecode; From c2c924fb41765f222e42b3d06b0227dce1f43dec Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 5 Sep 2024 06:35:39 +0100 Subject: [PATCH 65/76] Update src/handlers/shared/start.ts Co-authored-by: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> --- src/handlers/shared/start.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 318300c5..e92c1c24 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,4 +1,3 @@ - import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue"; import { calculateDurations } from "../../utils/shared"; From c36c9566ade570722dfc554299708cf75e33f3f1 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 5 Sep 2024 06:38:56 +0100 Subject: [PATCH 66/76] chore: fix read me example indentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 183d01bb..2ffae6f6 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu reviewDelayTolerance: "3 Days" taskStaleTimeoutDuration: "30 Days" maxConcurrentTasks: # Default concurrent task limits per role. - admin: 10 - member: 5 - contributor: 3 + admin: 10 + member: 5 + contributor: 3 startRequiresWallet: true # default is true emptyWalletText: "Please set your wallet address with the /wallet command first and try again." ``` From 0bfa5e0bba90837cdb3826490949c25d6f1bf14e Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 5 Sep 2024 12:02:55 +0100 Subject: [PATCH 67/76] fix: address duplicate logging on max limit --- src/handlers/shared/start.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index e92c1c24..082f1610 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -143,12 +143,22 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc // check for max and enforce max if (Math.abs(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`, { - assignedIssues: assignedIssues.length, - openedPullRequests: openedPullRequests.length, - maxConcurrentTasks, - }); - await addCommentToIssue(context, log?.logMessage.diff as string); + const logMessage = username !== sender + ? `${username} has reached their max task limit` + : null; + + const log = logMessage + ? logger.error(logMessage, { + assignedIssues: assignedIssues.length, + openedPullRequests: openedPullRequests.length, + maxConcurrentTasks, + }) + : null; + + if (log && log.logMessage.diff) { + await addCommentToIssue(context, log.logMessage.diff as string); + } + return false; } From acb23889aeb0c63bbd414eba249b9ae0ffb1753f Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 5 Sep 2024 12:04:40 +0100 Subject: [PATCH 68/76] chore: fix readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2ffae6f6..d17ece76 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu command: "\/start|\/stop" example: "/start" # or "/stop" with: - timers: reviewDelayTolerance: "3 Days" taskStaleTimeoutDuration: "30 Days" maxConcurrentTasks: # Default concurrent task limits per role. From 3a80686ab9618c71df238a986114ad2ec74ecb5a Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Fri, 6 Sep 2024 15:42:39 +0100 Subject: [PATCH 69/76] chore: cleanup code to avoid tenaries --- src/handlers/shared/start.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 082f1610..02a34927 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -143,20 +143,17 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc // check for max and enforce max if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxConcurrentTasks) { - const logMessage = username !== sender - ? `${username} has reached their max task limit` - : null; + if (username !== sender) { + const logMessage = `${username} has reached their max task limit`; + const log = logger.error(logMessage, { + assignedIssues: assignedIssues.length, + openedPullRequests: openedPullRequests.length, + maxConcurrentTasks, + }); - const log = logMessage - ? logger.error(logMessage, { - assignedIssues: assignedIssues.length, - openedPullRequests: openedPullRequests.length, - maxConcurrentTasks, - }) - : null; - - if (log && log.logMessage.diff) { - await addCommentToIssue(context, log.logMessage.diff as string); + if (log?.logMessage?.diff) { + await addCommentToIssue(context, log.logMessage.diff as string); + } } return false; From 9416b25389063ebe9f25307c31845adab9e8f7e4 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Sat, 7 Sep 2024 16:21:39 +0100 Subject: [PATCH 70/76] fix: check limit per role correctly --- package.json | 3 +-- src/handlers/shared/start.ts | 27 +++++++++++---------------- tests/main.test.ts | 16 ++++++++-------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a52f1cf5..be4682a2 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,5 @@ "extends": [ "@commitlint/config-conventional" ] - }, - "packageManager": "yarn@4.2.2" + } } diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index b3d19666..d39d8c19 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -1,12 +1,8 @@ -import { Assignee, Context, ISSUE_TYPE, Label } from "../../types"; +import { Context, ISSUE_TYPE, Label } from "../../types"; import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue"; -import { calculateDurations } from "../../utils/shared"; import { checkTaskStale } from "./check-task-stale"; import { hasUserBeenUnassigned } from "./check-assignments"; -import { generateAssignmentComment } from "./generate-assignment-comment"; import { getUserRoleAndTaskLimit } from "./get-user-task-limit-and-role"; -import { Context, ISSUE_TYPE, Label } from "../../types"; -import { addAssignees, addCommentToIssue, getAssignedIssues, getAvailableOpenedPullRequests, getTimeValue, isParentIssue } from "../../utils/issue"; import { HttpStatusCode, Result } from "../result-types"; import { generateAssignmentComment, getDeadline } from "./generate-assignment-comment"; import structuredMetadata from "./structured-metadata"; @@ -14,8 +10,7 @@ import { assignTableComment } from "./table"; export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]): Promise { const { logger, config } = context; - const { maxConcurrentTasks, taskStaleTimeoutDuration } = config; - const maxTask = await getUserRoleAndTaskLimit(context, sender.login); + const { taskStaleTimeoutDuration } = config; // is it a child issue? if (issue.body && isParentIssue(issue.body)) { @@ -38,7 +33,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] } catch (e) { logger.error("Error while getting commit hash", { error: e as Error }); } - + // is it assignable? if (issue.state === ISSUE_TYPE.CLOSED) { @@ -63,7 +58,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"] const toAssign = []; // check max assigned issues for (const user of teammates) { - if (await handleTaskLimitChecks(user, context, maxTask.limit, logger, sender.login)) { + if (await handleTaskLimitChecks(user, context, logger, sender.login)) { toAssign.push(user); } } @@ -140,26 +135,26 @@ async function fetchUserIds(context: Context, username: string[]) { return ids; } -async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: number, logger: Context["logger"], sender: string) { +async function handleTaskLimitChecks(username: string, context: Context, logger: Context["logger"], sender: string) { const openedPullRequests = await getAvailableOpenedPullRequests(context, username); const assignedIssues = await getAssignedIssues(context, username); + const { limit } = await getUserRoleAndTaskLimit(context, username); // check for max and enforce max - - if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxConcurrentTasks) { + if (Math.abs(assignedIssues.length - openedPullRequests.length) >= limit) { if (username !== sender) { const logMessage = `${username} has reached their max task limit`; const log = logger.error(logMessage, { assignedIssues: assignedIssues.length, openedPullRequests: openedPullRequests.length, - maxConcurrentTasks, + limit, }); - + if (log?.logMessage?.diff) { await addCommentToIssue(context, log.logMessage.diff as string); } } - + return false; } @@ -168,4 +163,4 @@ async function handleTaskLimitChecks(username: string, context: Context, maxConc } return true; -} \ No newline at end of file +} diff --git a/tests/main.test.ts b/tests/main.test.ts index 6c1957e1..27165e00 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -172,7 +172,7 @@ describe("User start/stop", () => { await expect(userStartStop(context)).rejects.toThrow("Skipping '/start' since the issue is a parent issue"); }); - test("should set maxLimits to 4 if the user is a member", async () => { + test("should set maxLimits to 6 if the user is a member", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 5 } } }) as unknown as Sender; @@ -184,10 +184,10 @@ describe("User start/stop", () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); - expect(memberLimit).toEqual(4); + expect(memberLimit).toEqual(6); }); -test("should set maxLimits to 6 if the user is an admin", async () => { + test("should set maxLimits to 8 if the user is an admin", 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; @@ -199,7 +199,7 @@ test("should set maxLimits to 6 if the user is an admin", async () => { context.adapters = createAdapters(getSupabase(), context as unknown as Context); await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); - expect(adminLimit).toEqual(6); + expect(adminLimit).toEqual(8); }); test("User can't start an issue if they have previously been unassigned by an admin", async () => { @@ -572,9 +572,9 @@ function createIssuesForMaxAssignment(n: number, userId: number) { } const maxConcurrentDefaults = { - admin: 6, - member: 4, - contributor: 3, + admin: 8, + member: 6, + contributor: 4, }; function createContext( @@ -641,4 +641,4 @@ function getSupabase(withData = true) { }; return mockedSupabase as unknown as ReturnType; -} \ No newline at end of file +} From bd4d1cb4b46216b597a282f576ce6ad16cab312b Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Sat, 7 Sep 2024 16:23:06 +0100 Subject: [PATCH 71/76] chore: revery change in package json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index be4682a2..a52f1cf5 100644 --- a/package.json +++ b/package.json @@ -83,5 +83,6 @@ "extends": [ "@commitlint/config-conventional" ] - } + }, + "packageManager": "yarn@4.2.2" } From ba92dd7509aeecf9daae796944e188285a4cb271 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Sat, 7 Sep 2024 16:26:25 +0100 Subject: [PATCH 72/76] chore: remove optional access to the LogReturn --- src/handlers/shared/start.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index d39d8c19..52fcc617 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -150,7 +150,7 @@ async function handleTaskLimitChecks(username: string, context: Context, logger: limit, }); - if (log?.logMessage?.diff) { + if (log.logMessage?.diff) { await addCommentToIssue(context, log.logMessage.diff as string); } } From 14f3ceb444e2389d8c33e0d26443d089fa1d8427 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Sat, 7 Sep 2024 22:13:43 +0100 Subject: [PATCH 73/76] chore: validate org login --- .../shared/get-user-task-limit-and-role.ts | 17 +++++++++++------ src/handlers/shared/start.ts | 4 +--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 98ba3e8a..a3ea85a2 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -16,16 +16,21 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P ); try { + // Validate the organization login + if (typeof orgLogin !== 'string' || orgLogin.trim() === '') { + throw new Error('Invalid organization name'); + } + const response = await context.octokit.orgs.getMembershipForUser({ - org: orgLogin as string, + org: orgLogin, username: user, }); - - const role = response.data.role.toLowerCase() + + const role = response.data.role.toLowerCase(); const limit = maxConcurrentTasks[role]; - - return limit ? { role, limit } : smallestTask; - + + return limit ? { role, limit } : smallestTask; + } catch (err) { logger.error("Could not get user role", { err }); return smallestTask; diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 52fcc617..4ab3eb9d 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -150,9 +150,7 @@ async function handleTaskLimitChecks(username: string, context: Context, logger: limit, }); - if (log.logMessage?.diff) { - await addCommentToIssue(context, log.logMessage.diff as string); - } + await addCommentToIssue(context, log.logMessage.diff); } return false; From b313fa89d3f373cac0d141043813d9f23b3e8508 Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Wed, 11 Sep 2024 08:28:57 +0100 Subject: [PATCH 74/76] chore: format with prettier --- src/handlers/shared/get-user-task-limit-and-role.ts | 2 +- src/plugin.ts | 5 +---- src/types/plugin-input.ts | 4 ++-- src/utils/issue.ts | 8 ++------ 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index a3ea85a2..1b0f75f2 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -26,7 +26,7 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P username: user, }); - const role = response.data.role.toLowerCase(); + const role = response.data.role.toLowerCase() const limit = maxConcurrentTasks[role]; return limit ? { role, limit } : smallestTask; diff --git a/src/plugin.ts b/src/plugin.ts index 4f1886ab..84763253 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -45,8 +45,5 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { } function sanitizeMetadata(obj: LogReturn["metadata"]): string { - return JSON.stringify(obj, null, 2) - .replace(//g, ">") - .replace(/--/g, "--") + return JSON.stringify(obj, null, 2).replace(//g, ">").replace(/--/g, "--"); } diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 646eb7de..f246517e 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -20,9 +20,9 @@ export const startStopSchema = T.Object( emptyWalletText: T.String({ default: "Please set your wallet address with the /wallet command first and try again." }), }, { - default: {} + default: {}, } ); export type StartStopSettings = StaticDecode; -export const startStopSettingsValidator = new StandardValidator(startStopSchema); \ No newline at end of file +export const startStopSettingsValidator = new StandardValidator(startStopSchema); diff --git a/src/utils/issue.ts b/src/utils/issue.ts index fbba7902..48c4ce73 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,6 +1,6 @@ import ms from "ms"; import { Context } from "../types/context"; -import { Issue, GitHubIssueSearch, Review } from "../types/payload"; +import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; export function isParentIssue(body: string) { @@ -21,10 +21,7 @@ export async function getAssignedIssues(context: Context, username: string): Pro }) .then((issues) => issues.filter((issue) => { - return ( - issue.state === "open" && - (issue.assignee?.login === username || issue.assignees?.some((assignee) => assignee.login === username)) - ); + return issue.state === "open" && (issue.assignee?.login === username || issue.assignees?.some((assignee) => assignee.login === username)); }) ); } catch (err: unknown) { @@ -32,7 +29,6 @@ export async function getAssignedIssues(context: Context, username: string): Pro } } - export async function addCommentToIssue(context: Context, message: string | null) { const { payload, logger } = context; if (!message) { From a4df1b99f2839f81dd0fea8334e15265458678ff Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Wed, 11 Sep 2024 11:08:53 +0100 Subject: [PATCH 75/76] fix: cleanup merge --- .../shared/get-user-task-limit-and-role.ts | 21 +++++++++---------- src/handlers/shared/start.ts | 18 ++-------------- src/utils/issue.ts | 2 +- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/handlers/shared/get-user-task-limit-and-role.ts b/src/handlers/shared/get-user-task-limit-and-role.ts index 1b0f75f2..53b96715 100644 --- a/src/handlers/shared/get-user-task-limit-and-role.ts +++ b/src/handlers/shared/get-user-task-limit-and-role.ts @@ -10,27 +10,26 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P const { config, logger } = context; const { maxConcurrentTasks } = config; - const smallestTask = Object.entries(maxConcurrentTasks).reduce( - (minTask, [role, limit]) => (limit < minTask.limit ? { role, limit } : minTask), - { role: "", limit: Infinity } as MatchingUserProps - ); + const smallestTask = Object.entries(maxConcurrentTasks).reduce((minTask, [role, limit]) => (limit < minTask.limit ? { role, limit } : minTask), { + role: "", + limit: Infinity, + } as MatchingUserProps); try { // Validate the organization login - if (typeof orgLogin !== 'string' || orgLogin.trim() === '') { - throw new Error('Invalid organization name'); + if (typeof orgLogin !== "string" || orgLogin.trim() === "") { + throw new Error("Invalid organization name"); } - + const response = await context.octokit.orgs.getMembershipForUser({ org: orgLogin, username: user, }); - - const role = response.data.role.toLowerCase() + + const role = response.data.role.toLowerCase(); const limit = maxConcurrentTasks[role]; - + return limit ? { role, limit } : smallestTask; - } catch (err) { logger.error("Could not get user role", { err }); return smallestTask; diff --git a/src/handlers/shared/start.ts b/src/handlers/shared/start.ts index 953cf6e1..f93b6d9e 100644 --- a/src/handlers/shared/start.ts +++ b/src/handlers/shared/start.ts @@ -147,26 +147,12 @@ async function handleTaskLimitChecks(username: string, context: Context, logger: // check for max and enforce max if (Math.abs(assignedIssues.length - openedPullRequests.length) >= limit) { - if (username !== sender) { - const logMessage = `${username} has reached their max task limit`; - const log = logger.error(logMessage, { - assignedIssues: assignedIssues.length, - openedPullRequests: openedPullRequests.length, - limit, - }); - - await addCommentToIssue(context, log.logMessage.diff); - } - -<<<<<<< max-assignments -======= - if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxConcurrentTasks) { 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, + limit, }); ->>>>>>> development + return false; } diff --git a/src/utils/issue.ts b/src/utils/issue.ts index cf8b6c78..fa9dbd92 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,6 +1,6 @@ import ms from "ms"; import { Context, isContextCommentCreated } from "../types/context"; -import { GitHubIssueSearch, Issue, Review } from "../types/payload"; +import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; export function isParentIssue(body: string) { From 7b4e031ef811373b9cefd1d9652c1108c43d770c Mon Sep 17 00:00:00 2001 From: jordan-ae Date: Thu, 12 Sep 2024 10:52:19 +0100 Subject: [PATCH 76/76] chore: remove limits for admins --- README.md | 1 - src/types/plugin-input.ts | 2 +- tests/main.test.ts | 17 +---------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d17ece76..d43e56fa 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ To configure your Ubiquibot to run this plugin, add the following to the `.ubiqu reviewDelayTolerance: "3 Days" taskStaleTimeoutDuration: "30 Days" maxConcurrentTasks: # Default concurrent task limits per role. - admin: 10 member: 5 contributor: 3 startRequiresWallet: true # default is true diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index f246517e..6278fb69 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -16,7 +16,7 @@ export const startStopSchema = T.Object( reviewDelayTolerance: T.String({ default: "1 Day" }), taskStaleTimeoutDuration: T.String({ default: "30 Days" }), startRequiresWallet: T.Boolean({ default: true }), - maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: 20, member: 10, contributor: 2 } }), + maxConcurrentTasks: T.Record(T.String(), T.Integer(), { default: { admin: Infinity, member: 10, contributor: 2 } }), emptyWalletText: T.String({ default: "Please set your wallet address with the /wallet command first and try again." }), }, { diff --git a/tests/main.test.ts b/tests/main.test.ts index 6d825c04..3f349dff 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -202,21 +202,6 @@ describe("User start/stop", () => { expect(memberLimit).toEqual(6); }); - test("should set maxLimits to 8 if the user is an admin", 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 adminLimit = maxConcurrentDefaults.admin; - - createIssuesForMaxAssignment(adminLimit + 4, sender.id); - const context = createContext(issue, sender) as unknown as Context; - - context.adapters = createAdapters(getSupabase(), context as unknown as Context); - await expect(userStartStop(context)).rejects.toThrow("You have reached your max task limit. Please close out some tasks before assigning new ones."); - - expect(adminLimit).toEqual(8); - }); - test("User can't start an issue if they have previously been unassigned by an admin", async () => { const issue = db.issue.findFirst({ where: { id: { equals: 6 } } }) as unknown as Issue; const sender = db.users.findFirst({ where: { id: { equals: 2 } } }) as unknown as PayloadSender; @@ -587,7 +572,7 @@ function createIssuesForMaxAssignment(n: number, userId: number) { } const maxConcurrentDefaults = { - admin: 8, + admin: Infinity, member: 6, contributor: 4, };