diff --git a/package.json b/package.json index a8b41f6fa..16e1d4240 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,7 @@ "prettier": "^2.7.1", "probot": "^12.2.4", "tsx": "^3.12.7", - "yaml": "^2.2.2", - "zod": "^3.22.4" + "yaml": "^2.2.2" }, "devDependencies": { "@types/dotenv": "^8.2.0", diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts index 78a4a8819..d7a92ae1c 100644 --- a/src/handlers/assign/action.ts +++ b/src/handlers/assign/action.ts @@ -31,9 +31,9 @@ export async function startCommandHandler(context: Context) { } // Filter out labels that match the time labels defined in the config - const timeLabelsAssigned: Label[] = labels.filter((label) => - typeof label === "string" || typeof label === "object" - ? config.labels.time.some((item) => item.name === label.name) + const timeLabelsAssigned: Label[] = labels.filter((assignedLabel) => + typeof assignedLabel === "string" || typeof assignedLabel === "object" + ? config.labels.time.some((label) => label === assignedLabel.name) : false ); @@ -44,8 +44,8 @@ export async function startCommandHandler(context: Context) { // Sort labels by weight and select the one with the smallest weight const sortedLabels = timeLabelsAssigned .sort((a, b) => { - const fullLabelA = labels.find((label) => label.name === a.name); - const fullLabelB = labels.find((label) => label.name === b.name); + const fullLabelA = labels.find((label) => label.name === a.name)?.name; + const fullLabelB = labels.find((label) => label.name === b.name)?.name; if (!fullLabelA || !fullLabelB) { return 0; // return a default value diff --git a/src/handlers/comment/action.ts b/src/handlers/comment/action.ts index d939ccec7..15314dbe3 100644 --- a/src/handlers/comment/action.ts +++ b/src/handlers/comment/action.ts @@ -33,9 +33,9 @@ export async function commentCreatedOrEdited(context: Context) { const { id, handler } = userCommand; logger.info("Running a comment handler", { id, handler: handler.name }); - const feature = config.commands.find((e) => e.name === id.split("/")[1]); + const disabled = config.disabledCommands.some((command) => command === id.replace("/", "")); - if (feature?.enabled === false && id !== "/help") { + if (disabled && id !== "/help") { return logger.warn("Skipping because it is disabled on this repo.", { id }); } diff --git a/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts b/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts index c6de00a40..332ffe482 100644 --- a/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts +++ b/src/handlers/comment/handlers/assign/get-time-labels-assigned.ts @@ -15,7 +15,7 @@ export function getTimeLabelsAssigned(payload: Payload, config: BotConfig) { const _labelType = typeof _label; const _labelName = _labelType === "string" ? _label.toString() : _labelType === "object" ? _label.name : "unknown"; - const timeLabel = timeLabelsDefined.find((item) => item.name === _labelName); + const timeLabel = timeLabelsDefined.find((label) => label === _labelName); if (timeLabel) { timeLabelsAssigned.push(_label); } diff --git a/src/handlers/comment/handlers/assign/index.ts b/src/handlers/comment/handlers/assign/index.ts index 970a89e89..29b03ec63 100644 --- a/src/handlers/comment/handlers/assign/index.ts +++ b/src/handlers/comment/handlers/assign/index.ts @@ -22,10 +22,10 @@ export async function assign(context: Context, body: string) { const { miscellaneous: { maxConcurrentTasks }, timers: { taskStaleTimeoutDuration }, - commands, + disabledCommands, } = context.config; - const startEnabled = commands.find((command) => command.name === "start"); + const startDisabled = disabledCommands.some((command) => command === "start"); logger.info("Received '/start' command", { sender: payload.sender.login, body }); @@ -33,7 +33,7 @@ export async function assign(context: Context, body: string) { throw logger.warn(`Skipping '/start' because of no issue instance`); } - if (!startEnabled?.enabled) { + if (startDisabled) { throw logger.warn("The `/assign` command is disabled for this repository."); } @@ -74,13 +74,13 @@ export async function assign(context: Context, body: string) { const labels = issue.labels; const priceLabel = labels.find((label) => label.name.startsWith("Price: ")); - let duration = null; + let duration: number | null = null; if (!priceLabel) { throw logger.warn("Skipping '/start' since no price label is set to calculate the timeline", priceLabel); } else { const timeLabelsAssigned = getTimeLabelsAssigned(payload, config); if (timeLabelsAssigned) { - duration = calculateDurations(timeLabelsAssigned).shift(); + duration = calculateDurations(timeLabelsAssigned).shift() || null; } } diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts index 970db8f65..c5d11b288 100644 --- a/src/handlers/comment/handlers/help.ts +++ b/src/handlers/comment/handlers/help.ts @@ -20,7 +20,7 @@ export async function listAvailableCommands(context: Context, body: string) { export function generateHelpMenu(context: Context) { const config = context.config; - const startEnabled = config.commands.find((command) => command.name === "start"); + const startDisabled = config.disabledCommands.some((command) => command === "start"); let helpMenu = "### Available Commands\n\n| Command | Description | Example |\n| --- | --- | --- |\n"; const commands = userCommands(config.miscellaneous.registerWalletWithVerification); @@ -31,7 +31,7 @@ export function generateHelpMenu(context: Context) { } |\n`) // add to help menu ); - if (!startEnabled) { + if (startDisabled) { helpMenu += "\n\n***_To assign yourself to an issue, please open a draft pull request that is linked to it._***"; } return helpMenu; diff --git a/src/handlers/pricing/action.ts b/src/handlers/pricing/action.ts index 9c9cfaad5..1b9f8b3e4 100644 --- a/src/handlers/pricing/action.ts +++ b/src/handlers/pricing/action.ts @@ -12,7 +12,7 @@ export async function handleParentIssue(context: Context, labels: Label[]) { } export function sortLabelsByValue(labels: Label[]) { - return labels.sort((a, b) => calculateLabelValue(a) - calculateLabelValue(b)); + return labels.sort((a, b) => calculateLabelValue(a.name) - calculateLabelValue(b.name)); } export function isParentIssue(body: string) { diff --git a/src/handlers/pricing/pre.ts b/src/handlers/pricing/pre.ts index fc6ef513e..1650589a6 100644 --- a/src/handlers/pricing/pre.ts +++ b/src/handlers/pricing/pre.ts @@ -13,7 +13,6 @@ export async function syncPriceLabelsToConfig(context: Context) { const { features: { assistivePricing }, - labels, } = config; if (!assistivePricing) { @@ -21,8 +20,6 @@ export async function syncPriceLabelsToConfig(context: Context) { return; } - const timeLabels = labels.time.map((i) => i.name); - const priorityLabels = labels.priority.map((i) => i.name); const aiLabels: string[] = []; for (const timeLabel of config.labels.time) { for (const priorityLabel of config.labels.priority) { @@ -37,7 +34,7 @@ export async function syncPriceLabelsToConfig(context: Context) { } } - const pricingLabels: string[] = [...timeLabels, ...priorityLabels]; + const pricingLabels: string[] = [...config.labels.time, ...config.labels.priority]; // List all the labels for a repository const allLabels = await listLabelsForRepo(context); diff --git a/src/handlers/pricing/pricing-label.ts b/src/handlers/pricing/pricing-label.ts index f8d0467cb..2817f4253 100644 --- a/src/handlers/pricing/pricing-label.ts +++ b/src/handlers/pricing/pricing-label.ts @@ -82,8 +82,9 @@ export async function onLabelChangeSetPricing(context: Context): Promise { } function getRecognizedLabels(labels: Label[], config: BotConfig) { - const isRecognizedLabel = (label: Label, labelConfig: { name: string }[]) => - (typeof label === "string" || typeof label === "object") && labelConfig.some((item) => item.name === label.name); + const isRecognizedLabel = (label: Label, configLabels: string[]) => + (typeof label === "string" || typeof label === "object") && + configLabels.some((configLabel) => configLabel === label.name); const recognizedTimeLabels: Label[] = labels.filter((label: Label) => isRecognizedLabel(label, config.labels.time)); diff --git a/src/handlers/shared/pricing.ts b/src/handlers/shared/pricing.ts index 325216a21..0f2df4dcb 100644 --- a/src/handlers/shared/pricing.ts +++ b/src/handlers/shared/pricing.ts @@ -21,10 +21,10 @@ export function setPrice(context: Context, timeLabel: Label, priorityLabel: Labe if (!timeLabel || !priorityLabel) throw logger.warn("Time or priority label is not defined"); - const recognizedTimeLabels = labels.time.find((item) => item.name === timeLabel.name); + const recognizedTimeLabels = labels.time.find((configLabel) => configLabel === timeLabel.name); if (!recognizedTimeLabels) throw logger.warn("Time label is not recognized"); - const recognizedPriorityLabels = labels.priority.find((item) => item.name === priorityLabel.name); + const recognizedPriorityLabels = labels.priority.find((configLabel) => configLabel === priorityLabel.name); if (!recognizedPriorityLabels) throw logger.warn("Priority label is not recognized"); const timeValue = calculateLabelValue(recognizedTimeLabels); diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts index 003b242df..177df86ce 100644 --- a/src/handlers/wildcard/analytics.ts +++ b/src/handlers/wildcard/analytics.ts @@ -13,19 +13,19 @@ export function taskPaymentMetaData( } { const { labels } = context.config; - const timeLabels = labels.time.filter((item) => issue.labels.map((i) => i.name).includes(item.name)); - const priorityLabels = labels.priority.filter((item) => issue.labels.map((i) => i.name).includes(item.name)); + const timeLabels = labels.time.filter((configLabel) => issue.labels.map((i) => i.name).includes(configLabel)); + const priorityLabels = labels.priority.filter((configLabel) => issue.labels.map((i) => i.name).includes(configLabel)); const isTask = timeLabels.length > 0 && priorityLabels.length > 0; const minTimeLabel = timeLabels.length > 0 - ? timeLabels.reduce((a, b) => (calculateLabelValue(a) < calculateLabelValue(b) ? a : b)).name + ? timeLabels.reduce((a, b) => (calculateLabelValue(a) < calculateLabelValue(b) ? a : b)) : null; const minPriorityLabel = priorityLabels.length > 0 - ? priorityLabels.reduce((a, b) => (calculateLabelValue(a) < calculateLabelValue(b) ? a : b)).name + ? priorityLabels.reduce((a, b) => (calculateLabelValue(a) < calculateLabelValue(b) ? a : b)) : null; const priceLabel = issue.labels.find((label) => label.name.includes("Price"))?.name || null; diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index fce30fe1a..4ec9e30ee 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -19,16 +19,16 @@ export function shouldSkip(context: ProbotContext) { return response; } -export function calculateLabelValue(label: { name: string }): number { - const matches = label.name.match(/\d+/); +export function calculateLabelValue(label: string): number { + const matches = label.match(/\d+/); const number = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0; - if (label.name.toLowerCase().includes("priority")) return number; - // throw new Error(`Label ${label.name} is not a priority label`); - if (label.name.toLowerCase().includes("minute")) return number * 0.002; - if (label.name.toLowerCase().includes("hour")) return number * 0.125; - if (label.name.toLowerCase().includes("day")) return 1 + (number - 1) * 0.25; - if (label.name.toLowerCase().includes("week")) return number + 1; - if (label.name.toLowerCase().includes("month")) return 5 + (number - 1) * 8; + if (label.toLowerCase().includes("priority")) return number; + // throw new Error(`Label ${label} is not a priority label`); + if (label.toLowerCase().includes("minute")) return number * 0.002; + if (label.toLowerCase().includes("hour")) return number * 0.125; + if (label.toLowerCase().includes("day")) return 1 + (number - 1) * 0.25; + if (label.toLowerCase().includes("week")) return number + 1; + if (label.toLowerCase().includes("month")) return 5 + (number - 1) * 8; return 0; } diff --git a/src/types/configuration-types.ts b/src/types/configuration-types.ts index e16f946aa..136de6812 100644 --- a/src/types/configuration-types.ts +++ b/src/types/configuration-types.ts @@ -17,22 +17,16 @@ const allHtmlElementsSetToZero = validHTMLElements.reduce((accumulator, current) return accumulator; }, {} as Record); -const allCommands = userCommands(false).map((cmd) => ({ name: cmd.id.replace("/", ""), enabled: false })); +const allCommands = userCommands(false).map((cmd) => cmd.id.replace("/", "")); -const defaultTimeLabels = [ - { name: "Time: <1 Hour" }, - { name: "Time: <2 Hours" }, - { name: "Time: <4 Hours" }, - { name: "Time: <1 Day" }, - { name: "Time: <1 Week" }, -]; +const defaultTimeLabels = ["Time: <1 Hour", "Time: <2 Hours", "Time: <4 Hours", "Time: <1 Day", "Time: <1 Week"]; const defaultPriorityLabels = [ - { name: "Priority: 1 (Normal)" }, - { name: "Priority: 2 (Medium)" }, - { name: "Priority: 3 (High)" }, - { name: "Priority: 4 (Urgent)" }, - { name: "Priority: 5 (Emergency)" }, + "Priority: 1 (Normal)", + "Priority: 2 (Medium)", + "Priority: 3 (High)", + "Priority: 4 (Urgent)", + "Priority: 5 (Emergency)", ]; function StrictObject(obj: T, options?: ObjectOptions) { @@ -101,13 +95,7 @@ export const BotConfigSchema = StrictObject( basePriceMultiplier: T.Number({ default: 1 }), issueCreatorMultiplier: T.Number({ default: 1 }), }), - commands: T.Array( - StrictObject({ - name: T.String(), - enabled: T.Boolean(), - }), - { default: allCommands } - ), + disabledCommands: T.Array(T.String(), { default: allCommands }), incentives: StrictObject({ comment: StrictObject({ elements: T.Record(T.Union(HtmlEntities), T.Number({ default: 0 }), { default: allHtmlElementsSetToZero }), @@ -121,8 +109,8 @@ export const BotConfigSchema = StrictObject( }), }), labels: StrictObject({ - time: T.Array(StrictObject({ name: T.String() }), { default: defaultTimeLabels }), - priority: T.Array(StrictObject({ name: T.String() }), { default: defaultPriorityLabels }), + time: T.Array(T.String(), { default: defaultTimeLabels }), + priority: T.Array(T.String(), { default: defaultPriorityLabels }), }), miscellaneous: StrictObject({ maxConcurrentTasks: T.Number({ default: Number.MAX_SAFE_INTEGER }), diff --git a/src/utils/generate-configuration.ts b/src/utils/generate-configuration.ts index 3ab56a1b0..35162bf7c 100644 --- a/src/utils/generate-configuration.ts +++ b/src/utils/generate-configuration.ts @@ -1,11 +1,10 @@ import { Value } from "@sinclair/typebox/value"; import { DefinedError } from "ajv"; -import merge from "lodash/merge"; +import mergeWith from "lodash/merge"; import { Context as ProbotContext } from "probot"; import YAML from "yaml"; import Runtime from "../bindings/bot-runtime"; import { BotConfig, Payload, stringDuration, validateBotConfig } from "../types"; -import ubiquibotConfigDefault from "../ubiquibot-config-default"; const UBIQUIBOT_CONFIG_REPOSITORY = "ubiquibot-config"; const UBIQUIBOT_CONFIG_FULL_PATH = ".github/ubiquibot-config.yml"; @@ -13,7 +12,7 @@ const UBIQUIBOT_CONFIG_FULL_PATH = ".github/ubiquibot-config.yml"; export async function generateConfiguration(context: ProbotContext): Promise { const payload = context.payload as Payload; - const organizationConfiguration = parseYaml( + const orgConfig = parseYaml( await download({ context, repository: UBIQUIBOT_CONFIG_REPOSITORY, @@ -21,7 +20,7 @@ export async function generateConfiguration(context: ProbotContext): Promise { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + // if it's string array, concat and remove duplicates + if (objValue.every((value) => typeof value === "string")) { + return [...new Set(objValue.concat(srcValue))]; } + // otherwise just concat + return objValue.concat(srcValue); } - orgConfig = organizationConfiguration as BotConfig; - } - - let repoConfig: BotConfig | undefined; - if (repositoryConfiguration) { - const valid = validateBotConfig(repositoryConfiguration); - if (!valid) { - let errMsg = getErrorMsg(validateBotConfig.errors as DefinedError[]); - if (errMsg) { - errMsg = `Invalid repo configuration! \n${errMsg}`; - if (payload.issue?.number) - await context.octokit.issues.createComment({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: payload.issue?.number, - body: errMsg, - }); - throw new Error(errMsg); - } - } - repoConfig = repositoryConfiguration as BotConfig; - } + }); - const merged = merge(ubiquibotConfigDefault, orgConfig, repoConfig); const valid = validateBotConfig(merged); if (!valid) { let errMsg = getErrorMsg(validateBotConfig.errors as DefinedError[]); diff --git a/yarn.lock b/yarn.lock index a437cf3dd..aad1181a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8797,7 +8797,7 @@ zod-validation-error@^1.5.0: resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.5.0.tgz#2b355007a1c3b7fb04fa476bfad4e7b3fd5491e3" integrity sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw== -zod@3.22.4, zod@^3.22.4: +zod@3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==