Skip to content

Commit a5213cd

Browse files
authored
feat(ncu-ci)!: require --certify-safe flag (#798)
Or ensure the PR has received at least one approving review since last time it was pushed.
1 parent 78ad337 commit a5213cd

File tree

5 files changed

+69
-9
lines changed

5 files changed

+69
-9
lines changed

bin/ncu-ci.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ const args = yargs(hideBin(process.argv))
113113
describe: 'ID of the PR',
114114
type: 'number'
115115
})
116+
.positional('certify-safe', {
117+
describe: 'If not provided, the command will reject PRs that have ' +
118+
'been pushed since the last review',
119+
type: 'boolean'
120+
})
116121
.option('owner', {
117122
default: '',
118123
describe: 'GitHub repository owner'
@@ -291,7 +296,7 @@ class RunPRJobCommand {
291296
this.cli.setExitCode(1);
292297
return;
293298
}
294-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
299+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, this.argv.certifySafe);
295300
if (!(await jobRunner.start())) {
296301
this.cli.setExitCode(1);
297302
process.exitCode = 1;

lib/ci/run_ci.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from './ci_type_parser.js';
88
import PRData from '../pr_data.js';
99
import { debuglog } from '../verbosity.js';
10+
import PRChecker from '../pr_checker.js';
1011

1112
export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
1213
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
@@ -16,13 +17,16 @@ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
1617
export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;
1718

1819
export class RunPRJob {
19-
constructor(cli, request, owner, repo, prid) {
20+
constructor(cli, request, owner, repo, prid, certifySafe) {
2021
this.cli = cli;
2122
this.request = request;
2223
this.owner = owner;
2324
this.repo = repo;
2425
this.prid = prid;
2526
this.prData = new PRData({ prid, owner, repo }, cli, request);
27+
this.certifySafe =
28+
certifySafe ||
29+
new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReview();
2630
}
2731

2832
async getCrumb() {
@@ -62,7 +66,13 @@ export class RunPRJob {
6266
}
6367

6468
async start() {
65-
const { cli } = this;
69+
const { cli, certifySafe } = this;
70+
71+
if (!certifySafe) {
72+
cli.error('Refusing to run CI on potentially unsafe PR');
73+
return false;
74+
}
75+
6676
cli.startSpinner('Validating Jenkins credentials');
6777
const crumb = await this.getCrumb();
6878

lib/pr_checker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ export default class PRChecker {
534534
);
535535

536536
if (reviewIndex === -1) {
537+
cli.warn('No approving reviews found');
537538
return false;
538539
}
539540

test/unit/ci_start.test.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, before } from 'node:test';
1+
import { describe, it, before, afterEach } from 'node:test';
22
import assert from 'assert';
33

44
import sinon from 'sinon';
@@ -9,6 +9,7 @@ import {
99
CI_CRUMB_URL,
1010
CI_PR_URL
1111
} from '../../lib/ci/run_ci.js';
12+
import PRChecker from '../../lib/pr_checker.js';
1213

1314
import TestCLI from '../fixtures/test_cli.js';
1415

@@ -51,7 +52,7 @@ describe('Jenkins', () => {
5152
.returns(Promise.resolve({ crumb }))
5253
};
5354

54-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
55+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, true);
5556
assert.strictEqual(await jobRunner.start(), false);
5657
});
5758

@@ -61,7 +62,7 @@ describe('Jenkins', () => {
6162
json: sinon.stub().throws()
6263
};
6364

64-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
65+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, true);
6566
assert.strictEqual(await jobRunner.start(), false);
6667
});
6768

@@ -89,7 +90,7 @@ describe('Jenkins', () => {
8990
json: sinon.stub().withArgs(CI_CRUMB_URL)
9091
.returns(Promise.resolve({ crumb }))
9192
};
92-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
93+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, true);
9394
assert.ok(await jobRunner.start());
9495
});
9596

@@ -108,7 +109,48 @@ describe('Jenkins', () => {
108109
json: sinon.stub().withArgs(CI_CRUMB_URL)
109110
.returns(Promise.resolve({ crumb }))
110111
};
111-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
112+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, true);
112113
assert.strictEqual(await jobRunner.start(), false);
113114
});
115+
116+
describe('without --certify-safe flag', { concurrency: false }, () => {
117+
afterEach(() => {
118+
sinon.restore();
119+
});
120+
for (const certifySafe of [true, false]) {
121+
it(`should return ${certifySafe} if PR checker reports it as ${
122+
certifySafe ? '' : 'potentially un'
123+
}safe`, async() => {
124+
const cli = new TestCLI();
125+
126+
sinon.replace(PRChecker.prototype, 'checkCommitsAfterReview',
127+
sinon.fake.returns(certifySafe));
128+
129+
const request = {
130+
gql: sinon.stub().returns({
131+
repository: {
132+
pullRequest: {
133+
labels: {
134+
nodes: []
135+
}
136+
}
137+
}
138+
}),
139+
fetch: sinon.stub()
140+
.callsFake((url, { method, headers, body }) => {
141+
assert.strictEqual(url, CI_PR_URL);
142+
assert.strictEqual(method, 'POST');
143+
assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb });
144+
assert.ok(body._validated);
145+
return Promise.resolve({ status: 201 });
146+
}),
147+
json: sinon.stub().withArgs(CI_CRUMB_URL)
148+
.returns(Promise.resolve({ crumb }))
149+
};
150+
151+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, false);
152+
assert.strictEqual(await jobRunner.start(), certifySafe);
153+
});
154+
}
155+
});
114156
});

test/unit/pr_checker.test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2179,7 +2179,9 @@ describe('PRChecker', () => {
21792179

21802180
it('should skip the check if there are no reviews', () => {
21812181
const { commits } = multipleCommitsAfterReview;
2182-
const expectedLogs = {};
2182+
const expectedLogs = {
2183+
warn: [['No approving reviews found']]
2184+
};
21832185

21842186
const data = {
21852187
pr: firstTimerPR,

0 commit comments

Comments
 (0)