Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ship It - Adding comments from GitHub to Jira #1637

Merged
merged 16 commits into from
Oct 7, 2022
3 changes: 1 addition & 2 deletions src/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ export enum BooleanFlags {
MAINTENANCE_MODE = "maintenance-mode",
ASSOCIATE_PR_TO_ISSUES_IN_BODY = "associate-pr-to-issues-in-body",
VERBOSE_LOGGING = "verbose-logging",
LOG_UNSAFE_DATA = "log-unsafe-data",
REGEX_FIX = "regex-fix",
USE_NEW_GITHUB_CLIENT_FOR_INSTALLATION_API = "use-new-github-client-for-installation-api",
RETRY_ALL_ERRORS = "retry-all-errors",
GHE_SERVER = "ghe_server",
USE_REST_API_FOR_DISCOVERY = "use-rest-api-for-discovery",
TAG_BACKFILL_REQUESTS = "tag-backfill-requests",
CREATE_BRANCH = "create-branch",
USE_FIXED_GET_REF = "use-fixed-get-ref"
SEND_PR_COMMENTS_TO_JIRA = "send-pr-comments-to-jira_zy5ib"
}

export enum StringFlags {
Expand Down
59 changes: 52 additions & 7 deletions src/github/issue-comment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ import { createWebhookApp } from "test/utils/probot";
import { Application } from "probot";
import { Installation } from "models/installation";
import { Subscription } from "models/subscription";

import issueCommentBasic from "fixtures/issue-comment-basic.json";
import { booleanFlag, BooleanFlags } from "config/feature-flags";
import { when } from "jest-when";

jest.mock("config/feature-flags");

const turnFF_OnOff = (newStatus: boolean) => {
when(jest.mocked(booleanFlag))
.calledWith(BooleanFlags.SEND_PR_COMMENTS_TO_JIRA, expect.anything(), expect.anything())
.mockResolvedValue(newStatus);
};

describe("Issue Comment Webhook", () => {
let app: Application;
Expand All @@ -27,9 +36,35 @@ describe("Issue Comment Webhook", () => {

describe("issue_comment", () => {
describe("created", () => {
it("should update the GitHub issue with a linked Jira ticket", async () => {
it("FF ON - should update the GitHub issue with a linked Jira ticket and add PR comment as comment in Jira issue", async () => {
turnFF_OnOff(true);
githubUserTokenNock(gitHubInstallationId);

// Mocks for updating Jira with GitHub comment
githubNock
.get("/repos/test-repo-owner/test-repo-name/pulls/TEST-123")
.reply(200, {
title: "pull-request-title",
head: {
ref: "TEST-123-branch-name"
}
});

jiraNock
.post("/rest/api/latest/issue/TEST-123/comment", {
body: "Test example comment with linked Jira issue: [TEST-123] - some-comment-url",
properties: [
{
key: "gitHubId",
value: {
gitHubId: "5678"
}
}
]
})
.reply(201);

// Mocks for updating GitHub with a linked Jira ticket
jiraNock
.get("/rest/api/latest/issue/TEST-123?fields=summary")
.reply(200, {
Expand All @@ -39,11 +74,21 @@ describe("Issue Comment Webhook", () => {
}
});

githubNock
.patch("/repos/test-repo-owner/test-repo-name/issues/comments/5678", {
body: `Test example comment with linked Jira issue: [TEST-123]\n\n[TEST-123]: ${jiraHost}/browse/TEST-123`
})
.reply(200);
await expect(app.receive(issueCommentBasic as any)).toResolve();
});

it("FF OFF - should update the GitHub issue with a linked Jira ticket", async () => {
githubUserTokenNock(gitHubInstallationId);

// Mocks for updating GitHub with a linked Jira ticket
jiraNock
.get("/rest/api/latest/issue/TEST-123?fields=summary")
.reply(200, {
key: "TEST-123",
fields: {
summary: "Example Issue"
}
});

await expect(app.receive(issueCommentBasic as any)).toResolve();
});
Expand Down
77 changes: 75 additions & 2 deletions src/github/issue-comment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { emitWebhookProcessedMetrics } from "utils/webhook-utils";
import { GitHubIssue, GitHubIssueCommentData } from "interfaces/github";
import { createInstallationClient } from "utils/get-github-client-config";
import { WebhookContext } from "../routes/github/webhook/webhook-context";
import { WebhookContext } from "routes/github/webhook/webhook-context";
import { GitHubInstallationClient } from "~/src/github/client/github-installation-client";
import { jiraIssueKeyParser } from "utils/jira-utils";
import { getJiraClient } from "~/src/jira/client/jira-client";
import { booleanFlag, BooleanFlags } from "config/feature-flags";

export const issueCommentWebhookHandler = async (
context: WebhookContext,
Expand All @@ -17,15 +21,21 @@ export const issueCommentWebhookHandler = async (
}
} = context.payload;

const jiraHost = jiraClient.baseURL;

context.log = context.log.child({
gitHubInstallationId,
jiraHost: jiraClient.baseURL
jiraHost
});

let linkifiedBody;
const gitHubAppId = context.gitHubAppConfig?.gitHubAppId;
const gitHubInstallationClient = await createInstallationClient(gitHubInstallationId, jiraClient.baseURL, context.log, gitHubAppId);

if (await booleanFlag(BooleanFlags.SEND_PR_COMMENTS_TO_JIRA, false, jiraHost)){
await syncIssueCommentsToJira(jiraClient.baseURL, context, gitHubInstallationClient);
}

// TODO: need to create reusable function for unfurling
try {
linkifiedBody = await util.unfurl(comment.body);
Expand Down Expand Up @@ -59,3 +69,66 @@ export const issueCommentWebhookHandler = async (
gitHubAppId
);
};

const syncIssueCommentsToJira = async (jiraHost: string, context: WebhookContext, gitHubInstallationClient: GitHubInstallationClient) => {
const { comment, repository, issue } = context.payload;
const { body: gitHubMessage, id: gitHubId, html_url: gitHubCommentUrl } = comment;
const pullRequest = await gitHubInstallationClient.getPullRequest(repository.owner.login, repository.name, issue.number);
// Note: we are only considering the branch name here. Should we also check for pr titles?
const issueKey = jiraIssueKeyParser(pullRequest.data.head.ref)[0] || "";
const jiraClient = await getJiraClient(
jiraHost,
gitHubInstallationClient.githubInstallationId.installationId,
context.gitHubAppConfig?.gitHubAppId,
context.log
);

switch (context.action) {
case "created": {
await jiraClient.issues.comments.addForIssue(issueKey, {
body: gitHubMessage + " - " + gitHubCommentUrl,
properties: [
{
key: "gitHubId",
value: {
gitHubId
}
}
]
});
break;
}
case "edited": {
await jiraClient.issues.comments.updateForIssue(issueKey, await getCommentId(jiraClient, issueKey, gitHubId), {
body: gitHubMessage + " - " + gitHubCommentUrl
});
break;
}
case "deleted":
await jiraClient.issues.comments.deleteForIssue(issueKey, await getCommentId(jiraClient, issueKey, gitHubId));
break;
default:
context.log.error("This shouldn't happen", context);
break;
}
};

const getCommentId = async (jiraClient, issueKey: string, gitHubId: string) => {

// TODO - this currently only fetchs 50, do we want to loop de loop and find everything!?!?!?
const listOfComments = await jiraClient.issues.comments.list(issueKey);

// TODO Tidy up the getting of the githubid from comment props
const mappedResults = listOfComments.data.comments.map(comment => {
return {
commentId: comment.id,
gitHubId: comment.properties?.find(prop => {
return prop.key === "gitHubId";
})?.value?.gitHubId
};
});

return mappedResults.find(comment => {
return comment.gitHubId === gitHubId;
})?.commentId;
};
3 changes: 2 additions & 1 deletion src/github/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const setupGithubWebhooks = (robot: Application) => {
robot.on(
[
"issue_comment.created",
"issue_comment.edited"
"issue_comment.edited",
"issue_comment.deleted"
],
convertToWebhookContext(webhookTimeout(GithubWebhookMiddleware(issueCommentWebhookHandler)))
);
Expand Down
20 changes: 20 additions & 0 deletions src/jira/client/jira-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,31 @@ export const getJiraClient = async (
},
comments: {
// eslint-disable-next-line camelcase
list: (issue_id: string) =>
instance.get("/rest/api/latest/issue/{issue_id}/comment?expand=properties", {
urlParams: {
issue_id
}
}),
addForIssue: (issue_id: string, payload) =>
instance.post("/rest/api/latest/issue/{issue_id}/comment", payload, {
urlParams: {
issue_id
}
}),
updateForIssue: (issue_id: string, comment_id: string, payload) =>
instance.put("rest/api/latest/issue/{issue_id}/comment/{comment_id}", payload, {
urlParams: {
issue_id,
comment_id
}
}),
deleteForIssue: (issue_id: string, comment_id: string) =>
instance.delete("rest/api/latest/issue/{issue_id}/comment/{comment_id}", {
urlParams: {
issue_id,
comment_id
}
})
},
transitions: {
Expand Down
5 changes: 3 additions & 2 deletions test/fixtures/issue-comment-basic.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"payload": {
"action": "created",
"issue": {
"number": "test-issue-number"
"number": "TEST-123"
},
"comment": {
"body": "Test example comment with linked Jira issue: [TEST-123]",
"id": "5678"
"id": "5678",
"html_url": "some-comment-url"
},
"repository": {
"name": "test-repo-name",
Expand Down
3 changes: 3 additions & 0 deletions test/snapshots/jira/client/jira-client.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Object {
"issues": Object {
"comments": Object {
"addForIssue": [Function],
"deleteForIssue": [Function],
"list": [Function],
"updateForIssue": [Function],
},
"get": [Function],
"getAll": [Function],
Expand Down