forked from ubiquity-os-marketplace/daemon-disqualifier
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: code clean up and verbose logging
- Loading branch information
Showing
8 changed files
with
323 additions
and
260 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { getWatchedRepos } from "../helpers/get-watched-repos"; | ||
import { updateTaskReminder } from "../helpers/task-update"; | ||
import { Context } from "../types/context"; | ||
import { ListForOrg, ListIssueForRepo } from "../types/github-types"; | ||
|
||
export async function watchUserActivity(context: Context) { | ||
const { logger } = context; | ||
|
||
const repos = await getWatchedRepos(context); | ||
|
||
if (!repos?.length) { | ||
logger.info("No watched repos have been found, no work to do."); | ||
return false; | ||
} | ||
|
||
for (const repo of repos) { | ||
await updateReminders(context, repo); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
async function updateReminders(context: Context, repo: ListForOrg["data"][0]) { | ||
const { logger, octokit, payload } = context; | ||
const issues = (await octokit.paginate(octokit.rest.issues.listForRepo, { | ||
owner: payload.repository.owner.login, | ||
repo: repo.name, | ||
per_page: 100, | ||
state: "open", | ||
})) as ListIssueForRepo[]; | ||
|
||
for (const issue of issues) { | ||
// I think we can safely ignore the following | ||
if (issue.draft || issue.pull_request || issue.locked || issue.state !== "open") { | ||
continue; | ||
} | ||
|
||
if (issue.assignees?.length || issue.assignee) { | ||
logger.debug(`Checking assigned issue: ${issue.html_url}`); | ||
await updateTaskReminder(context, repo, issue); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { DateTime } from "luxon"; | ||
import { collectLinkedPullRequests } from "../handlers/collect-linked-pulls"; | ||
import { Context } from "../types/context"; | ||
import { parseIssueUrl } from "./github-url"; | ||
import { GitHubListEvents, ListIssueForRepo } from "../types/github-types"; | ||
|
||
/** | ||
* Retrieves all the activity for users that are assigned to the issue. Also takes into account linked pull requests. | ||
*/ | ||
export async function getAssigneesActivityForIssue(context: Context, issue: ListIssueForRepo, assigneeIds: number[]) { | ||
const gitHubUrl = parseIssueUrl(issue.html_url); | ||
const issueEvents: GitHubListEvents[] = await context.octokit.paginate(context.octokit.rest.issues.listEvents, { | ||
owner: gitHubUrl.owner, | ||
repo: gitHubUrl.repo, | ||
issue_number: gitHubUrl.issue_number, | ||
per_page: 100, | ||
}); | ||
const linkedPullRequests = await collectLinkedPullRequests(context, gitHubUrl); | ||
for (const linkedPullRequest of linkedPullRequests) { | ||
const { owner, repo, issue_number } = parseIssueUrl(linkedPullRequest.pull_request?.html_url || ""); | ||
const events = await context.octokit.paginate(context.octokit.rest.issues.listEvents, { | ||
owner, | ||
repo, | ||
issue_number, | ||
per_page: 100, | ||
}); | ||
issueEvents.push(...events); | ||
} | ||
|
||
return issueEvents | ||
.reduce((acc, event) => { | ||
if (event.actor && event.actor.id) { | ||
if (assigneeIds.includes(event.actor.id)) acc.push(event); | ||
} | ||
return acc; | ||
}, [] as GitHubListEvents[]) | ||
.sort((a, b) => DateTime.fromISO(b.created_at).toMillis() - DateTime.fromISO(a.created_at).toMillis()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Context } from "../types/context"; | ||
import { parseIssueUrl } from "./github-url"; | ||
import { ListIssueForRepo } from "../types/github-types"; | ||
|
||
export async function unassignUserFromIssue(context: Context, issue: ListIssueForRepo) { | ||
const { logger, config } = context; | ||
|
||
if (config.disqualification <= 0) { | ||
logger.info("The unassign threshold is <= 0, won't unassign users."); | ||
} else { | ||
logger.info(`Passed the deadline on ${issue.html_url} and no activity is detected, removing assignees.`); | ||
await removeAllAssignees(context, issue); | ||
} | ||
} | ||
|
||
export async function remindAssigneesForIssue(context: Context, issue: ListIssueForRepo) { | ||
const { logger, config } = context; | ||
if (config.warning <= 0) { | ||
logger.info("The reminder threshold is <= 0, won't send any reminder."); | ||
} else { | ||
logger.info(`Passed the reminder threshold on ${issue.html_url}, sending a reminder.`); | ||
await remindAssignees(context, issue); | ||
} | ||
} | ||
|
||
async function remindAssignees(context: Context, issue: ListIssueForRepo) { | ||
const { octokit, logger } = context; | ||
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url); | ||
|
||
if (!issue?.assignees?.length) { | ||
logger.error(`Missing Assignees from ${issue.html_url}`); | ||
return false; | ||
} | ||
const logins = issue.assignees | ||
.map((o) => o?.login) | ||
.filter((o) => !!o) | ||
.join(", @"); | ||
|
||
await octokit.rest.issues.createComment({ | ||
owner, | ||
repo, | ||
issue_number, | ||
body: `@${logins}, this task has been idle for a while. Please provide an update.`, | ||
}); | ||
return true; | ||
} | ||
|
||
async function removeAllAssignees(context: Context, issue: ListIssueForRepo) { | ||
const { octokit, logger } = context; | ||
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url); | ||
|
||
if (!issue?.assignees?.length) { | ||
logger.error(`Missing Assignees from ${issue.html_url}`); | ||
return false; | ||
} | ||
const logins = issue.assignees.map((o) => o?.login).filter((o) => !!o) as string[]; | ||
await octokit.rest.issues.removeAssignees({ | ||
owner, | ||
repo, | ||
issue_number, | ||
assignees: logins, | ||
}); | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { DateTime } from "luxon"; | ||
import { Context } from "../types/context"; | ||
import { ListIssueForRepo } from "../types/github-types"; | ||
import { getAssigneesActivityForIssue } from "./get-assignee-activity"; | ||
|
||
export async function handleDeadline( | ||
context: Context, | ||
metadata: { | ||
taskDeadline: string; | ||
taskAssignees: number[] | undefined; | ||
}, | ||
issue: ListIssueForRepo, | ||
lastCheck: DateTime, | ||
) { | ||
const { logger, config } = context; | ||
|
||
const assigneeIds = issue.assignees?.map((o) => o.id) || []; | ||
|
||
if (assigneeIds.length && metadata.taskAssignees?.some((a) => !assigneeIds.includes(a))) { | ||
logger.info(`Assignees mismatch found for ${issue.html_url}`, { | ||
metadata: metadata.taskAssignees, | ||
issue: assigneeIds, | ||
}); | ||
return false; | ||
} | ||
|
||
const deadline = DateTime.fromISO(metadata.taskDeadline); | ||
const now = DateTime.now(); | ||
|
||
if (!deadline.isValid && !lastCheck.isValid) { | ||
logger.error(`Invalid date found on ${issue.html_url}`); | ||
return false; | ||
} | ||
|
||
const activity = (await getAssigneesActivityForIssue(context, issue, assigneeIds)).filter((o) => { | ||
return DateTime.fromISO(o.created_at) > lastCheck; | ||
}) | ||
|
||
let deadlineWithThreshold = deadline.plus({ milliseconds: config.disqualification }); | ||
let reminderWithThreshold = deadline.plus({ milliseconds: config.warning }); | ||
|
||
if (activity?.length) { | ||
const lastActivity = DateTime.fromISO(activity[0].created_at); | ||
deadlineWithThreshold = lastActivity.plus({ milliseconds: config.disqualification }); | ||
reminderWithThreshold = lastActivity.plus({ milliseconds: config.warning }); | ||
} | ||
|
||
return { deadlineWithThreshold, reminderWithThreshold, now }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { DateTime } from "luxon"; | ||
import { Context } from "../types/context"; | ||
import { ListCommentsForIssue, ListForOrg, ListIssueForRepo } from "../types/github-types"; | ||
import ms from "ms"; | ||
|
||
export async function handleCommentsAndMetadata( | ||
context: Context, | ||
repo: ListForOrg["data"][0], | ||
issue: ListIssueForRepo | ||
) { | ||
const { logger, octokit } = context; | ||
|
||
const comments = (await octokit.paginate(octokit.rest.issues.listComments, { | ||
owner: repo.owner.login, | ||
repo: repo.name, | ||
issue_number: issue.number, | ||
per_page: 100, | ||
})) as ListCommentsForIssue[]; | ||
|
||
const botComments = comments.filter((o) => o.user?.type === "Bot"); | ||
const taskDeadlineJsonRegex = /"taskDeadline": "([^"]*)"/g; | ||
const taskAssigneesJsonRegex = /"taskAssignees": \[([^\]]*)\]/g; | ||
const assignmentRegex = /Ubiquity - Assignment - start -/gi; | ||
const botAssignmentComments = botComments.filter( | ||
(o) => assignmentRegex.test(o?.body || "") | ||
).sort((a, b) => | ||
DateTime.fromISO(a.created_at).toMillis() - DateTime.fromISO(b.created_at).toMillis() | ||
) | ||
|
||
const botFollowup = /this task has been idle for a while. Please provide an update./gi; | ||
const botFollowupComments = botComments.filter((o) => botFollowup.test(o?.body || "")); | ||
|
||
if (!botAssignmentComments.length && !botFollowupComments.length) { | ||
logger.info(`No assignment or followup comments found for ${issue.html_url}`); | ||
return false; | ||
} | ||
|
||
const lastCheckComment = botFollowupComments[0]?.created_at ? botFollowupComments[0] : botAssignmentComments[0]; | ||
const lastCheck = DateTime.fromISO(lastCheckComment.created_at); | ||
|
||
const taskDeadlineMatch = taskDeadlineJsonRegex.exec(botAssignmentComments[0]?.body || ""); | ||
const taskAssigneesMatch = taskAssigneesJsonRegex.exec(botAssignmentComments[0]?.body || ""); | ||
|
||
const metadata = { | ||
taskDeadline: taskDeadlineMatch?.[1] || "", | ||
taskAssignees: taskAssigneesMatch?.[1] | ||
.split(",") | ||
.map((o) => o.trim()) | ||
.map(Number), | ||
}; | ||
|
||
// supporting legacy format | ||
if (!metadata.taskAssignees?.length) { | ||
metadata.taskAssignees = issue.assignees ? issue.assignees.map((o) => o.id) : issue.assignee ? [issue.assignee.id] : []; | ||
} | ||
|
||
if (!metadata.taskAssignees?.length) { | ||
logger.error(`Missing Assignees from ${issue.html_url}`); | ||
return false; | ||
} | ||
|
||
if (!metadata.taskDeadline) { | ||
const taskDeadlineJsonRegex = /"duration": ([^,]*),/g; | ||
const taskDeadlineMatch = taskDeadlineJsonRegex.exec(botAssignmentComments[0]?.body || ""); | ||
if (!taskDeadlineMatch) { | ||
logger.error(`Missing deadline from ${issue.html_url}`); | ||
return false; | ||
} | ||
const duration = taskDeadlineMatch[1] || ""; | ||
const durationInMs = ms(duration); | ||
if (durationInMs === 0) { | ||
// it could mean there was no time label set on the issue | ||
// but it could still be workable and priced | ||
} else if (durationInMs < 0 || !durationInMs) { | ||
logger.error(`Invalid deadline found on ${issue.html_url}`); | ||
return false; | ||
} | ||
metadata.taskDeadline = DateTime.fromMillis(lastCheck.toMillis() + durationInMs).toISO() || ""; | ||
} | ||
|
||
return { metadata, lastCheck }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { DateTime } from "luxon"; | ||
import { Context } from "../types/context"; | ||
import { ListForOrg, ListIssueForRepo } from "../types/github-types"; | ||
import { remindAssigneesForIssue, unassignUserFromIssue } from "./remind-and-remove"; | ||
import { handleDeadline } from "./task-deadline"; | ||
import { handleCommentsAndMetadata } from "./task-metadata"; | ||
|
||
export async function updateTaskReminder(context: Context, repo: ListForOrg["data"][0], issue: ListIssueForRepo) { | ||
const { logger } = context; | ||
|
||
let metadata, lastCheck, deadlineWithThreshold, reminderWithThreshold, now; | ||
|
||
const handledMetadata = await handleCommentsAndMetadata(context, repo, issue); | ||
|
||
if (handledMetadata) { | ||
metadata = handledMetadata.metadata; | ||
lastCheck = handledMetadata.lastCheck; | ||
|
||
const handledDeadline = await handleDeadline(context, metadata, issue, lastCheck); | ||
if (handledDeadline) { | ||
deadlineWithThreshold = handledDeadline.deadlineWithThreshold; | ||
reminderWithThreshold = handledDeadline.reminderWithThreshold; | ||
now = handledDeadline.now; | ||
} | ||
} | ||
|
||
if (!metadata || !lastCheck || !deadlineWithThreshold || !reminderWithThreshold || !now) { | ||
logger.error(`Failed to handle metadata or deadline for ${issue.html_url}`); | ||
return false; | ||
} | ||
|
||
if (now >= deadlineWithThreshold) { | ||
await unassignUserFromIssue(context, issue); | ||
} else if (now >= reminderWithThreshold) { | ||
await remindAssigneesForIssue(context, issue); | ||
} else { | ||
logger.info(`Nothing to do for ${issue.html_url}, still within due-time.`); | ||
logger.info(`Last check was on ${lastCheck.toISO()}`, { | ||
now: now.toLocaleString(DateTime.DATETIME_MED), | ||
reminder: reminderWithThreshold.toLocaleString(DateTime.DATETIME_MED), | ||
deadline: deadlineWithThreshold.toLocaleString(DateTime.DATETIME_MED), | ||
}); | ||
} | ||
} |
Oops, something went wrong.