Skip to content

Commit

Permalink
fix: use task assignment event fallback for lastCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
Keyrxng committed Sep 12, 2024
1 parent 7ab97aa commit ca1bc9f
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/handlers/collect-linked-pulls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ export async function collectLinkedPullRequests(context: Context, issue: {
});

return result.repository.issue.closedByPullRequestsReferences.edges.map((edge) => edge.node);
}
}
116 changes: 78 additions & 38 deletions src/helpers/task-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function getTaskMetadata(
context: Context,
repo: ListForOrg["data"][0],
issue: ListIssueForRepo
) {
): Promise<{ metadata: { taskDeadline: string; taskAssignees: number[] }, lastCheck: DateTime } | false> {
const { logger, octokit } = context;

const comments = (await octokit.paginate(octokit.rest.issues.listComments, {
Expand All @@ -18,65 +18,105 @@ export async function getTaskMetadata(
})) as ListCommentsForIssue[];

const botComments = comments.filter((o) => o.user?.type === "Bot");
const taskDeadlineJsonRegex = /"taskDeadline": "([^"]*)"/g;
const taskAssigneesJsonRegex = /"taskAssignees": \[([^\]]*)\]/g;
// Has the bot assigned them, typically via the `/start` command
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()
)

// Has the bot previously reminded them?
const botFollowup = /<!-- Ubiquity - Followup - remindAssignees/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 botFollowupComments = botComments.filter((o) => botFollowup.test(o?.body || ""))
.sort((a, b) =>
DateTime.fromISO(a.created_at).toMillis() - DateTime.fromISO(b.created_at).toMillis()
);

// `lastCheck` represents the last time the bot intervened in the issue, separate from the activity tracking of a user.
const lastCheckComment = botFollowupComments[0]?.created_at ? botFollowupComments[0] : botAssignmentComments[0];
const lastCheck = DateTime.fromISO(lastCheckComment.created_at);
let lastCheck = lastCheckComment?.created_at ? DateTime.fromISO(lastCheckComment.created_at) : null;

// if we don't have a lastCheck yet, use the assignment event
if (!lastCheck) {
const assignmentEvents = await octokit.paginate(octokit.rest.issues.listEvents, {
owner: repo.owner.login,
repo: repo.name,
issue_number: issue.number
});

const taskDeadlineMatch = taskDeadlineJsonRegex.exec(botAssignmentComments[0]?.body || "");
const taskAssigneesMatch = taskAssigneesJsonRegex.exec(botAssignmentComments[0]?.body || "");
const assignmentEvent = assignmentEvents.find((o) => o.event === "assigned");
if (assignmentEvent) {
lastCheck = DateTime.fromISO(assignmentEvent.created_at);
} else {
logger.error(`Failed to find last check for ${issue.html_url}`);
return false;
}
}

if (!lastCheck) {
logger.error(`Failed to find last check for ${issue.html_url}`);
return false;
}

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] : [];
taskDeadline: "",
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 durationInMs = parseTimeLabel(issue.labels);

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 };
}

function parseTimeLabel(labels: (string | {
id?: number;
node_id?: string;
url?: string;
name?: string;
description?: string | null;
color?: string | null;
default?: boolean;
})[]): number {
let taskTimeEstimate = 0;

for (const label of labels) {
let timeLabel = "";
if (typeof label === "string") {
timeLabel = label;
} else {
timeLabel = label.name || "";
}
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;

if (timeLabel.startsWith("Time:")) {
const matched = timeLabel.match(/Time: <(\d+) (\w+)/i);
if (!matched) {
return 0;
}

const [_, duration, unit] = matched;
taskTimeEstimate = ms(`${duration} ${unit}`);
}

if (taskTimeEstimate) {
break;
}
metadata.taskDeadline = DateTime.fromMillis(lastCheck.toMillis() + durationInMs).toISO() || "";
}

return { metadata, lastCheck };
return taskTimeEstimate;
}
3 changes: 2 additions & 1 deletion src/types/github-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { RestEndpointMethodTypes } from "@octokit/rest";
export type ListForOrg = RestEndpointMethodTypes["repos"]["listForOrg"]["response"];
export type ListIssueForRepo = RestEndpointMethodTypes["issues"]["listForRepo"]["response"]["data"][0];
export type ListCommentsForIssue = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][0];
export type GitHubListEvents = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"][0];
export type GitHubListEvents = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"][0];
export type Label = RestEndpointMethodTypes["issues"]["getLabel"]["response"]["data"];
4 changes: 1 addition & 3 deletions tests/__mocks__/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { http, HttpResponse } from "msw";
import { db } from "./db";
import issueEventsGet from "./routes/get-events.json";
import issuesLabelsGet from "./routes/get-labels.json";
import issueTimeline from "./routes/get-timeline.json";

Expand All @@ -9,7 +8,7 @@ import issueTimeline from "./routes/get-timeline.json";
*/
export const handlers = [
http.get("https://api.github.com/repos/:owner/:repo/issues/:id/events", () => {
return HttpResponse.json(issueEventsGet);
return HttpResponse.json(db.event.getAll());
}),
http.get("https://api.github.com/repos/:owner/:repo/issues/:id/labels", () => {
return HttpResponse.json(issuesLabelsGet);
Expand Down Expand Up @@ -37,7 +36,6 @@ export const handlers = [
if (!comment) {
return HttpResponse.json({ message: "No body" });
}

db.issueComments.create({ issueId: Number(id), body: comment, created_at: new Date().toISOString(), id: db.issueComments.count() + 1, owner: { login: owner as string }, repo: { name: repo as string } });
return HttpResponse.json({ message: "Comment created" });
}),
Expand Down
49 changes: 46 additions & 3 deletions tests/__mocks__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { STRINGS, getIssueUrl, getIssueHtmlUrl } from "./strings";
export const ONE_DAY = 1000 * 60 * 60 * 24;

export function createRepo(name?: string, id?: number, owner?: string) {
db.repo.create({ ...repoTemplate, id: id || 1, name: name || repoTemplate.name, owner: { login: owner || STRINGS.UBIQUITY } });
db.repo.create({
...repoTemplate, id: id || 1, name: name || repoTemplate.name, owner: { login: owner || STRINGS.UBIQUITY },
url: `https://api.github.com/repos/${owner || STRINGS.UBIQUITY}/${name || repoTemplate.name}`,
html_url: `https://github.com/${owner || STRINGS.UBIQUITY}/${name || repoTemplate.name}`
});
}

export function createComment(
Expand Down Expand Up @@ -39,11 +43,50 @@ export function createIssue(
id,
assignees: assignees.length ? assignees : [{ login: STRINGS.UBIQUITY, id: 1 }],
created_at: created_at || new Date(Date.now() - ONE_DAY).toISOString(),
url: getIssueUrl(id),
html_url: getIssueHtmlUrl(id),
url: `https://github.com/${owner || STRINGS.UBIQUITY}/${repo || "test-repo"}/issues/${id}`,
html_url: `https://github.com/${owner || STRINGS.UBIQUITY}/${repo || "test-repo"}/issues/${id}`,
owner: { login: owner || STRINGS.UBIQUITY },
number: id,
repo: repo || "test-repo",
body: body || "test",
});
}

export function createEvent() {
db.event.create({
id: 1,
actor: {
id: 1,
type: "User",
login: "ubiquity",
name: null,
},
owner: "ubiquity",
repo: "test-repo",
issue_number: 1,
event: "assigned",
commit_id: null,
commit_url: "https://github.com/ubiquity/test-repo/commit/1",
created_at: new Date(Date.now() - ONE_DAY).toISOString(),
assignee: {
login: "ubiquity",
},
source: {
issue: {
number: 1,
html_url: getIssueHtmlUrl(1),
body: "test",
pull_request: {
merged_at: new Date(Date.now() - ONE_DAY).toISOString(),
},
repository: {
full_name: "ubiquity/test-repo",
},
state: "open",
user: {
login: "ubiquity",
},
}
}
});
}
4 changes: 4 additions & 0 deletions tests/__mocks__/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export function botAssignmentComment(assigneeId: number, deadlineStr: string) {
-->`;
}

export function lastCheckWasOn(deadlineStr: string) {
return `Last check was on ${deadlineStr}`;
}

export function updatingRemindersFor(repo: string) {
return `Updating reminders for ${repo}`;
}
Expand Down
Loading

0 comments on commit ca1bc9f

Please sign in to comment.