diff --git a/bin/ncu-ci b/bin/ncu-ci index 8037a26d..0b2a57d7 100755 --- a/bin/ncu-ci +++ b/bin/ncu-ci @@ -14,8 +14,12 @@ const { PRBuild, BenchmarkRun, CommitBuild, HealthBuild, listBuilds, FailureAggregator, jobCache } = require('../lib/ci/ci_result_parser'); +const { + RunPRJob +} = require('../lib/ci/run_ci'); const clipboardy = require('clipboardy'); const { writeJson, writeFile } = require('../lib/file'); +const { getMergedConfig } = require('../lib/config'); const { runPromise } = require('../lib/run'); const auth = require('../lib/auth'); @@ -70,6 +74,26 @@ const argv = yargs }, handler }) + .command({ + command: 'run ', + desc: 'Run CI for given PR', + builder: (yargs) => { + yargs + .positional('prid', { + describe: 'ID of the PR', + type: 'number' + }) + .option('owner', { + default: '', + describe: 'GitHub repository owner' + }) + .option('repo', { + default: '', + describe: 'GitHub repository name' + }); + }, + handler + }) .command({ command: 'url ', desc: 'Automatically detect CI type and show results', @@ -147,6 +171,54 @@ const commandToType = { benchmark: BENCHMARK }; +class RunPRJobCommand { + constructor(cli, request, argv) { + this.cli = cli; + this.request = request; + this.dir = process.cwd(); + this.argv = argv; + this.config = getMergedConfig(this.dir); + } + + get owner() { + return this.argv.owner || this.config.owner; + } + + get repo() { + return this.argv.repo || this.config.repo; + } + + get prid() { + return this.argv.prid; + } + + async start() { + const { + cli, request, prid, repo, owner + } = this; + let validArgs = true; + if (!repo) { + validArgs = false; + cli.error('GitHub repository is missing, please set it via ncu-config ' + + 'or pass it via the --repo option'); + } + if (!owner) { + cli.error('GitHub owner is missing, please set it via ncu-config ' + + 'or pass it via the --owner option'); + validArgs = false; + } + if (!validArgs) { + this.cli.setExitCode(1); + return; + } + const jobRunner = new RunPRJob(cli, request, owner, repo, prid); + if (!jobRunner.start()) { + this.cli.setExitCode(1); + process.exitCode = 1; + } + } +} + class CICommand { constructor(cli, request, argv) { this.cli = cli; @@ -343,6 +415,10 @@ async function main(command, argv) { let commandHandler; // Prepare queue. switch (command) { + case 'run': { + const jobRunner = new RunPRJobCommand(cli, request, argv); + return jobRunner.start(); + } case 'rate': { commandHandler = new RateCommand(cli, request, argv); break; diff --git a/lib/ci/run_ci.js b/lib/ci/run_ci.js new file mode 100644 index 00000000..f5e37a1b --- /dev/null +++ b/lib/ci/run_ci.js @@ -0,0 +1,78 @@ +'use strict'; + +const FormData = require('form-data'); + +const { + CI_DOMAIN, + CI_TYPES, + CI_TYPES_KEYS +} = require('./ci_type_parser'); + +const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`; +const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName; +const CI_PR_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/build`; + +class RunPRJob { + constructor(cli, request, owner, repo, prid) { + this.cli = cli; + this.request = request; + this.owner = owner; + this.repo = repo; + this.prid = prid; + } + + async getCrumb() { + try { + const { crumb } = await this.request.json(CI_CRUMB_URL); + return crumb; + } catch (e) { + return false; + } + } + + get payload() { + const payload = new FormData(); + payload.append('json', JSON.stringify({ + parameter: [ + { name: 'CERTIFY_SAFE', value: 'on' }, + { name: 'TARGET_GITHUB_ORG', value: this.owner }, + { name: 'TARGET_REPO_NAME', value: this.repo }, + { name: 'PR_ID', value: this.prid }, + { name: 'REBASE_ONTO', value: '' }, + { name: 'DESCRIPTION_SETTER_DESCRIPTION', value: '' } + ] + })); + return payload; + } + + async start() { + const { cli } = this; + cli.startSpinner('Validating Jenkins credentials'); + const crumb = await this.getCrumb(); + + if (crumb === false) { + cli.stopSpinner('Jenkins credentials invalid', + this.cli.SPINNER_STATUS.FAILED); + return false; + } + cli.stopSpinner('Jenkins credentials valid'); + + try { + cli.startSpinner('Starting PR CI job'); + await this.request.text(CI_PR_URL, { + method: 'POST', + headers: { + 'Jenkins-Crumb': crumb + }, + body: this.payload + }); + cli.stopSpinner('PR CI job successfully started'); + } catch (err) { + cli.stopSpinner('Failed to start CI', this.cli.SPINNER_STATUS.FAILED); + return false; + } + return true; + } +} + +module.exports = { RunPRJob, CI_CRUMB_URL, CI_PR_URL }; diff --git a/package.json b/package.json index e2d2cc82..96a7fe7d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "core-validate-commit": "^3.13.1", "execa": "^4.0.1", "figures": "^3.2.0", + "form-data": "^3.0.0", "fs-extra": "^9.0.0", "ghauth": "^4.0.0", "inquirer": "^7.1.0", diff --git a/test/unit/ci_start.js b/test/unit/ci_start.js new file mode 100644 index 00000000..518350a0 --- /dev/null +++ b/test/unit/ci_start.js @@ -0,0 +1,88 @@ +'use strict'; + +const assert = require('assert'); + +const sinon = require('sinon'); +const FormData = require('form-data'); + +const { + RunPRJob, + CI_CRUMB_URL, + CI_PR_URL +} = require('../../lib/ci/run_ci'); +const TestCLI = require('../fixtures/test_cli'); + +describe('Jenkins', () => { + it('should fail if starting node-pull-request fails', async() => { + const cli = new TestCLI(); + const crumb = 'asdf1234'; + const request = { + text: sinon.stub().throws(), + json: sinon.stub().withArgs(CI_CRUMB_URL) + .returns(Promise.resolve({ crumb })) + }; + const owner = 'nodejs'; + const repo = 'node-auto-test'; + const prid = 123456; + + const jobRunner = new RunPRJob(cli, request, owner, repo, prid); + assert.strictEqual(await jobRunner.start(), false); + }); + + it('should return false if crumb fails', async() => { + const cli = new TestCLI(); + const request = { + json: sinon.stub().throws() + }; + const owner = 'nodejs'; + const repo = 'node-auto-test'; + const prid = 123456; + + const jobRunner = new RunPRJob(cli, request, owner, repo, prid); + assert.strictEqual(await jobRunner.start(), false); + }); + + it('should start node-pull-request', async() => { + const cli = new TestCLI(); + const crumb = 'asdf1234'; + const owner = 'nodejs'; + const repo = 'node-auto-test'; + const prid = 123456; + + sinon.stub(FormData.prototype, 'append').callsFake(function(key, value) { + assert.strictEqual(key, 'json'); + const { parameter } = JSON.parse(value); + const expectedParameters = { + CERTIFY_SAFE: 'on', + TARGET_GITHUB_ORG: owner, + TARGET_REPO_NAME: repo, + PR_ID: prid, + REBASE_ONTO: '', + DESCRIPTION_SETTER_DESCRIPTION: '' + }; + for (const { name, value } of parameter) { + assert.strictEqual(value, expectedParameters[name]); + delete expectedParameters[name]; + } + assert.strictEqual(Object.keys(expectedParameters).length, 0); + + this._validated = true; + + return FormData.prototype.append.wrappedMethod.bind(this)(key, value); + }); + + const request = { + text: sinon.stub() + .callsFake((url, { method, headers, body }) => { + assert.strictEqual(url, CI_PR_URL); + assert.strictEqual(method, 'POST'); + assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb }); + assert.ok(body._validated); + }), + json: sinon.stub().withArgs(CI_CRUMB_URL) + .returns(Promise.resolve({ crumb })) + }; + const jobRunner = new RunPRJob(cli, request, owner, repo, prid); + assert.ok(await jobRunner.start()); + }); +});