Skip to content

Commit

Permalink
Add exit conditions to assignee sync (#109)
Browse files Browse the repository at this point in the history
* Handle removal of one of many GH assignees
* Make assignee sync more robust
* Handle assignment of unknown user
  • Loading branch information
tedspare committed Jul 4, 2023
1 parent 0f8d5ec commit 90e01cf
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 80 deletions.
87 changes: 54 additions & 33 deletions utils/webhook/github.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { replaceMentions, upsertUser } from "../../pages/api/utils";
import {
Issue,
IssueCommentCreatedEvent,
IssuesAssignedEvent,
IssuesEvent,
IssuesUnassignedEvent,
MilestoneEvent,
Repository,
User
Expand Down Expand Up @@ -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)) {
Expand Down
94 changes: 47 additions & 47 deletions utils/webhook/linear.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -659,24 +660,47 @@ 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
}
})
: 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,
Expand All @@ -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}.`
);
}
}
Expand Down

0 comments on commit 90e01cf

Please sign in to comment.