Skip to content

Commit

Permalink
feat(ncu-ci): check for request-ci label (#806)
Browse files Browse the repository at this point in the history
When running `ncu-ci` without the `--certify-safe` CLI flag,
if something was pushed to a PR since the last approving review, check
if the last time the `request-ci` label was added, it was done by a
Collaborator and after the last push event on the PR.

Co-authored-by: Mathis Wiehl <mail@mathiswiehl.de>
  • Loading branch information
aduh95 and fahrradflucht authored May 21, 2024
1 parent 9f8df53 commit 6cc2b1a
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/ci/run_ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class RunPRJob {
this.certifySafe =
certifySafe ||
Promise.all([this.prData.getReviews(), this.prData.getPR()]).then(() =>
new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReview()
new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReviewOrLabel()
);
}

Expand Down
26 changes: 26 additions & 0 deletions lib/pr_checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,32 @@ export default class PRChecker {
return true;
}

async checkCommitsAfterReviewOrLabel() {
if (this.checkCommitsAfterReview()) return true;

await Promise.all([this.data.getLabeledEvents(), this.data.getCollaborators()]);

const {
cli, data, pr
} = this;

const { updatedAt } = pr.timelineItems;
const requestCiLabels = data.labeledEvents.findLast(
({ createdAt, label: { name } }) => name === 'request-ci' && createdAt > updatedAt
);
if (requestCiLabels == null) return false;

const { actor: { login } } = requestCiLabels;
const collaborators = Array.from(data.collaborators.values(),
(c) => c.login.toLowerCase());
if (collaborators.includes(login.toLowerCase())) {
cli.info('request-ci label was added by a Collaborator after the last push event.');
return true;
}

return false;
}

checkCommitsAfterReview() {
const {
commits, reviews, cli, argv
Expand Down
10 changes: 10 additions & 0 deletions lib/pr_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from './user_status.js';

// lib/queries/*.gql file names
const LABELED_EVENTS_QUERY = 'PRLabeledEvents';
const PR_QUERY = 'PR';
const REVIEWS_QUERY = 'Reviews';
const COMMENTS_QUERY = 'PRComments';
Expand Down Expand Up @@ -33,6 +34,7 @@ export default class PRData {
this.comments = [];
this.commits = [];
this.reviewers = [];
this.labeledEvents = [];
}

getThread() {
Expand Down Expand Up @@ -90,6 +92,14 @@ export default class PRData {
]);
}

async getLabeledEvents() {
const { prid, owner, repo, cli, request, prStr } = this;
const vars = { prid, owner, repo };
cli.updateSpinner(`Getting labels from ${prStr}`);
this.labeledEvents = (await request.gql(LABELED_EVENTS_QUERY, vars))
.repository.pullRequest.timelineItems.nodes;
}

async getComments() {
const { prid, owner, repo, cli, request, prStr } = this;
const vars = { prid, owner, repo };
Expand Down
19 changes: 19 additions & 0 deletions lib/queries/PRLabeledEvents.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
query PRLabeledEvents($prid: Int!, $owner: String!, $repo: String!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prid) {
timelineItems(itemTypes: LABELED_EVENT, after: $after, last: 100) {
nodes {
... on LabeledEvent {
actor {
login
}
label {
name
}
createdAt
}
}
}
}
}
}
9 changes: 9 additions & 0 deletions test/fixtures/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,12 @@ for (const subdir of readdirSync(path('./jenkins'))) {
readJSON(`./jenkins/${subdir}/${item}`);
}
};

export const labeledEvents = {};

for (const item of readdirSync(path('./labeled_events'))) {
if (!item.endsWith('.json')) {
continue;
}
labeledEvents[basename(item, '.json')] = readJSON(`./labeled_events/${item}`);
}
1 change: 1 addition & 0 deletions test/fixtures/first_timer_pr.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
}
]
},
"timelineItems": { "updatedAt": "2017-10-24T11:13:43Z" },
"title": "test: awesome changes",
"baseRefName": "main",
"headRefName": "awesome-changes"
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/labeled_events/no-request-ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"actor": { "login": "nodejs-github-bot" },
"label": { "name": "doc" },
"createdAt": "2024-05-13T15:57:10Z"
},
{
"actor": { "login": "nodejs-github-bot" },
"label": { "name": "test_runner" },
"createdAt": "2024-05-13T15:57:10Z"
}
]
12 changes: 12 additions & 0 deletions test/fixtures/labeled_events/old-request-ci-collaborator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"actor": { "login": "nodejs-github-bot" },
"label": { "name": "doc" },
"createdAt": "2024-05-13T15:57:10Z"
},
{
"actor": { "login": "foo" },
"label": { "name": "request-ci" },
"createdAt": "1999-10-24T11:13:43Z"
}
]
12 changes: 12 additions & 0 deletions test/fixtures/labeled_events/recent-request-ci-collaborator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"actor": { "login": "nodejs-github-bot" },
"label": { "name": "doc" },
"createdAt": "2024-05-13T15:57:10Z"
},
{
"actor": { "login": "foo" },
"label": { "name": "request-ci" },
"createdAt": "2024-05-13T15:57:10Z"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"actor": { "login": "nodejs-github-bot" },
"label": { "name": "doc" },
"createdAt": "2024-05-13T15:57:10Z"
},
{
"actor": { "login": "random-person" },
"label": { "name": "request-ci" },
"createdAt": "2024-05-13T15:57:10Z"
}
]
95 changes: 95 additions & 0 deletions test/unit/pr_checker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
semverMajorPR,
conflictingPR,
closedPR,
labeledEvents,
mergedPR,
pullRequests
} from '../fixtures/data.js';
Expand Down Expand Up @@ -2048,6 +2049,100 @@ describe('PRChecker', () => {
});
});

describe('checkCommitsAfterReviewOrLabel', () => {
it('should return true if PR passes post review checks', async() => {
const cli = new TestCLI();
const checker = new PRChecker(cli, {
pr: semverMajorPR,
reviewers: allGreenReviewers,
comments: commentsWithLGTM,
reviews: approvingReviews,
commits: simpleCommits,
collaborators,
authorIsNew: () => true,
getThread: PRData.prototype.getThread
}, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, true);
});

describe('without approvals', () => {
const data = {
pr: firstTimerPR,
reviewers: noReviewers,
comments: [],
reviews: [],
commits: [],
collaborators: [],
labeledEvents: [],
authorIsNew: () => true,
getThread: PRData.prototype.getThread,
getLabeledEvents: async() => {
data.labeledEvents = [];
},
getCollaborators: async() => {
data.collaborators = collaborators;
}
};

it('should return false if PR has no labels', async() => {
const cli = new TestCLI();
data.getLabeledEvents = async() => {
data.labeledEvents = [];
};
const checker = new PRChecker(cli, data, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, false);
});

it('should return false if PR has no request-ci label', async() => {
const cli = new TestCLI();
data.getLabeledEvents = async() => {
data.labeledEvents = labeledEvents['no-request-ci'];
};
const checker = new PRChecker(cli, data, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, false);
});

it('should return false if PR has request-ci from non-collaborator', async() => {
const cli = new TestCLI();
data.getLabeledEvents = async() => {
data.labeledEvents = labeledEvents['recent-request-ci-non-collaborator'];
};
const checker = new PRChecker(cli, data, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, false);
});

it('should return false if PR has outdated request-ci from a collaborator', async() => {
const cli = new TestCLI();
data.getLabeledEvents = async() => {
data.labeledEvents = labeledEvents['old-request-ci-collaborator'];
};
const checker = new PRChecker(cli, data, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, false);
});

it('should return true if PR has recent request-ci from a collaborator', async() => {
const cli = new TestCLI();
data.getLabeledEvents = async() => {
data.labeledEvents = labeledEvents['recent-request-ci-collaborator'];
};
const checker = new PRChecker(cli, data, {}, argv);

const status = await checker.checkCommitsAfterReviewOrLabel();
assert.strictEqual(status, true);
});
});
});

describe('checkCommitsAfterReview', () => {
const cli = new TestCLI();

Expand Down

0 comments on commit 6cc2b1a

Please sign in to comment.