Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion bin/ncu-ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ const args = yargs(hideBin(process.argv))
'If not provided, the command will use the SHA of the last approved commit.',
type: 'string'
})
.option('check-for-duplicates', {
describe: 'When set, NCU will query Jenkins recent builds to ensure ' +
'there is not an existing job for the same commit.',
type: 'boolean'
})
.option('owner', {
default: '',
describe: 'GitHub repository owner'
Expand Down Expand Up @@ -298,7 +303,10 @@ class RunPRJobCommand {
this.cli.setExitCode(1);
return;
}
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, this.argv.certifySafe);
const { certifySafe, checkForDuplicates } = this.argv;
const jobRunner = new RunPRJob(
cli, request, owner, repo, prid,
certifySafe, checkForDuplicates);
if (!(await jobRunner.start())) {
this.cli.setExitCode(1);
process.exitCode = 1;
Expand Down
3 changes: 1 addition & 2 deletions lib/ci/build-types/pr_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ const {
} = CIFailureParser;

export class PRBuild extends TestBuild {
constructor(cli, request, id, skipMoreThan) {
constructor(cli, request, id, skipMoreThan, tree = PR_TREE) {
const path = `job/node-test-pull-request/${id}/`;
const tree = PR_TREE;
super(cli, request, path, tree);
this.skipMoreThan = skipMoreThan;
this.commitBuild = null;
Expand Down
31 changes: 25 additions & 6 deletions lib/ci/run_ci.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { FormData } from 'undici';

import {
JobParser,
CI_DOMAIN,
CI_TYPES,
CI_TYPES_KEYS
} from './ci_type_parser.js';
import PRData from '../pr_data.js';
import { debuglog } from '../verbosity.js';
import PRChecker from '../pr_checker.js';
import { PRBuild } from './build-types/pr_build.js';

export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
Expand All @@ -17,7 +19,7 @@ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;

export class RunPRJob {
constructor(cli, request, owner, repo, prid, certifySafe) {
constructor(cli, request, owner, repo, prid, certifySafe, checkForDuplicates) {
this.cli = cli;
this.request = request;
this.owner = owner;
Expand All @@ -29,6 +31,7 @@ export class RunPRJob {
Promise.all([this.prData.getReviews(), this.prData.getCommits()]).then(() =>
(this.certifySafe = new PRChecker(cli, this.prData, request, {}).getApprovedTipOfHead())
);
this.checkForDuplicates = checkForDuplicates;
}

async getCrumb() {
Expand Down Expand Up @@ -70,7 +73,7 @@ export class RunPRJob {
}

async start() {
const { cli, certifySafe } = this;
const { cli, request, certifySafe, checkForDuplicates } = this;

if (!(await certifySafe)) {
cli.error('Refusing to run CI on potentially unsafe PR');
Expand All @@ -87,9 +90,25 @@ export class RunPRJob {
}
cli.stopSpinner('Jenkins credentials valid');

if (checkForDuplicates) {
await this.prData.getComments();
const { jobid, link } = new JobParser(this.prData.comments).parse().get('PR') ?? {};
const { actions } = jobid
? (await new PRBuild(cli, request, jobid, undefined, 'actions[parameters[name,value]]')
.getBuildData())
: {};
const { parameters } = actions?.find(a => 'parameters' in a) ?? {};
if (parameters?.find(c => c.name === 'COMMIT_SHA_CHECK')?.value === certifySafe) {
cli.info('Existing CI run found: ' + link);
cli.error('Refusing to start a potentially duplicate CI job. Use the ' +
'"Resume build" button in the Jenkins UI, or start a new CI manually.');
return false;
}
}

try {
cli.startSpinner('Starting PR CI job');
const response = await this.request.fetch(CI_PR_URL, {
const response = await request.fetch(CI_PR_URL, {
method: 'POST',
headers: {
'Jenkins-Crumb': crumb
Expand All @@ -106,10 +125,10 @@ export class RunPRJob {

// check if the job need a v8 build and trigger it
await this.prData.getPR();
const labels = this.prData.pr.labels;
if (labels.nodes.map(i => i.name).includes('v8 engine')) {
const { labels } = this.prData.pr;
if (labels?.nodes.some(i => i.name === 'v8 engine')) {
cli.startSpinner('Starting V8 CI job');
const response = await this.request.fetch(CI_V8_URL, {
const response = await request.fetch(CI_V8_URL, {
method: 'POST',
headers: {
'Jenkins-Crumb': crumb
Expand Down
130 changes: 129 additions & 1 deletion test/unit/ci_start.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, before, afterEach } from 'node:test';
import { describe, it, before, afterEach, beforeEach } from 'node:test';
import assert from 'assert';

import * as sinon from 'sinon';
Expand All @@ -13,6 +13,9 @@ import {
import PRChecker from '../../lib/pr_checker.js';

import TestCLI from '../fixtures/test_cli.js';
import { PRBuild } from '../../lib/ci/build-types/pr_build.js';
import { JobParser } from '../../lib/ci/ci_type_parser.js';
import PRData from '../../lib/pr_data.js';

describe('Jenkins', () => {
const owner = 'nodejs';
Expand Down Expand Up @@ -199,4 +202,129 @@ describe('Jenkins', () => {
});
}
});

describe('--check-for-duplicates', { concurrency: false }, () => {
beforeEach(() => {
sinon.replace(PRData.prototype, 'getComments', sinon.fake.resolves());
sinon.replace(PRData.prototype, 'getPR', sinon.fake.resolves());
sinon.replace(JobParser.prototype, 'parse',
sinon.fake.returns(new Map().set('PR', { jobid: 123456 })));
});
afterEach(() => {
sinon.restore();
});

const getParameters = (commitHash) =>
[
{
_class: 'hudson.model.BooleanParameterValue',
name: 'CERTIFY_SAFE',
value: true
},
{
_class: 'hudson.model.StringParameterValue',
name: 'COMMIT_SHA_CHECK',
value: commitHash
},
{
_class: 'hudson.model.StringParameterValue',
name: 'TARGET_GITHUB_ORG',
value: 'nodejs'
},
{
_class: 'hudson.model.StringParameterValue',
name: 'TARGET_REPO_NAME',
value: 'node'
},
{
_class: 'hudson.model.StringParameterValue',
name: 'PR_ID',
value: prid
},
{
_class: 'hudson.model.StringParameterValue',
name: 'REBASE_ONTO',
value: '<pr base branch>'
},
{
_class: 'com.wangyin.parameter.WHideParameterValue',
name: 'DESCRIPTION_SETTER_DESCRIPTION',
value: ''
}
];
const mockJenkinsResponse = parameters => ({
_class: 'com.tikal.jenkins.plugins.multijob.MultiJobBuild',
actions: [
{ _class: 'hudson.model.CauseAction' },
{ _class: 'hudson.model.ParametersAction', parameters },
{ _class: 'hudson.model.ParametersAction', parameters },
{ _class: 'hudson.model.ParametersAction', parameters },
{},
{ _class: 'hudson.model.CauseAction' },
{},
{},
{},
{},
{ _class: 'hudson.plugins.git.util.BuildData' },
{},
{},
{},
{},
{ _class: 'hudson.model.ParametersAction', parameters },
{
_class: 'hudson.plugins.parameterizedtrigger.BuildInfoExporterAction'
},
{
_class: 'com.tikal.jenkins.plugins.multijob.MultiJobTestResults'
},
{},
{},
{},
{},
{},
{},
{},
{},
{
_class: 'org.jenkinsci.plugins.displayurlapi.actions.RunDisplayAction'
}
]
});

it('should return false if already started', async() => {
const cli = new TestCLI();
sinon.replace(PRBuild.prototype, 'getBuildData',
sinon.fake.resolves(mockJenkinsResponse(getParameters('deadbeef'))));

const jobRunner = new RunPRJob(cli, {}, owner, repo, prid, 'deadbeef', true);
assert.strictEqual(await jobRunner.start(), false);
});
it('should return true when last CI is on a different commit', async() => {
const cli = new TestCLI();
sinon.replace(PRBuild.prototype, 'getBuildData',
sinon.fake.resolves(mockJenkinsResponse(getParameters('123456789abcdef'))));

const request = {
gql: sinon.stub().returns({
repository: {
pullRequest: {
labels: {
nodes: []
}
}
}
}),
fetch: sinon.stub()
.callsFake((url, { method, headers, body }) => {
assert.strictEqual(url, CI_PR_URL);
assert.strictEqual(method, 'POST');
assert.deepStrictEqual(headers, { 'Jenkins-Crumb': crumb });
return Promise.resolve({ status: 201 });
}),
json: sinon.stub().withArgs(CI_CRUMB_URL).resolves({ crumb })
};
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, 'deadbeef', true);
assert.strictEqual(await jobRunner.start(), true);
});
});
});
Loading