Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(build-cmd): option to interactively enable disabled job before triggering a build on it #71

Merged
merged 1 commit into from
Dec 7, 2020
Merged
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
81 changes: 81 additions & 0 deletions src/commands/build/__tests__/assert-job-buildable.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { enableJob, isJobBuildable } = require('../../../lib/jenkins');
const { getConfirmationToEnableTheJob } = require('../../../lib/prompt');
const { logNetworkErrors } = require('../../../lib/log');
const assertJobBuildable = require('../assert-job-buildable');

jest.mock('../../../lib/jenkins');
jest.mock('../../../lib/prompt');
jest.mock('../../../lib/log');

const spinnerStub = {
start: jest.fn(),
stop: jest.fn(),
fail: jest.fn(),
succeed: jest.fn(),
};
const branchName = 'foo';

describe('assertJobBuildable', () => {
beforeEach(() => {
isJobBuildable.mockImplementation(() => Promise.resolve(true));

// Note: mocked `process.exit` wont stop the execution
jest.spyOn(process, 'exit').mockImplementation(() => {});
});

afterEach(jest.clearAllMocks);

it('should not prompt confirmation when the job is buildable', async () => {
await assertJobBuildable(branchName, spinnerStub);

expect(spinnerStub.start).toHaveBeenCalledTimes(1);
expect(isJobBuildable).toHaveBeenCalledWith(branchName);
expect(getConfirmationToEnableTheJob).not.toHaveBeenCalled();
});

it('should catch all the exceptions locally', async () => {
const error = new Error('bar');
isJobBuildable.mockImplementation(() => Promise.reject(error));

await assertJobBuildable(branchName, spinnerStub);

expect(getConfirmationToEnableTheJob).not.toHaveBeenCalled();
expect(spinnerStub.fail).toHaveBeenCalledWith(error.message);
expect(logNetworkErrors).toHaveBeenCalledWith(error);
expect(process.exit).toHaveBeenCalled();
});

it('should prompt confirmation when the job is disabled', async () => {
isJobBuildable.mockImplementation(() => Promise.resolve(false));

await assertJobBuildable(branchName, spinnerStub);

expect(spinnerStub.stop).toHaveBeenCalled();
expect(getConfirmationToEnableTheJob).toHaveBeenCalled();
});

it('should end the process when the confirmation gets rejected', async () => {
getConfirmationToEnableTheJob.mockImplementation(() =>
Promise.resolve({ confirmation: false })
);
isJobBuildable.mockImplementation(() => Promise.resolve(false));

await assertJobBuildable(branchName, spinnerStub);

expect(spinnerStub.fail).toHaveBeenCalledWith('Aborted');
expect(process.exit).toHaveBeenCalledTimes(1);
});

it('should enable the job when the confirmation gets accepted', async () => {
getConfirmationToEnableTheJob.mockImplementation(() =>
Promise.resolve({ confirmation: false })
);
isJobBuildable.mockImplementation(() => Promise.resolve(false));

await assertJobBuildable(branchName, spinnerStub);

expect(spinnerStub.start).toHaveBeenNthCalledWith(2, 'Enabling the job');
expect(enableJob).toHaveBeenCalledWith(branchName);
expect(spinnerStub.succeed).toHaveBeenCalledWith('Job successfully enabled');
});
});
18 changes: 16 additions & 2 deletions src/commands/build/__tests__/build.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { red, yellow } = require('kleur');
const { yellow } = require('kleur');

const { getCurrentBranchName } = require('../../../lib/git-cmd');
const { triggerNewBuild, getRunningBuilds } = require('../../../lib/jenkins');
Expand All @@ -7,13 +7,15 @@ const { askConfirmationBeforeTriggeringNewBuild } = require('../../../lib/prompt
const { WatchError } = require('../../../lib/errors');
const reportBuildProgress = require('../watch-option');
const reportBuildStages = require('../stage-option');
const assertJobBuildable = require('../assert-job-buildable');

jest.mock('../../../lib/git-cmd');
jest.mock('../../../lib/log');
jest.mock('../../../lib/jenkins');
jest.mock('../../../lib/prompt');
jest.mock('../watch-option');
jest.mock('../stage-option');
jest.mock('../assert-job-buildable');

const spinner = {
start: jest.fn(),
Expand Down Expand Up @@ -52,12 +54,24 @@ describe('build', () => {
})
);

assertJobBuildable.mockImplementationOnce(() => Promise.resolve());

jest.spyOn(global.console, 'log').mockImplementation();
jest.spyOn(process, 'exit').mockImplementation();
});

afterEach(jest.clearAllMocks);

it('should assert job is buildable', async () => {
getRunningBuilds.mockImplementation(() => Promise.resolve([]));

const options = {};
await build(options);

expect(assertJobBuildable).toHaveBeenCalledWith(branchName, spinner);
expect(triggerNewBuild).toHaveBeenCalled();
});

it('should throw an error when the watch, stage options are enabled together', async () => {
const options = { watch: true, stage: true };
await build(options);
Expand Down Expand Up @@ -107,7 +121,7 @@ describe('build', () => {

await build();

expect(console.log.mock.calls[0][0]).toBe(red('Aborted'));
expect(spinner.fail).toHaveBeenCalledWith('Aborted');
expect(process.exit).toHaveBeenCalledTimes(1);
});

Expand Down
29 changes: 29 additions & 0 deletions src/commands/build/assert-job-buildable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { enableJob, isJobBuildable } = require('../../lib/jenkins');
const { getConfirmationToEnableTheJob } = require('../../lib/prompt');
const { logNetworkErrors } = require('../../lib/log');

async function assertJobBuildable(branchName, spinner) {
try {
spinner.start();

if (!(await isJobBuildable(branchName))) {
spinner.stop();

const { confirmation } = await getConfirmationToEnableTheJob();
if (!confirmation) {
spinner.fail('Aborted');
process.exit();
}

spinner.start('Enabling the job');
await enableJob(branchName);
spinner.succeed('Job successfully enabled');
}
} catch (error) {
spinner.fail(error.message);
logNetworkErrors(error);
process.exit();
}
}

module.exports = assertJobBuildable;
9 changes: 6 additions & 3 deletions src/commands/build/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const ora = require('ora');
const { red, yellow } = require('kleur');
const { yellow } = require('kleur');

const { getCurrentBranchName } = require('../../lib/git-cmd');
const { logNetworkErrors, debug } = require('../../lib/log');
Expand All @@ -8,6 +8,7 @@ const { askConfirmationBeforeTriggeringNewBuild } = require('../../lib/prompt');
const { ERROR_TYPE } = require('../../lib/errors');
const reportBuildStages = require('./stage-option');
const reportBuildProgress = require('./watch-option');
const assertJobBuildable = require('./assert-job-buildable');

const spinner = ora();

Expand All @@ -32,16 +33,18 @@ module.exports = async function build(options = {}) {
const branchName = getCurrentBranchName();
debug(`Branch name: ${branchName}`);

await assertJobBuildable(branchName, spinner);

try {
spinner.start();
if (!spinner.isSpinning) spinner.start();

const runningBuilds = await getRunningBuilds(branchName);
if (runningBuilds.length) {
spinner.stop();

const { confirmation } = await askConfirmationBeforeTriggeringNewBuild();
if (!confirmation) {
console.log(red('Aborted'));
spinner.fail('Aborted');
process.exit();
}

Expand Down
47 changes: 47 additions & 0 deletions src/lib/__tests__/jenkins.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
getQueueItem,
createProgressiveTextStream,
createBuildStageStream,
enableJob,
isJobBuildable,
} = require('../jenkins');
const { getGitRootDirPath } = require('../git-cmd');
const { JOB_TYPE } = require('../../config');
Expand Down Expand Up @@ -602,3 +604,48 @@ describe('createBuildStageStream', () => {
});
});
});

describe('isJobBuildable', () => {
const branchName = 'feature-z';
let mockServer;
let url;
beforeAll(() => {
mockServer = nock(jenkinsCredentials.url);
url = `${jobConfigPath}/job/${branchName}/api/json`;
});

it("should throw an error when it couldn't find the `buildable` property in the response", async () => {
mockServer
.get(url)
.query({ tree: 'buildable' })
.reply(200, {});

await expect(isJobBuildable(branchName)).rejects.toThrow(
"Failed to get the job's buildable status"
);
});

it("should return the job's buildable status", async () => {
mockServer
.get(url)
.query({ tree: 'buildable' })
.reply(200, { buildable: true });

expect(await isJobBuildable(branchName)).toBe(true);
});
});

describe('enableJob', () => {
const branchName = 'feature-z';
let mockServer;
let url;
beforeAll(() => {
mockServer = nock(jenkinsCredentials.url);
url = `${jobConfigPath}/job/${branchName}/enable`;
});

it('should enable the job', async () => {
mockServer.post(url).reply(302);
await enableJob(branchName);
});
});
36 changes: 35 additions & 1 deletion src/lib/jenkins.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function getJobUrl(branchName, includeCredentials = true) {
const config = store.get(getGitRootDirPath());
const baseUrl = getBaseUrl(config, includeCredentials);

// TODO: add other Jenkins job types
// TODO: support other Jenkins job types
switch (config.job.type) {
case JOB_TYPE.WorkflowJob:
return `${baseUrl}${config.job.path}`;
Expand Down Expand Up @@ -172,6 +172,21 @@ function filterRunningBuilds(builds) {
return builds.filter(build => build.status === STATUS_TYPES.inProgress);
}

async function getJob(branchName, fieldsToPick = []) {
const jobUrl = getJobUrl(branchName);

let qs = '';
if (fieldsToPick.length) {
qs = `tree=${fieldsToPick.join(',')}`;
}

const { body } = await client.get(`${jobUrl}/api/json?${qs}`, {
json: true,
});

return body;
}

exports.getJobLink = function(branchName) {
return getJobUrl(branchName, false);
};
Expand Down Expand Up @@ -314,3 +329,22 @@ exports.getQueueItem = async function(
return _getQueueItem();
});
};

exports.isJobBuildable = async function(branchName) {
const { buildable } = await getJob(branchName, ['buildable']);

if (typeof buildable !== 'boolean') {
throw Error("Failed to get the job's buildable status");
}

return buildable;
};

exports.enableJob = async function(branchName) {
const jobUrl = getJobUrl(branchName);

// Jenkins returns 302 (redirect) response if the request succeeds.
return client.post(`${jobUrl}/enable`, {
followRedirect: false,
});
};
12 changes: 12 additions & 0 deletions src/lib/prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,15 @@ exports.getDeleteConfigConfirmation = function() {

return prompts(question);
};

exports.getConfirmationToEnableTheJob = function() {
const question = {
type: 'confirm',
name: 'confirmation',
message:
'The job is currently disabled. Do you like to enable and trigger a build on it',
initial: true,
};

return prompts(question);
};