From 90e01cff93c472755b463c38ae391f22ca650356 Mon Sep 17 00:00:00 2001 From: Ted Spare Date: Tue, 4 Jul 2023 16:51:29 -0400 Subject: [PATCH] Add exit conditions to assignee sync (#109) * Handle removal of one of many GH assignees * Make assignee sync more robust * Handle assignment of unknown user --- utils/webhook/github.handler.ts | 87 ++++++++++++++++++------------ utils/webhook/linear.handler.ts | 94 ++++++++++++++++----------------- 2 files changed, 101 insertions(+), 80 deletions(-) diff --git a/utils/webhook/github.handler.ts b/utils/webhook/github.handler.ts index 2de37bd..2bfd773 100644 --- a/utils/webhook/github.handler.ts +++ b/utils/webhook/github.handler.ts @@ -13,7 +13,9 @@ import { replaceMentions, upsertUser } from "../../pages/api/utils"; import { Issue, IssueCommentCreatedEvent, + IssuesAssignedEvent, IssuesEvent, + IssuesUnassignedEvent, MilestoneEvent, Repository, User @@ -483,52 +485,71 @@ export async function githubWebhookHandler( return reason; } - const { assignee } = issue; + const { assignee: modifiedAssignee } = body as + | IssuesAssignedEvent + | IssuesUnassignedEvent; - if (!assignee?.id && action === "unassigned") { + const ticket = await linear.issue(syncedIssue.linearIssueId); + const linearAssignee = await ticket?.assignee; + + const remainingAssignee = issue?.assignee?.id + ? await prisma.user.findFirst({ + where: { githubUserId: issue?.assignee?.id }, + select: { linearUserId: true } + }) + : null; + + if (action === "unassigned") { // Remove assignee - const response = await linear.issueUpdate( - syncedIssue.linearIssueId, - { assigneeId: null } - ); + // Set remaining assignee only if different from current + if (linearAssignee?.id != remainingAssignee?.linearUserId) { + const response = await linear.issueUpdate( + syncedIssue.linearIssueId, + { assigneeId: remainingAssignee?.linearUserId || null } + ); - if (!response?.success) { - const reason = `Failed to remove assignee on Linear ticket for GitHub issue #${issue.number}.`; - console.log(reason); - throw new ApiError(reason, 500); - } else { - const reason = `Removed assignee from Linear ticket for GitHub issue #${issue.number}.`; - console.log(reason); - return reason; + if (!response?.success) { + const reason = `Failed to remove assignee on Linear ticket for GitHub issue #${issue.number}.`; + console.log(reason); + throw new ApiError(reason, 500); + } else { + const reason = `Removed assignee from Linear ticket for GitHub issue #${issue.number}.`; + console.log(reason); + return reason; + } } - } else { + } else if (action === "assigned") { // Add assignee - const user = await prisma.user.findFirst({ - where: { githubUserId: assignee?.id }, - select: { linearUserId: true } - }); + const newAssignee = modifiedAssignee?.id + ? await prisma.user.findFirst({ + where: { githubUserId: modifiedAssignee?.id }, + select: { linearUserId: true } + }) + : null; - if (!user) { - const reason = `Skipping assignee change for issue #${issue.number} as no Linear username was found for GitHub user ${assignee?.login}.`; + if (!newAssignee) { + const reason = `Skipping assignee for issue #${issue.number} as no Linear user was found for GitHub user ${modifiedAssignee?.login}.`; console.log(reason); return reason; } - const response = await linear.issueUpdate( - syncedIssue.linearIssueId, - { assigneeId: user.linearUserId } - ); + if (linearAssignee?.id != newAssignee?.linearUserId) { + const response = await linear.issueUpdate( + syncedIssue.linearIssueId, + { assigneeId: newAssignee.linearUserId } + ); - if (!response?.success) { - const reason = `Failed to add assignee on Linear ticket for GitHub issue #${issue.number}.`; - console.log(reason); - throw new ApiError(reason, 500); - } else { - const reason = `Added assignee to Linear ticket for GitHub issue #${issue.number}.`; - console.log(reason); - return reason; + if (!response?.success) { + const reason = `Failed to add assignee on Linear ticket for GitHub issue #${issue.number}.`; + console.log(reason); + throw new ApiError(reason, 500); + } else { + const reason = `Added assignee to Linear ticket for GitHub issue #${issue.number}.`; + console.log(reason); + return reason; + } } } } else if (["milestoned", "demilestoned"].includes(action)) { diff --git a/utils/webhook/linear.handler.ts b/utils/webhook/linear.handler.ts index e5bb5b9..a0b05d2 100644 --- a/utils/webhook/linear.handler.ts +++ b/utils/webhook/linear.handler.ts @@ -25,6 +25,7 @@ import { components } from "@octokit/openapi-types"; import { linearQuery } from "../apollo"; import { createMilestone, getGitHubFooter, setIssueMilestone } from "../github"; import { ApiError, getIssueUpdateError } from "../errors"; +import { Issue, User } from "@octokit/webhooks-types"; export async function linearWebhookHandler( body: LinearWebhookPayload, @@ -659,13 +660,26 @@ export async function linearWebhookHandler( // Assignee change if ("assigneeId" in updatedFrom) { - const assigneeEndpoint = `${GITHUB.REPO_ENDPOINT}/${syncedIssue.GitHubRepo.repoName}/issues/${syncedIssue.githubIssueNumber}/assignees`; + // Remove all assignees before re-assigning to avoid false re-assignment events + const issueEndpoint = `${GITHUB.REPO_ENDPOINT}/${syncedIssue.GitHubRepo.repoName}/issues/${syncedIssue.githubIssueNumber}`; - // Assignee added - const assignee = data.assigneeId + const issueResponse = await got.get(issueEndpoint, { + headers: { + Authorization: githubAuthHeader, + "User-Agent": userAgentHeader + }, + responseType: "json" + }); + + const prevAssignees = ( + (await issueResponse.body) as Issue + ).assignees?.map((assignee: User) => assignee?.login); + + // Set new assignee + const newAssignee = data?.assigneeId ? await prisma.user.findFirst({ where: { - linearUserId: data.assigneeId + linearUserId: data?.assigneeId }, select: { githubUsername: true @@ -673,10 +687,20 @@ export async function linearWebhookHandler( }) : null; - if (assignee) { + if (data?.assigneeId && !newAssignee?.githubUsername) { + console.log( + `Skipping assignee for ${ticketName} as no GitHub username was found for Linear user ${data.assigneeId}.` + ); + } else if (prevAssignees?.includes(newAssignee?.githubUsername)) { + console.log( + `Skipping assignee for ${ticketName} as Linear user ${data.assigneeId} is already assigned.` + ); + } else { + const assigneeEndpoint = `${GITHUB.REPO_ENDPOINT}/${syncedIssue.GitHubRepo.repoName}/issues/${syncedIssue.githubIssueNumber}/assignees`; + const response = await got.post(assigneeEndpoint, { json: { - assignees: [assignee.githubUsername] + assignees: [newAssignee?.githubUsername] }, headers: { Authorization: githubAuthHeader, @@ -698,54 +722,30 @@ export async function linearWebhookHandler( `Added assignee to GitHub issue #${syncedIssue.githubIssueNumber} for ${ticketName}.` ); } - } else { - console.log( - `Skipping assignee for ${ticketName} as no GitHub username was found for Linear user ${data.assigneeId}.` - ); - } - // Remove previous assignee only if reassigned or deassigned explicitly - if ( - updatedFrom.assigneeId !== null && - (assignee || data.assigneeId === undefined) - ) { - const prevAssignee = await prisma.user.findFirst({ - where: { - linearUserId: updatedFrom.assigneeId + // Remove old assignees on GitHub + const unassignResponse = await got.delete(assigneeEndpoint, { + json: { + assignees: [prevAssignees] }, - select: { - githubUsername: true + headers: { + Authorization: githubAuthHeader, + "User-Agent": userAgentHeader } }); - if (prevAssignee) { - const response = await got.delete(assigneeEndpoint, { - json: { - assignees: [prevAssignee.githubUsername] - }, - headers: { - Authorization: githubAuthHeader, - "User-Agent": userAgentHeader - } - }); - - if (response.statusCode > 201) { - console.log( - getIssueUpdateError( - "assignee", - data, - syncedIssue, - response - ) - ); - } else { - console.log( - `Removed assignee on GitHub issue #${syncedIssue.githubIssueNumber} for ${ticketName}.` - ); - } + if (unassignResponse.statusCode > 201) { + console.log( + getIssueUpdateError( + "assignee", + data, + syncedIssue, + unassignResponse + ) + ); } else { console.log( - `Skipping assignee removal for ${ticketName} as no GitHub username was found for Linear user ${updatedFrom.assigneeId}.` + `Removed assignee from GitHub issue #${syncedIssue.githubIssueNumber} for ${ticketName}.` ); } }