Skip to content

Commit 332576a

Browse files
authored
ncu-ci: command to start CI for PRs (#445)
1 parent 336dc7f commit 332576a

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed

bin/ncu-ci

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ const {
2727
FailureAggregator,
2828
jobCache
2929
} = require('../lib/ci/ci_result_parser');
30+
const {
31+
RunPRJob
32+
} = require('../lib/ci/run_ci');
3033
const clipboardy = require('clipboardy');
3134
const { writeJson, writeFile } = require('../lib/file');
35+
const { getMergedConfig } = require('../lib/config');
3236

3337
const { runPromise } = require('../lib/run');
3438
const auth = require('../lib/auth');
@@ -84,6 +88,26 @@ const argv = yargs
8488
},
8589
handler
8690
})
91+
.command({
92+
command: 'run <prid>',
93+
desc: 'Run CI for given PR',
94+
builder: (yargs) => {
95+
yargs
96+
.positional('prid', {
97+
describe: 'ID of the PR',
98+
type: 'number'
99+
})
100+
.option('owner', {
101+
default: '',
102+
describe: 'GitHub repository owner'
103+
})
104+
.option('repo', {
105+
default: '',
106+
describe: 'GitHub repository name'
107+
});
108+
},
109+
handler
110+
})
87111
.command({
88112
command: 'url <url>',
89113
desc: 'Automatically detect CI type and show results',
@@ -205,6 +229,54 @@ const commandToType = {
205229
citgm: CITGM
206230
};
207231

232+
class RunPRJobCommand {
233+
constructor(cli, request, argv) {
234+
this.cli = cli;
235+
this.request = request;
236+
this.dir = process.cwd();
237+
this.argv = argv;
238+
this.config = getMergedConfig(this.dir);
239+
}
240+
241+
get owner() {
242+
return this.argv.owner || this.config.owner;
243+
}
244+
245+
get repo() {
246+
return this.argv.repo || this.config.repo;
247+
}
248+
249+
get prid() {
250+
return this.argv.prid;
251+
}
252+
253+
async start() {
254+
const {
255+
cli, request, prid, repo, owner
256+
} = this;
257+
let validArgs = true;
258+
if (!repo) {
259+
validArgs = false;
260+
cli.error('GitHub repository is missing, please set it via ncu-config ' +
261+
'or pass it via the --repo option');
262+
}
263+
if (!owner) {
264+
cli.error('GitHub owner is missing, please set it via ncu-config ' +
265+
'or pass it via the --owner option');
266+
validArgs = false;
267+
}
268+
if (!validArgs) {
269+
this.cli.setExitCode(1);
270+
return;
271+
}
272+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
273+
if (!jobRunner.start()) {
274+
this.cli.setExitCode(1);
275+
process.exitCode = 1;
276+
}
277+
}
278+
}
279+
208280
class CICommand {
209281
constructor(cli, request, argv) {
210282
this.cli = cli;
@@ -457,6 +529,10 @@ async function main(command, argv) {
457529
let commandHandler;
458530
// Prepare queue.
459531
switch (command) {
532+
case 'run': {
533+
const jobRunner = new RunPRJobCommand(cli, request, argv);
534+
return jobRunner.start();
535+
}
460536
case 'rate': {
461537
commandHandler = new RateCommand(cli, request, argv);
462538
break;

lib/ci/run_ci.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
const FormData = require('form-data');
4+
5+
const {
6+
CI_DOMAIN,
7+
CI_TYPES,
8+
CI_TYPES_KEYS
9+
} = require('./ci_type_parser');
10+
11+
const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
12+
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
13+
const CI_PR_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/build`;
14+
15+
class RunPRJob {
16+
constructor(cli, request, owner, repo, prid) {
17+
this.cli = cli;
18+
this.request = request;
19+
this.owner = owner;
20+
this.repo = repo;
21+
this.prid = prid;
22+
}
23+
24+
async getCrumb() {
25+
try {
26+
const { crumb } = await this.request.json(CI_CRUMB_URL);
27+
return crumb;
28+
} catch (e) {
29+
return false;
30+
}
31+
}
32+
33+
get payload() {
34+
const payload = new FormData();
35+
payload.append('json', JSON.stringify({
36+
parameter: [
37+
{ name: 'CERTIFY_SAFE', value: 'on' },
38+
{ name: 'TARGET_GITHUB_ORG', value: this.owner },
39+
{ name: 'TARGET_REPO_NAME', value: this.repo },
40+
{ name: 'PR_ID', value: this.prid },
41+
{ name: 'REBASE_ONTO', value: '<pr base branch>' },
42+
{ name: 'DESCRIPTION_SETTER_DESCRIPTION', value: '' }
43+
]
44+
}));
45+
return payload;
46+
}
47+
48+
async start() {
49+
const { cli } = this;
50+
cli.startSpinner('Validating Jenkins credentials');
51+
const crumb = await this.getCrumb();
52+
53+
if (crumb === false) {
54+
cli.stopSpinner('Jenkins credentials invalid',
55+
this.cli.SPINNER_STATUS.FAILED);
56+
return false;
57+
}
58+
cli.stopSpinner('Jenkins credentials valid');
59+
60+
try {
61+
cli.startSpinner('Starting PR CI job');
62+
await this.request.text(CI_PR_URL, {
63+
method: 'POST',
64+
headers: {
65+
'Jenkins-Crumb': crumb
66+
},
67+
body: this.payload
68+
});
69+
cli.stopSpinner('PR CI job successfully started');
70+
} catch (err) {
71+
cli.stopSpinner('Failed to start CI', this.cli.SPINNER_STATUS.FAILED);
72+
return false;
73+
}
74+
return true;
75+
}
76+
}
77+
78+
module.exports = { RunPRJob, CI_CRUMB_URL, CI_PR_URL };

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"core-validate-commit": "^3.13.1",
4040
"execa": "^4.0.1",
4141
"figures": "^3.2.0",
42+
"form-data": "^3.0.0",
4243
"fs-extra": "^9.0.0",
4344
"ghauth": "^4.0.0",
4445
"inquirer": "^7.1.0",

test/unit/ci_start.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
5+
const sinon = require('sinon');
6+
const FormData = require('form-data');
7+
8+
const {
9+
RunPRJob,
10+
CI_CRUMB_URL,
11+
CI_PR_URL
12+
} = require('../../lib/ci/run_ci');
13+
const TestCLI = require('../fixtures/test_cli');
14+
15+
describe('Jenkins', () => {
16+
it('should fail if starting node-pull-request fails', async() => {
17+
const cli = new TestCLI();
18+
const crumb = 'asdf1234';
19+
const request = {
20+
text: sinon.stub().throws(),
21+
json: sinon.stub().withArgs(CI_CRUMB_URL)
22+
.returns(Promise.resolve({ crumb }))
23+
};
24+
const owner = 'nodejs';
25+
const repo = 'node-auto-test';
26+
const prid = 123456;
27+
28+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
29+
assert.strictEqual(await jobRunner.start(), false);
30+
});
31+
32+
it('should return false if crumb fails', async() => {
33+
const cli = new TestCLI();
34+
const request = {
35+
json: sinon.stub().throws()
36+
};
37+
const owner = 'nodejs';
38+
const repo = 'node-auto-test';
39+
const prid = 123456;
40+
41+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
42+
assert.strictEqual(await jobRunner.start(), false);
43+
});
44+
45+
it('should start node-pull-request', async() => {
46+
const cli = new TestCLI();
47+
const crumb = 'asdf1234';
48+
const owner = 'nodejs';
49+
const repo = 'node-auto-test';
50+
const prid = 123456;
51+
52+
sinon.stub(FormData.prototype, 'append').callsFake(function(key, value) {
53+
assert.strictEqual(key, 'json');
54+
const { parameter } = JSON.parse(value);
55+
const expectedParameters = {
56+
CERTIFY_SAFE: 'on',
57+
TARGET_GITHUB_ORG: owner,
58+
TARGET_REPO_NAME: repo,
59+
PR_ID: prid,
60+
REBASE_ONTO: '<pr base branch>',
61+
DESCRIPTION_SETTER_DESCRIPTION: ''
62+
};
63+
for (const { name, value } of parameter) {
64+
assert.strictEqual(value, expectedParameters[name]);
65+
delete expectedParameters[name];
66+
}
67+
assert.strictEqual(Object.keys(expectedParameters).length, 0);
68+
69+
this._validated = true;
70+
71+
return FormData.prototype.append.wrappedMethod.bind(this)(key, value);
72+
});
73+
74+
const request = {
75+
text: sinon.stub()
76+
.callsFake((url, { method, headers, body }) => {
77+
assert.strictEqual(url, CI_PR_URL);
78+
assert.strictEqual(method, 'POST');
79+
assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb });
80+
assert.ok(body._validated);
81+
}),
82+
json: sinon.stub().withArgs(CI_CRUMB_URL)
83+
.returns(Promise.resolve({ crumb }))
84+
};
85+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
86+
assert.ok(await jobRunner.start());
87+
});
88+
});

0 commit comments

Comments
 (0)