Skip to content

Commit 8b4ba8b

Browse files
committed
Replace stale alert comments, even when there are no new alerts
1 parent 6d7c0f3 commit 8b4ba8b

5 files changed

Lines changed: 308 additions & 7 deletions

File tree

src/comment/leaveCommitComment.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as github from '@actions/github';
22
import * as core from '@actions/core';
3-
import { wrapBodyWithBenchmarkTags } from './benchmarkCommentTags';
3+
import { benchmarkStartTag, wrapBodyWithBenchmarkTags } from './benchmarkCommentTags';
44

55
export async function leaveCommitComment(
66
repoOwner: string,
@@ -23,3 +23,45 @@ export async function leaveCommitComment(
2323
core.debug('leaveCommitComment end');
2424
return response;
2525
}
26+
27+
// Commit comments are attached to the commit where the alert was detected. When the alert is later
28+
// resolved (no alert on the current commit), replace the existing alert comment with a new message
29+
// instead of leaving the stale alert comment. Returns null when no matching comment was found.
30+
export async function updateCommitCommentIfExists(
31+
repoOwner: string,
32+
repoName: string,
33+
commitId: string,
34+
body: string,
35+
commentId: string,
36+
token: string,
37+
) {
38+
core.debug('updateCommitCommentIfExists start');
39+
const client = github.getOctokit(token);
40+
41+
const existingCommentsResponse = await client.rest.repos.listCommentsForCommit({
42+
owner: repoOwner,
43+
repo: repoName,
44+
// eslint-disable-next-line @typescript-eslint/naming-convention
45+
commit_sha: commitId,
46+
});
47+
48+
const existingComment = existingCommentsResponse.data.find((comment) =>
49+
comment.body.startsWith(benchmarkStartTag(commentId)),
50+
);
51+
52+
if (!existingComment) {
53+
core.debug(`No existing alert comment was found on commit ${commitId}. Skipping comment update`);
54+
return null;
55+
}
56+
57+
const updateResponse = await client.rest.repos.updateCommitComment({
58+
owner: repoOwner,
59+
repo: repoName,
60+
// eslint-disable-next-line @typescript-eslint/naming-convention
61+
comment_id: existingComment.id,
62+
body: wrapBodyWithBenchmarkTags(commentId, body),
63+
});
64+
console.log(`Comment was updated via ${updateResponse.url}. Response:`, updateResponse.status, updateResponse.data);
65+
core.debug('updateCommitCommentIfExists end');
66+
return updateResponse;
67+
}

src/comment/leavePRComment.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export async function leavePRComment(
1010
body: string,
1111
commentId: string,
1212
token: string,
13+
updateOnly = false,
1314
) {
1415
try {
1516
core.debug('leavePRComment start');
@@ -26,6 +27,10 @@ export async function leavePRComment(
2627
);
2728

2829
if (!existingCommentId) {
30+
if (updateOnly) {
31+
core.debug('No existing alert comment was found. Skipping comment creation');
32+
return null;
33+
}
2934
core.debug('creating new pr comment');
3035
const createReviewResponse = await client.rest.pulls.createReview({
3136
owner: repoOwner,

src/write.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Benchmark, BenchmarkResult } from './extract';
88
import { Config, ToolType } from './config';
99
import { DEFAULT_INDEX_HTML } from './default_index_html';
1010
import { leavePRComment } from './comment/leavePRComment';
11-
import { leaveCommitComment } from './comment/leaveCommitComment';
11+
import { leaveCommitComment, updateCommitCommentIfExists } from './comment/leaveCommitComment';
1212
import { addBenchmarkEntry } from './addBenchmarkEntry';
1313

1414
export type BenchmarkSuites = { [name: string]: Benchmark[] };
@@ -348,6 +348,28 @@ function buildAlertComment(
348348
return lines.join('\n');
349349
}
350350

351+
function buildNoAlertsComment(benchName: string, curSuite: Benchmark, prevSuite: Benchmark, cc: string[]): string {
352+
// Do not show benchmark name if it is the default value 'Benchmark'.
353+
const benchmarkText = benchName === 'Benchmark' ? '' : ` for **'${benchName}'**`;
354+
const lines = [
355+
'# Performance Report',
356+
'',
357+
`No performance alerts${benchmarkText}.`,
358+
'',
359+
`Previous commit: ${prevSuite.commit.id}`,
360+
`Current commit: ${curSuite.commit.id}`,
361+
];
362+
363+
// Footer
364+
lines.push('', commentFooter());
365+
366+
if (cc.length > 0) {
367+
lines.push('', `CC: ${cc.join(' ')}`);
368+
}
369+
370+
return lines.join('\n');
371+
}
372+
351373
async function leaveComment(commitId: string, body: string, commentId: string, token: string) {
352374
core.debug('Sending comment:\n' + body);
353375

@@ -378,6 +400,37 @@ async function handleComment(benchName: string, curSuite: Benchmark, prevSuite:
378400
await leaveComment(curSuite.commit.id, body, `${benchName} Summary`, githubToken);
379401
}
380402

403+
async function replaceAlertCommentWithNoAlerts(body: string, commentId: string, token: string, prevCommitId: string) {
404+
core.debug('Replacing an existing alert comment with a no-alerts message:\n' + body);
405+
406+
const repoMetadata = getCurrentRepoMetadata();
407+
const pr = github.context.payload.pull_request;
408+
409+
if (pr?.number) {
410+
// PR review comments are updated in place, so only update an existing alert comment
411+
return await leavePRComment(
412+
repoMetadata.owner.login,
413+
repoMetadata.name,
414+
pr.number,
415+
body,
416+
commentId,
417+
token,
418+
true,
419+
);
420+
}
421+
422+
// Commit comments are attached to the commit where the alert was detected. When the alert is
423+
// resolved, update the alert comment left on the previous commit.
424+
return await updateCommitCommentIfExists(
425+
repoMetadata.owner.login,
426+
repoMetadata.name,
427+
prevCommitId,
428+
body,
429+
commentId,
430+
token,
431+
);
432+
}
433+
381434
async function handleAlert(benchName: string, curSuite: Benchmark, prevSuite: Benchmark, config: Config) {
382435
const { alertThreshold, githubToken, commentOnAlert, failOnAlert, alertCommentCcUsers, failThreshold } = config;
383436

@@ -389,7 +442,19 @@ async function handleAlert(benchName: string, curSuite: Benchmark, prevSuite: Be
389442
const [losses, gains] = findAlerts(curSuite, prevSuite, alertThreshold);
390443
const alerts = [...losses, ...gains];
391444
if (alerts.length === 0) {
392-
core.debug('No performance alert found happily');
445+
if (commentOnAlert) {
446+
if (!githubToken) {
447+
throw new Error("'comment-on-alert' input is set but 'github-token' input is not set");
448+
}
449+
// When a previous alert comment exists (e.g. an alert was left on an earlier commit of
450+
// this PR), replace it with a message saying that no alerts are detected anymore instead
451+
// of leaving the stale alert comment. Do not create a new comment when none exists.
452+
core.debug('No performance alert was found. Replacing an existing alert comment with a no-alerts message');
453+
const body = buildNoAlertsComment(benchName, curSuite, prevSuite, alertCommentCcUsers);
454+
await replaceAlertCommentWithNoAlerts(body, `${benchName} Alert`, githubToken, prevSuite.commit.id);
455+
} else {
456+
core.debug('No performance alert found happily');
457+
}
393458
return;
394459
}
395460

@@ -409,8 +474,10 @@ async function handleAlert(benchName: string, curSuite: Benchmark, prevSuite: Be
409474
throw new Error("'comment-on-alert' input is set but 'github-token' input is not set");
410475
}
411476
const res = await leaveComment(curSuite.commit.id, body, `${benchName} Alert`, githubToken);
412-
const url = res.data.html_url;
413-
message = body + `\nComment was generated at ${url}`;
477+
if (res) {
478+
const url = res.data.html_url;
479+
message = body + `\nComment was generated at ${url}`;
480+
}
414481
}
415482

416483
if (failOnAlert) {

test/fakedOctokit.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,44 @@
11
type OctokitOpts = { owner: string; repo: string; commit_sha: string; body: string };
2+
type UpdateCommitCommentOpts = { owner: string; repo: string; comment_id: number; body: string };
3+
type ReviewOpts = {
4+
owner: string;
5+
repo: string;
6+
pull_number: number;
7+
event?: string;
8+
body: string;
9+
review_id?: number;
10+
};
11+
type ReviewCall = { method: 'createReview' | 'updateReview'; opts: ReviewOpts };
12+
type CommentLike = { id: number; body: string };
13+
214
class FakedOctokitRepos {
315
spyOpts: OctokitOpts[];
16+
updatedCommitComments: UpdateCommitCommentOpts[];
17+
commitComments: CommentLike[];
18+
419
constructor() {
520
this.spyOpts = [];
21+
this.updatedCommitComments = [];
22+
this.commitComments = [];
23+
}
24+
25+
setCommitComments(comments: CommentLike[]) {
26+
this.commitComments = comments;
27+
}
28+
29+
listCommentsForCommit() {
30+
return Promise.resolve({ data: this.commitComments });
31+
}
32+
33+
updateCommitComment(opt: UpdateCommitCommentOpts) {
34+
this.updatedCommitComments.push(opt);
35+
return Promise.resolve({
36+
url: 'https://dummy-comment-url',
37+
status: 200,
38+
data: {},
39+
});
640
}
41+
742
createCommitComment(opt: OctokitOpts) {
843
this.spyOpts.push(opt);
944
return Promise.resolve({
@@ -13,19 +48,69 @@ class FakedOctokitRepos {
1348
},
1449
});
1550
}
51+
1652
lastCall(): OctokitOpts {
1753
return this.spyOpts[this.spyOpts.length - 1];
1854
}
55+
1956
clear() {
2057
this.spyOpts = [];
58+
this.updatedCommitComments = [];
59+
this.commitComments = [];
2160
}
2261
}
2362

2463
export const fakedRepos = new FakedOctokitRepos();
2564

65+
class FakedOctokitPulls {
66+
reviews: CommentLike[];
67+
reviewCalls: ReviewCall[];
68+
69+
constructor() {
70+
this.reviews = [];
71+
this.reviewCalls = [];
72+
}
73+
74+
setReviews(reviews: CommentLike[]) {
75+
this.reviews = reviews;
76+
}
77+
78+
listReviews() {
79+
return Promise.resolve({ data: this.reviews });
80+
}
81+
82+
createReview(opt: ReviewOpts) {
83+
this.reviewCalls.push({ method: 'createReview', opts: opt });
84+
return Promise.resolve({
85+
status: 200,
86+
data: {
87+
html_url: 'https://dummy-comment-url',
88+
},
89+
});
90+
}
91+
92+
updateReview(opt: ReviewOpts) {
93+
this.reviewCalls.push({ method: 'updateReview', opts: opt });
94+
return Promise.resolve({
95+
status: 200,
96+
data: {
97+
html_url: 'https://dummy-comment-url',
98+
},
99+
});
100+
}
101+
102+
clear() {
103+
this.reviews = [];
104+
this.reviewCalls = [];
105+
}
106+
}
107+
108+
export const fakedPulls = new FakedOctokitPulls();
109+
26110
export class FakedOctokit {
27111
rest = {
28112
repos: fakedRepos,
113+
pulls: fakedPulls,
29114
};
30115
opt: { token: string };
31116
constructor(token: string) {

0 commit comments

Comments
 (0)