Skip to content

Commit 7b9f6c8

Browse files
authored
chore: make PR linter overwrite previous reviews and delete old comments (#33105)
Many changes here: - The PR linter now removes the text of previous reviews, so that they are not distracting. - The PR linter now deletes old comments; before the rewrite, it used to create both comments and reviews. The comments should be deleted. - There were a bunch of missing `await`s, add those. - The missing `await`s slipped in because in the past someone tried to turn off *some* linter rules, and in doing so disabled *all* linter rules. For now, just re-enable the ones that have to do with promises. - Fix a typo in a global constant - Only log the relevant details of reviews, instead of the entire GitHub object ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 809a7f0 commit 7b9f6c8

File tree

7 files changed

+253
-37
lines changed

7 files changed

+253
-37
lines changed

tools/@aws-cdk/prlint/.eslintrc.js

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ baseConfig.parserOptions.project = __dirname + '/tsconfig.json';
33
module.exports = {
44
...baseConfig,
55
rules: {
6+
// The line below should have been here, but wasn't. There are now a shitton
7+
// of linter warnings that I don't want to deal with. Just get the most important ones.
8+
/* ...baseConfig.rules, */
9+
10+
// One of the easiest mistakes to make
11+
'@typescript-eslint/no-floating-promises': ['error'],
12+
13+
// Make sure that inside try/catch blocks, promises are 'return await'ed
14+
// (must disable the base rule as it can report incorrect errors)
15+
'no-return-await': 'off',
16+
'@typescript-eslint/return-await': 'error',
17+
618
'no-console': 'off',
719
'jest/valid-expect': 'off',
820
},

tools/@aws-cdk/prlint/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
export const DEFEAULT_LINTER_LOGIN = 'aws-cdk-automation';
2+
export const DEFAULT_LINTER_LOGIN = 'aws-cdk-automation';
33

44
export const CODE_BUILD_CONTEXT = 'AWS CodeBuild us-east-1 (AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv)';
55

tools/@aws-cdk/prlint/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Octokit } from '@octokit/rest';
33
import { StatusEvent, PullRequestEvent } from '@octokit/webhooks-definitions/schema';
44
import { PullRequestLinter } from './lint';
55
import { LinterActions } from './linter-base';
6-
import { DEFEAULT_LINTER_LOGIN } from './constants';
6+
import { DEFAULT_LINTER_LOGIN } from './constants';
77

88
/**
99
* Entry point for PR linter
@@ -38,7 +38,7 @@ async function run() {
3838
repo,
3939
number,
4040
// On purpose || instead of ??, also collapse empty string
41-
linterLogin: process.env.LINTER_LOGIN || DEFEAULT_LINTER_LOGIN,
41+
linterLogin: process.env.LINTER_LOGIN || DEFAULT_LINTER_LOGIN,
4242
});
4343

4444
let actions: LinterActions | undefined;

tools/@aws-cdk/prlint/lint.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { execSync } from 'child_process';
21
import * as path from 'path';
32
import { findModulePath, moduleStability } from './module';
43
import { breakingModules } from './parser';
@@ -33,7 +32,7 @@ export class PullRequestLinter extends PullRequestLinterBase {
3332

3433
public async validateStatusEvent(status: StatusEvent): Promise<LinterActions> {
3534
if (status.context === CODE_BUILD_CONTEXT && status.state === 'success') {
36-
return await this.assessNeedsReview();
35+
return this.assessNeedsReview();
3736
}
3837
return {};
3938
}
@@ -60,7 +59,12 @@ export class PullRequestLinter extends PullRequestLinterBase {
6059
const pr = await this.pr();
6160

6261
const reviewsData = await this.client.paginate(this.client.pulls.listReviews, this.prParams);
63-
console.log(JSON.stringify(reviewsData));
62+
console.log(JSON.stringify(reviewsData.map(r => ({
63+
user: r.user?.login,
64+
state: r.state,
65+
author_association: r.author_association,
66+
submitted_at: r.submitted_at,
67+
})), undefined, 2));
6468

6569
// NOTE: MEMBER = a member of the organization that owns the repository
6670
// COLLABORATOR = has been invited to collaborate on the repository
@@ -89,8 +93,10 @@ export class PullRequestLinter extends PullRequestLinterBase {
8993
// be dismissed by a maintainer to respect another reviewer's requested changes.
9094
// 5. Checking if any reviewers' most recent review requested changes
9195
// -> If so, the PR is considered to still need changes to meet community review.
96+
const trustedCommunityMembers = await this.getTrustedCommunityMembers();
97+
9298
const reviewsByTrustedCommunityMembers = reviewsData
93-
.filter(review => this.getTrustedCommunityMembers().includes(review.user?.login ?? ''))
99+
.filter(review => trustedCommunityMembers.includes(review.user?.login ?? ''))
94100
.filter(review => review.state !== 'PENDING' && review.state !== 'COMMENTED')
95101
.reduce((grouping, review) => {
96102
// submitted_at is not present for PENDING comments but is present for other states.
@@ -171,10 +177,10 @@ export class PullRequestLinter extends PullRequestLinterBase {
171177
* Trusted community reviewers is derived from the source of truth at this wiki:
172178
* https://github.com/aws/aws-cdk/wiki/CDK-Community-PR-Reviews
173179
*/
174-
private getTrustedCommunityMembers(): string[] {
180+
private async getTrustedCommunityMembers(): Promise<string[]> {
175181
if (this.trustedCommunity.length > 0) { return this.trustedCommunity; }
176182

177-
const wiki = execSync('curl https://raw.githubusercontent.com/wiki/aws/aws-cdk/CDK-Community-PR-Reviews.md', { encoding: 'utf-8' }).toString();
183+
const wiki = await (await fetch('https://raw.githubusercontent.com/wiki/aws/aws-cdk/CDK-Community-PR-Reviews.md')).text();
178184
const rawMdTable = wiki.split('<!--section-->')[1].split('\n').filter(l => l !== '');
179185
for (let i = 2; i < rawMdTable.length; i++) {
180186
this.trustedCommunity.push(rawMdTable[i].split('|')[1].trim());
@@ -247,6 +253,9 @@ export class PullRequestLinter extends PullRequestLinterBase {
247253
],
248254
});
249255

256+
// We always delete all comments; in the future we will just communicate via reviews.
257+
ret.deleteComments = await this.findExistingPRLinterComments();
258+
250259
ret = mergeLinterActions(ret, await this.validationToActions(validationCollector));
251260

252261
// also assess whether the PR needs review or not
@@ -270,7 +279,7 @@ export class PullRequestLinter extends PullRequestLinterBase {
270279
*/
271280
private async validationToActions(result: ValidationCollector): Promise<LinterActions> {
272281
if (result.isValid()) {
273-
console.log('✅ Success');
282+
console.log('✅ Success');
274283
return {
275284
dismissPreviousReview: true,
276285
};

tools/@aws-cdk/prlint/linter-base.ts

+57-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Octokit } from '@octokit/rest';
2-
import { GitHubPr, Review } from "./github";
2+
import { GitHubComment, GitHubPr, Review } from "./github";
33

44
export const PR_FROM_MAIN_ERROR = 'Pull requests from `main` branch of a fork cannot be accepted. Please reopen this contribution from another branch on your fork. For more information, see https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md#step-4-pull-request.';
55

@@ -96,25 +96,29 @@ export class PullRequestLinterBase {
9696
const pr = await this.pr();
9797

9898
for (const label of actions.removeLabels ?? []) {
99-
this.removeLabel(label, pr);
99+
await this.removeLabel(label, pr);
100100
}
101101

102102
for (const label of actions.addLabels ?? []) {
103-
this.addLabel(label, pr);
103+
await this.addLabel(label, pr);
104+
}
105+
106+
for (const comment of actions.deleteComments ?? []) {
107+
await this.deleteComment(comment);
104108
}
105109

106110
if (actions.dismissPreviousReview || actions.requestChanges) {
107111
if (actions.dismissPreviousReview && actions.requestChanges) {
108112
throw new Error(`It does not make sense to supply both dismissPreviousReview and requestChanges: ${JSON.stringify(actions)}`);
109113
}
110114

111-
const existingReviews = await this.findExistingPRLinterReview();
115+
const existingReviews = await this.findExistingPRLinterReviews();
112116

113117
if (actions.dismissPreviousReview) {
114-
this.dismissPRLinterReviews(existingReviews, 'passing');
118+
await this.dismissPRLinterReviews(existingReviews, 'passing');
115119
}
116120
if (actions.requestChanges) {
117-
this.createOrUpdatePRLinterReview(actions.requestChanges.failures, actions.requestChanges.exemptionRequest, existingReviews);
121+
await this.createOrUpdatePRLinterReview(actions.requestChanges.failures, actions.requestChanges.exemptionRequest, existingReviews);
118122
}
119123
}
120124
}
@@ -150,7 +154,14 @@ export class PullRequestLinterBase {
150154

151155
for (const existingReview of existingReviews ?? []) {
152156
try {
153-
console.log('Dismissing review');
157+
console.log(`👉 Dismissing review ${existingReview.id}`);
158+
// Make sure to update the old review texts. Dismissing won't do that,
159+
// and having multiple messages in place is confusing.
160+
await this.client.pulls.updateReview({
161+
...this.prParams,
162+
review_id: existingReview.id,
163+
body: '(This review is outdated)',
164+
});
154165
await this.client.pulls.dismissReview({
155166
...this.prParams,
156167
review_id: existingReview.id,
@@ -167,11 +178,19 @@ export class PullRequestLinterBase {
167178
* Finds existing review, if present
168179
* @returns Existing review, if present
169180
*/
170-
private async findExistingPRLinterReview(): Promise<Review[]> {
181+
private async findExistingPRLinterReviews(): Promise<Review[]> {
171182
const reviews = await this.client.paginate(this.client.pulls.listReviews, this.prParams);
172183
return reviews.filter((review) => review.user?.login === this.linterLogin && review.state !== 'DISMISSED');
173184
}
174185

186+
/**
187+
* Return all existing comments made by the PR linter
188+
*/
189+
public async findExistingPRLinterComments(): Promise<GitHubComment[]> {
190+
const comments = await this.client.paginate(this.client.issues.listComments, this.issueParams);
191+
return comments.filter((x) => x.user?.login === this.linterLogin && x.body?.includes('pull request linter'));
192+
}
193+
175194
/**
176195
* Creates a new review and comment for first run with failure or creates a new comment with new failures for existing reviews.
177196
*
@@ -199,22 +218,22 @@ export class PullRequestLinterBase {
199218
const body = paras.join('\n\n');
200219

201220
// Dismiss every review except the last one
202-
this.dismissPRLinterReviews((existingReviews ?? []).slice(0, -1), 'stale');
221+
await this.dismissPRLinterReviews((existingReviews ?? []).slice(0, -1), 'stale');
203222

204223
// Update the last review
205224
const existingReview = (existingReviews ?? []).slice(-1)[0];
206225

207226
if (!existingReview) {
208-
console.log('Creating review');
227+
console.log('👉 Creating new request-changes review');
209228
await this.client.pulls.createReview({
210229
...this.prParams,
211230
event: 'REQUEST_CHANGES',
212231
body,
213232
});
214233
} else if (existingReview.body !== body && existingReview.state === 'CHANGES_REQUESTED') {
215234
// State is good but body is wrong
216-
console.log('Updating review');
217-
this.client.pulls.updateReview({
235+
console.log(`👉 Updating existing request-changes review: ${existingReview.id}`);
236+
await this.client.pulls.updateReview({
218237
...this.prParams,
219238
review_id: existingReview.id,
220239
body,
@@ -230,8 +249,8 @@ export class PullRequestLinterBase {
230249
+ '(this setting is enabled by default for personal accounts, and cannot be enabled for organization owned accounts). '
231250
+ 'The reason for this is that our automation needs to synchronize your branch with our main after it has been approved, '
232251
+ 'and we cannot do that if we cannot push to your branch.'
233-
console.log('Closing pull request');
234252

253+
console.log('👉 Closing pull request');
235254
await this.client.issues.createComment({
236255
...this.issueParams,
237256
body: errorMessageBody,
@@ -244,14 +263,27 @@ export class PullRequestLinterBase {
244263
}
245264
}
246265

266+
/**
267+
* Delete a comment
268+
*/
269+
private async deleteComment(comment: GitHubComment) {
270+
console.log(`👉 Deleting comment ${comment.id}`);
271+
await this.client.issues.deleteComment({
272+
...this.issueParams,
273+
comment_id: comment.id,
274+
});
275+
}
276+
247277
private formatErrors(errors: string[]) {
248278
return errors.map(e => ` ❌ ${e}`).join('\n');
249279
}
250280

251-
private addLabel(label: string, pr: Pick<GitHubPr, 'labels' | 'number'>) {
281+
private async addLabel(label: string, pr: Pick<GitHubPr, 'labels' | 'number'>) {
252282
// already has label, so no-op
253283
if (pr.labels.some(l => l.name === label)) { return; }
254-
this.client.issues.addLabels({
284+
285+
console.log(`👉 Add label ${label}`);
286+
await this.client.issues.addLabels({
255287
issue_number: pr.number,
256288
owner: this.prParams.owner,
257289
repo: this.prParams.repo,
@@ -261,10 +293,12 @@ export class PullRequestLinterBase {
261293
});
262294
}
263295

264-
private removeLabel(label: string, pr: Pick<GitHubPr, 'labels' | 'number'>) {
296+
private async removeLabel(label: string, pr: Pick<GitHubPr, 'labels' | 'number'>) {
265297
// does not have label, so no-op
266298
if (!pr.labels.some(l => l.name === label)) { return; }
267-
this.client.issues.removeLabel({
299+
300+
console.log(`👉 Remove label ${label}`);
301+
await this.client.issues.removeLabel({
268302
issue_number: pr.number,
269303
owner: this.prParams.owner,
270304
repo: this.prParams.repo,
@@ -294,6 +328,11 @@ export interface LinterActions {
294328
*/
295329
removeLabels?: string[];
296330

331+
/**
332+
* Delete the given comments
333+
*/
334+
deleteComments?: GitHubComment[];
335+
297336
/**
298337
* Post a "request changes" review
299338
*/
@@ -313,6 +352,7 @@ export function mergeLinterActions(a: LinterActions, b: LinterActions): LinterAc
313352
addLabels: nonEmpty([...(a.addLabels ?? []), ...(b.addLabels ?? [])]),
314353
removeLabels: nonEmpty([...(a.removeLabels ?? []), ...(b.removeLabels ?? [])]),
315354
requestChanges: b.requestChanges ?? a.requestChanges,
355+
deleteComments: nonEmpty([...(a.deleteComments ?? []), ...(b.deleteComments ?? [])]),
316356
dismissPreviousReview: b.dismissPreviousReview ?? a.dismissPreviousReview,
317357
};
318358
}

0 commit comments

Comments
 (0)