diff --git a/README.md b/README.md index 764f2f1..a5effb5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ yarn test with: disqualification: "7 days" warning: "3.5 days" + prioritySpeed: true watch: optOut: - "repoName" diff --git a/manifest.json b/manifest.json index f26c977..596e18a 100644 --- a/manifest.json +++ b/manifest.json @@ -23,6 +23,10 @@ } } }, + "prioritySpeed": { + "default": true, + "type": "boolean" + }, "disqualification": { "default": "7 days", "type": "string" diff --git a/src/helpers/task-metadata.ts b/src/helpers/task-metadata.ts index bdbcb3b..599d684 100644 --- a/src/helpers/task-metadata.ts +++ b/src/helpers/task-metadata.ts @@ -2,6 +2,11 @@ import { DateTime } from "luxon"; import ms from "ms"; import { ListForOrg, ListIssueForRepo } from "../types/github-types"; import { ContextPlugin } from "../types/plugin-input"; +import { RestEndpointMethodTypes } from "@octokit/rest"; + +type IssueLabel = Partial> & { + color?: string | null; +}; /** * Retrieves assignment events from the timeline of an issue and calculates the deadline based on the time label. @@ -67,20 +72,7 @@ export async function getTaskAssignmentDetails( return metadata; } -function parseTimeLabel( - labels: ( - | string - | { - id?: number; - node_id?: string; - url?: string; - name?: string; - description?: string | null; - color?: string | null; - default?: boolean; - } - )[] -): number { +function parseTimeLabel(labels: (IssueLabel | string)[]): number { let taskTimeEstimate = 0; for (const label of labels) { @@ -108,3 +100,25 @@ function parseTimeLabel( return taskTimeEstimate; } + +export function parsePriorityLabel(labels: (IssueLabel | string)[]): number { + for (const label of labels) { + let priorityLabel = ""; + if (typeof label === "string") { + priorityLabel = label; + } else { + priorityLabel = label.name || ""; + } + + if (priorityLabel.startsWith("Priority:")) { + const matched = priorityLabel.match(/Priority: (\d+)/i); + if (!matched) { + return 1; + } + + return Number(matched[1]); + } + } + + return 1; +} diff --git a/src/helpers/task-update.ts b/src/helpers/task-update.ts index 6c5ad7c..752abc6 100644 --- a/src/helpers/task-update.ts +++ b/src/helpers/task-update.ts @@ -8,7 +8,7 @@ import { getAssigneesActivityForIssue } from "./get-assignee-activity"; import { parseIssueUrl } from "./github-url"; import { remindAssigneesForIssue, unassignUserFromIssue } from "./remind-and-remove"; import { getCommentsFromMetadata } from "./structured-metadata"; -import { getTaskAssignmentDetails } from "./task-metadata"; +import { getTaskAssignmentDetails, parsePriorityLabel } from "./task-metadata"; const getMostRecentActivityDate = (assignedEventDate: DateTime, activityEventDate?: DateTime): DateTime => { return activityEventDate && activityEventDate > assignedEventDate ? activityEventDate : assignedEventDate; @@ -18,7 +18,7 @@ export async function updateTaskReminder(context: ContextPlugin, repo: ListForOr const { octokit, logger, - config: { eventWhitelist, warning, disqualification }, + config: { eventWhitelist, warning, disqualification, prioritySpeed }, } = context; const handledMetadata = await getTaskAssignmentDetails(context, repo, issue); const now = DateTime.local(); @@ -46,6 +46,8 @@ export async function updateTaskReminder(context: ContextPlugin, repo: ListForOr .shift(); const assignedDate = DateTime.fromISO(assignedEvent.created_at); + const priorityValue = parsePriorityLabel(issue.labels); + const priorityLevel = Math.max(1, priorityValue); const activityDate = activityEvent?.created_at ? DateTime.fromISO(activityEvent.created_at) : undefined; let mostRecentActivityDate = getMostRecentActivityDate(assignedDate, activityDate); @@ -75,16 +77,16 @@ export async function updateTaskReminder(context: ContextPlugin, repo: ListForOr if (lastReminderComment) { const lastReminderTime = DateTime.fromISO(lastReminderComment.created_at); mostRecentActivityDate = lastReminderTime > mostRecentActivityDate ? lastReminderTime : mostRecentActivityDate; - if (mostRecentActivityDate.plus({ milliseconds: disqualificationTimeDifference }) <= now) { + if (mostRecentActivityDate.plus({ milliseconds: prioritySpeed ? disqualificationTimeDifference / priorityLevel : disqualificationTimeDifference }) <= now) { await unassignUserFromIssue(context, issue); } else { logger.info(`Reminder was sent for ${issue.html_url} already, not beyond disqualification deadline yet.`); } } else { - if (mostRecentActivityDate.plus({ milliseconds: warning }) <= now) { + if (mostRecentActivityDate.plus({ milliseconds: prioritySpeed ? warning / priorityLevel : warning }) <= now) { await remindAssigneesForIssue(context, issue); } else { - logger.info(`Nothing to do for ${issue.html_url}, still within due-time.`); + logger.info(`Nothing to do for ${issue.html_url} still within due-time.`); } } } diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index fe0e96a..97509b7 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -66,6 +66,10 @@ export const pluginSettingsSchema = T.Object( }, { default: {} } ), + /* + * Whether to rush the follow ups by the priority level + */ + prioritySpeed: T.Boolean({ default: true }), /** * Delay to unassign users. 0 means disabled. Any other value is counted in days, e.g. 7 days */ diff --git a/tests/__mocks__/results/valid-configuration.json b/tests/__mocks__/results/valid-configuration.json index cbe85c8..9b50645 100644 --- a/tests/__mocks__/results/valid-configuration.json +++ b/tests/__mocks__/results/valid-configuration.json @@ -1,6 +1,7 @@ { "warning": "3.5 days", "disqualification": "7 days", + "prioritySpeed": true, "watch": { "optOut": ["private-repo"] }, diff --git a/tests/main.test.ts b/tests/main.test.ts index 41f732f..490fab6 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -50,6 +50,7 @@ describe("User start/stop", () => { expect(pluginSettings).toEqual({ pullRequestRequired: true, warning: 302400000, + prioritySpeed: true, disqualification: 604800000, watch: { optOut: [STRINGS.PRIVATE_REPO_NAME] }, eventWhitelist: ["review_requested", "ready_for_review", "commented", "committed"], @@ -103,6 +104,7 @@ describe("User start/stop", () => { pullRequestRequired: true, warning: ms("3.5 days"), disqualification: ms("7 days"), + prioritySpeed: true, watch: { optOut: [STRINGS.PRIVATE_REPO_NAME] }, eventWhitelist: ["review_requested", "ready_for_review", "commented", "committed"], }); @@ -121,7 +123,7 @@ describe("User start/stop", () => { await expect(run(context)).resolves.toEqual({ message: "OK" }); expect(errorSpy).toHaveBeenCalledWith(`Failed to update activity for ${getIssueHtmlUrl(1)}, there is no assigned event.`); - expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)}, still within due-time.`); + expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)} still within due-time.`); expect(infoSpy).toHaveBeenCalledWith(`Passed the reminder threshold on ${getIssueHtmlUrl(3)}, sending a reminder.`); expect(infoSpy).toHaveBeenCalledWith(`@user2, this task has been idle for a while. Please provide an update.\n\n`, { taskAssignees: [2], @@ -137,7 +139,7 @@ describe("User start/stop", () => { await expect(run(context)).resolves.toEqual({ message: "OK" }); - expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)}, still within due-time.`); + expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)} still within due-time.`); expect(infoSpy).toHaveBeenCalledWith(`Passed the reminder threshold on ${getIssueHtmlUrl(3)}, sending a reminder.`); expect(infoSpy).toHaveBeenCalledWith(`@user2, this task has been idle for a while. Please provide an update.\n\n`, { taskAssignees: [2], @@ -156,7 +158,7 @@ describe("User start/stop", () => { await run(context); - expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)}, still within due-time.`); + expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)} still within due-time.`); expect(infoSpy).toHaveBeenCalledWith(`Passed the reminder threshold on ${getIssueHtmlUrl(3)}, sending a reminder.`); expect(infoSpy).toHaveBeenCalledWith(`@user2, this task has been idle for a while. Please provide an update.\n\n`, { taskAssignees: [2], @@ -193,7 +195,7 @@ describe("User start/stop", () => { await run(context); - expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)}, still within due-time.`); + expect(infoSpy).toHaveBeenCalledWith(`Nothing to do for ${getIssueHtmlUrl(2)} still within due-time.`); const updatedIssue = db.issue.findFirst({ where: { id: { equals: 1 } } }); expect(updatedIssue?.assignees).toEqual([{ login: STRINGS.UBIQUITY, id: 1 }]); @@ -279,6 +281,7 @@ function createContext(issueId: number, senderId: number, optOut = [STRINGS.PRIV config: { disqualification: ONE_DAY * 7, warning: ONE_DAY * 3.5, + prioritySpeed: true, watch: { optOut }, eventWhitelist: ["review_requested", "ready_for_review", "commented", "committed"], pullRequestRequired: false,