Skip to content

Commit

Permalink
feat: collapse old comments (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
joscha authored May 17, 2021
1 parent 9e74045 commit c4bdb56
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 37 deletions.
100 changes: 100 additions & 0 deletions __tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import { ok } from 'assert';
import { readFileSync } from 'fs';
import { BuildkiteBuildRequest } from '../src/buildkite';
import nock from 'nock';
import {
commentsReq,
CommentsRequestData,
mutationReq,
} from '../src/events/commented';

// Enable this to record HTTP requests when adding a new test
// nock.recorder.rec();
Expand Down Expand Up @@ -629,6 +634,59 @@ describe('github-control', () => {
)
.reply(200, updateCommentReply);

const response: CommentsRequestData = {
self: { login: 'xx' },
comments: {
pullRequest: {
comments: {
nodes: [
{ id: 'A', viewerDidAuthor: true, isMinimized: true }, // minimized already, leave it alone
{ id: 'B', viewerDidAuthor: true, isMinimized: false },
{
id: 'C',
viewerDidAuthor: false,
isMinimized: false,
editor: {
login: 'xx',
},
},
{
id: 'D',
viewerDidAuthor: false,
isMinimized: false,
editor: {
login: 'xy', // different editor
},
},
],
},
},
},
};
nock('https://api.github.com')
.post('/graphql', {
query: commentsReq,
variables: {
repoName: 'some-repo',
repoOwner: 'some-org',
prNumber: 9500,
},
})
.reply(200, { data: response });

nock('https://api.github.com')
.post('/graphql', {
query: mutationReq,
variables: { subjectId: 'B' },
})
.reply(200, { data: { clientMutationId: null } });
nock('https://api.github.com')
.post('/graphql', {
query: mutationReq,
variables: { subjectId: 'C' },
})
.reply(200, { data: { clientMutationId: null } });

const res = await handler(lambdaRequest, context);
assertLambdaResponse(res, 200, {
success: true,
Expand Down Expand Up @@ -694,6 +752,20 @@ describe('github-control', () => {
)
.reply(200, updateCommentReply);

const response: CommentsRequestData = {
self: { login: 'xx' },
comments: {
pullRequest: {
comments: {
nodes: [],
},
},
},
};
nock('https://api.github.com')
.post('/graphql')
.reply(200, { data: response });

const res = await handler(lambdaRequestMultiBuild, context);
assertLambdaResponse(res, 200, {
success: true,
Expand Down Expand Up @@ -749,6 +821,20 @@ describe('github-control', () => {
)
.reply(200, updateCommentReply);

const response: CommentsRequestData = {
self: { login: 'xx' },
comments: {
pullRequest: {
comments: {
nodes: [],
},
},
},
};
nock('https://api.github.com')
.post('/graphql')
.reply(200, { data: response });

const res = await handler(lambdaRequest, context);
assertLambdaResponse(res, 200, {
success: true,
Expand Down Expand Up @@ -827,6 +913,20 @@ describe('github-control', () => {
)
.reply(200, updateCommentReply);

const response: CommentsRequestData = {
self: { login: 'xx' },
comments: {
pullRequest: {
comments: {
nodes: [],
},
},
},
};
nock('https://api.github.com')
.post('/graphql')
.reply(200, { data: response });

const res = await handler(lambdaRequest, context);
assertLambdaResponse(res, 200, {
success: true,
Expand Down
1 change: 1 addition & 0 deletions docs/guides/install-and-configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Config = {
BUILDKITE_ORG_NAME: string; // (the URL part of `https://buildkite.com/<your-org>/`)
ENABLE_DEBUG?: string; // defaults to "false"
GITHUB_WEBHOOK_SECRET?: string; // A webhook secret to verify the webhook request against
COLLAPSE_OLD_COMMENTS?: string; // defaults to "true"; collapses old RocketBot comments when adding/editing new ones
} & (
| {
// RocketBot uses a GitHub app. Recommended.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"license": "MIT",
"private": true,
"devDependencies": {
"@octokit/graphql-schema": "^10.39.1",
"@octokit/types": "^6.14.2",
"@octokit/webhooks-schemas": "^3.72.0",
"@octokit/webhooks-types": "^3.72.0",
Expand Down Expand Up @@ -68,6 +69,7 @@
},
"dependencies": {
"@octokit/auth-app": "^3.4.0",
"@octokit/graphql": "^4.6.1",
"@octokit/rest": "^18.5.3",
"@octokit/webhooks-methods": "^1.0.0",
"aws-lambda": "^1.0.6",
Expand Down
4 changes: 1 addition & 3 deletions src/buildkite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export async function buildkiteReadPipelines(
paginate: { all },
} = gotInstance(config);

const pipelines = await all<Pipeline>('pipelines?page=1&per_page=100');
logger.debug('All pipelines: %o', pipelines);
return pipelines;
return all<Pipeline>('pipelines?page=1&per_page=100');
}

export type Author = {
Expand Down
4 changes: 3 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const BaseConfig = z.object({
BUILDKITE_ORG_NAME: z.string(),
ENABLE_DEBUG: z.string().optional(),
[GITHUB_WEBHOOK_SECRET_KEY]: z.string().optional(),
COLLAPSE_OLD_COMMENTS: z.string().optional().default('true'),
});

export const Config = z.union([
Expand Down Expand Up @@ -46,6 +47,7 @@ export const getConfig = async function (
config.GITHUB_WEBHOOK_SECRET ?? process.env[GITHUB_WEBHOOK_SECRET_KEY];
return config;
} else {
return Config.parse(env);
const config = Config.parse(env);
return config;
}
};
136 changes: 129 additions & 7 deletions src/events/commented.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
IssueCommentEvent,
PullRequest,
PullRequestReviewCommentEvent,
Repository,
} from '@octokit/webhooks-types';
import { isTriggerComment, parseTriggerComment } from '../trigger';
import { Logger } from 'pino';
Expand All @@ -12,20 +13,40 @@ import {
githubGetPullRequestDetails,
githubUpdateComment,
} from '../github';
import type { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
import type { RestEndpointMethodTypes } from '@octokit/rest';
import type { JSONResponse } from '../response';
import { GithubApis } from '../github_apis';
import { IssueComment, Mutation, Scalars, User } from '@octokit/graphql-schema';

export type PullRequestData = RestEndpointMethodTypes['pulls']['get']['response']['data'];
export type UserData = RestEndpointMethodTypes['users']['getByUsername']['response']['data'];

type PullRequestContext = PullRequestData | PullRequest;
type Unpromisify<T> = T extends Promise<infer U> ? U : T;
type ID = Scalars['ID'];

export type CommentsRequestData = {
self: Pick<User, 'login'>;
comments: {
pullRequest: {
comments: {
nodes: (Pick<IssueComment, 'viewerDidAuthor' | 'id' | 'isMinimized'> & {
editor?: Pick<User, 'login'>;
})[];
};
};
};
};

const envVarPrefix = 'GH_CONTROL_USER_ENV_';
const BOT_SUFFIX = '[bot]';

export async function commented(
eventBody: IssueCommentEvent | PullRequestReviewCommentEvent,
currentEventType: 'issue_comment' | 'pull_request_review_comment',
logger: Logger,
config: Config,
octokit: Octokit,
apis: GithubApis,
): Promise<JSONResponse> {
if (eventBody.action === 'deleted') {
logger.info('Comment was deleted, nothing to do here');
Expand All @@ -45,16 +66,17 @@ export async function commented(
return { success: true, triggered: false };
}

const prHtmlUrl = isIssueComment(currentEventType, eventBody)
? eventBody.issue.pull_request?.html_url
: eventBody.pull_request.html_url;
const pr = isIssueComment(currentEventType, eventBody)
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
eventBody.issue.pull_request! // we know it came from a PR, otherwise we'd have exited above
: eventBody.pull_request;

const requestedBuildData = parseTriggerComment(eventBody.comment.body);
const commentUrl = eventBody.comment.url;
const commenter = eventBody.sender.login;
logger.info(
`@${commenter} requested "${requestedBuildData.buildNames.join(',')}" for ${
prHtmlUrl || 'unkown URL'
pr.html_url
}`,
);

Expand All @@ -69,8 +91,9 @@ export async function commented(
{},
);

const { octokit } = apis;
const [prData, { name: senderName, email: senderEmail }] = await Promise.all<
PullRequestData | PullRequest,
PullRequestContext,
UserData
>([
isIssueComment(currentEventType, eventBody)
Expand Down Expand Up @@ -143,10 +166,109 @@ ${requestedBuildData.buildNames
updatedComment,
);
logger.info(`Updated comment ${commentData.html_url} with build URL`);
if (config.COLLAPSE_OLD_COMMENTS === 'true') {
await collapseOldComments(
apis,
prData,
eventBody.repository,
commentData,
logger,
);
}
return {
success: true,
triggered: true,
commented: false,
updatedCommentUrl: commentData.html_url,
};
}

/**
* Collapses comments for and from RocketBot prior to the current one
*/
async function collapseOldComments(
{ graphql }: GithubApis,
prData: PullRequestContext,
repository: Repository,
currentComment: Unpromisify<ReturnType<typeof githubUpdateComment>>,
logger: Logger,
) {
const res = await graphql<CommentsRequestData>(commentsReq, {
repoName: repository.name,
repoOwner: repository.owner.login,
prNumber: prData.number,
});

const currentUser = stripBotSuffix(res.self.login);
logger.info('current user is "%s"', currentUser);
const { nodes: comments } = res.comments.pullRequest.comments;
logger.info('current comment ID is "%s"', currentComment.node_id);

const toCollapse: ID[] = comments.reduce<ID[]>((acc, comment) => {
if (comment.isMinimized) {
// ignore all minimized
return acc;
}

if (comment.id === currentComment.node_id) {
// ignore the current comment (the one that has triggered this run)
return acc;
}

if (comment.viewerDidAuthor) {
// author was rocketbot, collapse
return [...acc, comment.id];
}
if (comment.editor?.login === currentUser) {
// last editor was rocketbot, collapse
return [...acc, comment.id];
}
return acc;
}, []);

logger.info('Collapsing comment IDs: %o', toCollapse);

await Promise.all(
toCollapse.map((subjectId) =>
graphql<Pick<Mutation, 'minimizeComment'>>(mutationReq, { subjectId }),
),
);
}

export const commentsReq = `
query($repoName: String!, $repoOwner: String!, $prNumber: Int!) {
self: viewer {
login
}
comments: repository(name: $repoName, owner: $repoOwner) {
pullRequest(number: $prNumber) {
comments(last: 10) {
nodes {
id
viewerDidAuthor
editor {
login
}
isMinimized
}
}
}
}
}
`;

export const mutationReq = `
mutation($subjectId: ID!) {
minimizeComment(
input: { subjectId: $subjectId, classifier: OUTDATED }
) {
clientMutationId
}
}
`;

function stripBotSuffix(login: string) {
return login.endsWith(BOT_SUFFIX)
? login.substr(0, login.length - BOT_SUFFIX.length)
: login;
}
Loading

0 comments on commit c4bdb56

Please sign in to comment.