Skip to content

Commit

Permalink
feat: add ncu-cu resume <prid> command
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 19, 2022
1 parent bbad9a0 commit ed06933
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 9 deletions.
66 changes: 61 additions & 5 deletions bin/ncu-ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ const args = yargs(hideBin(process.argv))
},
handler
})
.command({
command: 'resume <prid>',
desc: 'Resume 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 <url>',
desc: 'Automatically detect CI type and show results',
Expand Down Expand Up @@ -253,10 +273,8 @@ class RunPRJobCommand {
return this.argv.prid;
}

async start() {
const {
cli, request, prid, repo, owner
} = this;
validate() {
const { cli, repo, owner } = this;
let validArgs = true;
if (!repo) {
validArgs = false;
Expand All @@ -270,10 +288,44 @@ class RunPRJobCommand {
}
if (!validArgs) {
this.cli.setExitCode(1);
}
return validArgs;
}

async start() {
const {
cli, request, prid, repo, owner
} = this;
if (!this.validate()) {
return;
}
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
if (!jobRunner.start()) {
if (!await jobRunner.start()) {
this.cli.setExitCode(1);
process.exitCode = 1;
}
}
}

class ResumePRJobCommand extends RunPRJobCommand {
async start() {
const {
cli, request, prid, repo, owner
} = this;
if (!this.validate()) {
return;
}
// Parse CI links from PR.
const parser = await JobParser.fromPRId(this, cli, request);
const ciMap = parser.parse();

if (!ciMap.has(PR)) {
cli.info(`No CI run detected from pull request ${prid}`);
}

const { jobid } = ciMap.get(PR);
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
if (!await jobRunner.resume()) {
this.cli.setExitCode(1);
process.exitCode = 1;
}
Expand Down Expand Up @@ -539,6 +591,10 @@ async function main(command, argv) {
const jobRunner = new RunPRJobCommand(cli, request, argv);
return jobRunner.start();
}
case 'resume': {
const jobResumer = new ResumePRJobCommand(cli, request, argv);
return jobResumer.start();
}
case 'rate': {
commandHandler = new RateCommand(cli, request, argv);
break;
Expand Down
7 changes: 7 additions & 0 deletions lib/ci/ci_type_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ JobParser.fromPR = async function(url, cli, request) {
return new JobParser(thread);
};

JobParser.fromPRId = async function({ owner, repo, prid }, cli, request) {
const data = new PRData({ owner, repo, prid }, cli, request);
await data.getThreadData();
const thread = data.getThread();
return new JobParser(thread);
};

export const CI_TYPES_KEYS = {
CITGM,
CITGM_NOBUILD,
Expand Down
1 change: 1 addition & 0 deletions lib/ci/jenkins_constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ACTION_TREE = 'actions[parameters[name,value]]';
const CHANGE_FIELDS = 'commitId,author[absoluteUrl,fullName],authorEmail,' +
'msg,date';
const CHANGE_TREE = `changeSet[items[${CHANGE_FIELDS}]]`;
export const BASIC_TREE = 'result,url,number';
export const PR_TREE =
`result,url,number,${ACTION_TREE},${CHANGE_TREE},builtOn,` +
`subBuilds[${BUILD_FIELDS},build[subBuilds[${BUILD_FIELDS}]]]`;
Expand Down
64 changes: 60 additions & 4 deletions lib/ci/run_ci.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import FormData from 'form-data';

import { BASIC_TREE } from './jenkins_constants.js';
import { TestBuild } from './build-types/test_build.js';
import {
CI_DOMAIN,
CI_TYPES,
CI_TYPES_KEYS
} from './ci_type_parser.js';

export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
export const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
export const CI_PR_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/build`;
export const CI_PR_RESUME_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/`;

export class RunPRJob {
constructor(cli, request, owner, repo, prid) {
constructor(cli, request, owner, repo, prid, jobid) {
this.cli = cli;
this.request = request;
this.owner = owner;
this.repo = repo;
this.prid = prid;
this.jobid = jobid;
}

async getCrumb() {
Expand All @@ -43,18 +47,28 @@ export class RunPRJob {
return payload;
}

async start() {
async #validateJenkinsCredentials() {
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;
return { crumb, success: false };
}
cli.stopSpinner('Jenkins credentials valid');

return { crumb, success: true };
}

async start() {
const { cli } = this;
const { crumb, success } = await this.#validateJenkinsCredentials();
if (success === false) {
return false;
}

try {
cli.startSpinner('Starting PR CI job');
const response = await this.request.fetch(CI_PR_URL, {
Expand All @@ -77,4 +91,46 @@ export class RunPRJob {
}
return true;
}

async resume() {
const { cli, request, jobid } = this;
const { crumb, success } = await this.#validateJenkinsCredentials();
if (success === false) {
return false;
}

try {
cli.startSpinner('Resuming PR CI job');
const path = `job/${CI_PR_NAME}/${jobid}/`;
const testBuild = new TestBuild(cli, request, path, BASIC_TREE);
const { result } = await testBuild.getBuildData();

if (result !== 'FAILURE') {
cli.stopSpinner(
`CI Job is in status ${result ?? 'RUNNING'}, skipping resume`,
this.cli.SPINNER_STATUS.FAILED);
return false;
}

const resume_url = `${CI_PR_RESUME_URL}${jobid}/resume`;
const response = await this.request.fetch(resume_url, {
method: 'POST',
headers: {
'Jenkins-Crumb': crumb
}
});
if (response.status !== 200) {
cli.stopSpinner(
`Failed to resume PR CI: ${response.status} ${response.statusText}`,
this.cli.SPINNER_STATUS.FAILED);
return false;
}

cli.stopSpinner('PR CI job successfully resumed');
} catch (err) {
cli.stopSpinner('Failed to resume CI', this.cli.SPINNER_STATUS.FAILED);
return false;
}
return true;
}
}
137 changes: 137 additions & 0 deletions test/unit/ci_resume.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import assert from 'assert';

import sinon from 'sinon';
import FormData from 'form-data';

import {
RunPRJob,
CI_CRUMB_URL,
CI_PR_NAME,
CI_PR_RESUME_URL
} from '../../lib/ci/run_ci.js';

import { CI_DOMAIN } from '../../lib/ci/ci_type_parser.js';
import TestCLI from '../fixtures/test_cli.js';
import { jobCache } from '../../lib/ci/build-types/job.js';

describe('Jenkins resume', () => {
const owner = 'nodejs';
const repo = 'node-auto-test';
const prid = 123456;
const jobid = 654321;
const crumb = 'asdf1234';

before(() => {
jobCache.disable();
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: '<pr base branch>',
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);
});
});

after(() => {
sinon.restore();
});

it('should return false if crumb fails', async() => {
const cli = new TestCLI();
const request = {
json: sinon.stub().throws()
};

const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
assert.strictEqual(await jobRunner.resume(), false);
});

it('should return false if run status not FAILURE', async() => {
const cli = new TestCLI();

const request = {
json: sinon.stub()
};

request.json.withArgs(CI_CRUMB_URL)
.returns(Promise.resolve({ crumb }));
request.json.withArgs(`https://${CI_DOMAIN}/job/${CI_PR_NAME}/${jobid}/api/json?tree=result%2Curl%2Cnumber`)
.returns(Promise.resolve({ result: null }));
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
assert.strictEqual(await jobRunner.resume(), false);
});

it('should resume node-pull-request job', async() => {
const cli = new TestCLI();

const request = {
fetch: sinon.stub()
.callsFake((url, { method, headers }) => {
assert.strictEqual(url, `${CI_PR_RESUME_URL}${jobid}/resume`);
assert.strictEqual(method, 'POST');
assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb });
return Promise.resolve({ status: 200 });
}),
json: sinon.stub()
};

request.json.withArgs(CI_CRUMB_URL)
.returns(Promise.resolve({ crumb }));
request.json.withArgs(`https://${CI_DOMAIN}/job/${CI_PR_NAME}/${jobid}/api/json?tree=result%2Curl%2Cnumber`)
.returns(Promise.resolve({ result: 'FAILURE' }));
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
assert.ok(await jobRunner.resume());
});

it('should fail if resuming node-pull-request throws', async() => {
const cli = new TestCLI();
const request = {
fetch: sinon.stub().throws(),
json: sinon.stub()
};

request.json.withArgs(CI_CRUMB_URL)
.returns(Promise.resolve({ crumb }));
request.json.withArgs(`https://${CI_DOMAIN}/job/${CI_PR_NAME}/${jobid}/api/json?tree=result%2Curl%2Cnumber`)
.returns(Promise.resolve({ result: 'FAILURE' }));

const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
assert.strictEqual(await jobRunner.resume(), false);
});

it('should return false if node-pull-request not resumed', async() => {
const cli = new TestCLI();

const request = {
fetch: sinon.stub()
.callsFake((url, { method, headers }) => {
assert.strictEqual(url, `${CI_PR_RESUME_URL}${jobid}/resume`);
assert.strictEqual(method, 'POST');
assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb });
return Promise.resolve({ status: 401 });
}),
json: sinon.stub()
};

request.json.withArgs(CI_CRUMB_URL)
.returns(Promise.resolve({ crumb }));
request.json.withArgs(`https://${CI_DOMAIN}/job/${CI_PR_NAME}/${jobid}/api/json?tree=result%2Curl%2Cnumber`)
.returns(Promise.resolve({ result: 'FAILURE' }));
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, jobid);
assert.strictEqual(await jobRunner.resume(), false);
});
});
3 changes: 3 additions & 0 deletions test/unit/ci_start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ describe('Jenkins', () => {
return FormData.prototype.append.wrappedMethod.bind(this)(key, value);
});
});
after(() => {
sinon.restore();
});

it('should fail if starting node-pull-request throws', async() => {
const cli = new TestCLI();
Expand Down

0 comments on commit ed06933

Please sign in to comment.