travis-after-all
Version:
Script that can help you execute code only once in a Travis CI build matrix
553 lines (368 loc) • 14.2 kB
JavaScript
var crypto = require('crypto');
var https = require('https');
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var travis = {
// Travis CI set environment variables
// https://docs.travis-ci.com/user/ci-environment/#Environment-variables
CURRENT_JOB_ID: parseInt(process.env.TRAVIS_JOB_ID, 10),
CURRENT_JOB_TEST_RESULT: parseInt(process.env.TRAVIS_TEST_RESULT, 10),
// Travis CI API URLs
// https://docs.travis-ci.com/api/
API_BUILD_URL: 'https://api.travis-ci.org/builds/' + parseInt(process.env.TRAVIS_BUILD_ID, 10),
API_JOBS_URL: 'https://api.travis-ci.org/jobs/'
};
var configs = {
callback: undefined,
CHECK_INTERVAL_LIMIT: 15000,
checkInterval: 3000,
keys: {
'FAILURE': 'travis-after-all: failed',
'SUCCESS': 'travis-after-all: succeeded'
},
LOG_PREFIX: '[travis-after-all]',
};
// ---------------------------------------------------------------------
function allJobsAreDone(jobs) {
return jobs.every(function (job) {
return job.result !== null;
});
}
function allJobsPassed(jobs) {
return jobs.every(function (job) {
// A job passed if it either succeeded or it failed but
// it was allowed to fail (has `allow_failure` set to true).
//
// https://docs.travis-ci.com/user/customizing-the-build/#Rows-that-are-Allowed-to-Fail
return job.result === 0 || ( job.result === 1 && job.allow_failure === true );
});
}
function endJob(code, error) {
if ( error !== undefined ) {
log(error.message);
}
if ( code === 2 ) {
log('Some other job was assigned to do the task');
}
if ( typeof configs.callback === 'function' ) {
configs.callback(code, error);
} else {
process.exit(code);
}
}
function generateToken(text, key) {
return crypto.createHmac('sha256', key).update('' + text).digest('hex');
}
function getBuildData() {
getJSON(travis.API_BUILD_URL, function (data) {
var jobs = data.matrix;
// For the current job the result is already
// known, so there is no need to figure that out.
jobs.some(function (job) {
if ( job.id === travis.CURRENT_JOB_ID ) {
job.result = travis.CURRENT_JOB_TEST_RESULT;
return true;
}
return false;
});
handleJob(jobs);
});
}
function getJSON(url, callback) {
https.get(url, function(response) {
var body = '';
response.setEncoding('utf8');
response.on('data', function (chunk) {
body += chunk;
});
response.on('end', function () {
try {
callback(JSON.parse(body));
} catch (error) {
error.message = 'Failed to parse JSON from "' + url +'".\nError: ' + error.message;
endJob(3, error);
}
});
}).on('error', function (error) {
endJob(3, error);
});
}
function getJob(jobs, id) {
var result;
jobs.some(function (job) {
if ( job.id === id ) {
result = job;
return true;
}
return false;
});
return result;
}
function getJobData(jobList) {
var currentJob;
var undoneJobs;
if ( jobList.index < jobList.jobs.length ) {
currentJob = jobList.jobs[jobList.index];
jobList.index++;
if ( currentJob.id === travis.CURRENT_JOB_ID ||
currentJob.finished_at !== null && currentJob.result !== null ) {
getJobData(jobList);
} else {
getJSON(travis.API_JOBS_URL + currentJob.id, function (data) {
var jobLog = data.log;
// If written, get the result of the job from
// the special token written within its log.
if ( jobLog.indexOf(generateToken(currentJob.id, configs.keys.SUCCESS)) !== -1 ) {
currentJob.result = 0;
} else if ( jobLog.indexOf(generateToken(currentJob.id, configs.keys.FAILURE)) !== -1 ) {
currentJob.result = 1;
}
currentJob.finished_at = data.finished_at;
getJobData(jobList);
});
}
} else {
handleJob(jobList.jobs);
}
}
function getJobNumbersOfUndoneJobs(jobs) {
var undoneJobs = [];
jobs.forEach(function (job) {
if ( job.finished_at === null ) {
undoneJobs.push(job.number);
}
});
return undoneJobs;
}
function getPreviousJobs(jobs, id) {
var result = [];
jobs.some(function (job) {
if ( job.id === id ) {
return true;
}
result.push(job);
return false;
});
return result;
}
function log(msg) {
console.log('%s %s', configs.LOG_PREFIX, msg);
}
function logJobResult(job) {
var msg = '';
var token;
// Create a special token based on the result of the job (this
// is done so that the chance of that text existing in the log
// is minimal / non-existent).
if ( travis.CURRENT_JOB_TEST_RESULT === 0 ) {
token = generateToken(travis.CURRENT_JOB_ID, configs.keys.SUCCESS);
msg += 'Job succeeded (' + token + ')';
} else {
token = generateToken(travis.CURRENT_JOB_ID, configs.keys.FAILURE);
msg += 'Job failed (' + token + ')';
}
// If the token is already present in the log, don't write
// it again (this is done in order to reduce the output in
// the case where this script is executed multiple times -
// e.g.: the user uses multiple scripts that include this
// script).
if ( job.log.indexOf(token) === -1 ) {
log(msg);
}
}
function removeJob(jobs, id) {
var result = [];
jobs.forEach(function (job) {
if ( job.id !== id ) {
result.push(job);
}
});
return result;
}
function thereIsAFailingJob(jobs) {
return jobs.some(function (job) {
return job.result === 1 && job.allow_failure === false;
});
}
function thereIsASuccessfulJob(jobs) {
return jobs.some(function (job) {
return job.result === 0;
});
}
function waitThenGetJobData(jobs) {
var jobList = {
index: 0,
jobs: jobs
};
var undoneJobs = getJobNumbersOfUndoneJobs(removeJob(jobList.jobs, travis.CURRENT_JOB_ID));
if ( undoneJobs.length !== 0 ) {
log('Waiting for ' + undoneJobs.join(', ').replace(/,(?!.*,)/, ' and') + ' to be done...');
}
// If the jobs take longer to be done, gradually increase
// the check interval time up to the specified limit.
configs.checkInterval = ( configs.checkInterval * 2 > configs.CHECK_INTERVAL_LIMIT ?
configs.CHECK_INTERVAL_LIMIT : configs.checkInterval * 2 );
setTimeout(function () {
getJobData(jobList);
}, configs.checkInterval);
}
// ---------------------------------------------------------------------
// Conventions:
//
// * For builds that failed, or for builds that succeeded but had
// all their jobs failed but being allowed to fail), the first
// job will be assign to run the user defined fail tasks.
//
// * For builds that succeeded, the first successful job will be
// assign to run the user defined success tasks.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function handleJob(jobs) {
var currentJob = getJob(jobs, travis.CURRENT_JOB_ID);
if ( currentJob.result === 1 ) {
if ( currentJob.allow_failure === false ) {
handleFailingJob(currentJob, jobs);
} else {
handleFailingJobAllowedToFail(currentJob, jobs);
}
} else {
handleSuccessfulJob(currentJob, jobs);
}
}
function handleFailingJob(currentJob, jobs) {
// Since the current job failed, the build also failed, therefore,
// the current job can be ended with the appropriate code.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var firstJob = jobs[0];
// If the current job is the first one in the build, assign it
// to run the user defined fail tasks.
if ( currentJob.id === firstJob.id ) {
endJob(1);
// Otherwise, don't assign it to do anything as the first job
// in the build will be assign to run the user defined fail tasks.
} else {
endJob(2);
}
}
function handleFailingJobAllowedToFail(currentJob, jobs) {
// Since the current job failed, but it was allowed to fail,
// the status of the build is unknown, therefore, the current
// job can only be ended with the appropriate code if there is
// enough information about the status of the other jobs.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var firstJob = jobs[0];
var otherJobs = removeJob(jobs, currentJob.id);
if ( currentJob.id === firstJob.id ) {
// If there is at least one job that failed, the build
// failed, therefore, assign the current job to run the
// user defined fail tasks.
if ( thereIsAFailingJob(otherJobs) ) {
endJob(1);
// Otherwise
} else {
// Note: By this point it is known that currently there
// aren't any jobs that were not allowed to fail and did
// fail.
if ( allJobsAreDone(otherJobs) ) {
// If there is a successful job, that job will be
// assigned to run the user defined success tasks,
// therefore, the current job doesn't need to be
// assigned to do anything.
if ( thereIsASuccessfulJob(otherJobs) ) {
endJob(2);
// Otherwise, there are only jobs that where allowed
// to fail and all of them fail, therefore, the build
// failed, so assign the current job to run the user
// defined fail tasks.
} else {
endJob(1);
}
// If this point is reached, it means a decision cannot
// be made because there isn't enough information. So, we
// need to wait a bit, check the status of the other jobs,
// and try again.
} else {
waitThenGetJobData(jobs);
}
}
// If the current job is not the first job, no matter what the
// status of build will be, this job won't be assign to do any
// tasks (if the build failed, the first job will be assign to
// do the fail tasks, and if the build succeeded, the first
// successful job will be assigned to do the success tasks)
} else {
endJob(2);
}
}
function handleSuccessfulJob(currentJob, jobs) {
// Since the current job is a successful job, the status of the
// build is unknown, therefore, the current job can only be ended
// with the appropriate code if there is enough information about
// the status of the other jobs
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var firstJob = jobs[0];
var otherJobs = removeJob(jobs, currentJob.id);
var previousJobs = getPreviousJobs(jobs, currentJob.id);
// If a job failed, it means the build failed, therefore,
if ( thereIsAFailingJob(otherJobs) ) {
// If the current job is the first one in the build, assign
// it to run the user defined fail tasks.
if ( currentJob.id === firstJob.id ) {
endJob(1);
// Otherwise, don't assign it to do anything as the first
// job in the build will be assign to run the user defined
// fail tasks.
} else {
endJob(2);
}
} else {
// If there is a previous successful job, no matter what the
// status of the build will be, this job won't be assign to do
// any tasks (if the build succeeded a previous successful job
// will be assigned to do the success tasks, and if the build
// fails, the first job in the build will be assign to do the
// fail tasks).
if ( thereIsASuccessfulJob(previousJobs) ) {
endJob(2);
} else {
// Note: By this point it is known that currently there
// aren't any jobs that were not allowed to fail and did
// fail, or any previous successful jobs.
// If all jobs are done, it means the current job
// is the first successful job in a successful build,
// therefore, assign it to run the success tasks.
if ( allJobsAreDone(otherJobs) ) {
endJob(0);
// If this point is reached, it means a decision cannot
// be made because there isn't enough information. So, we
// need to wait a bit, check the status of the other jobs,
// and try again.
} else {
waitThenGetJobData(jobs);
}
}
}
}
// ---------------------------------------------------------------------
function main(callback) {
configs.callback = callback;
getJSON(travis.API_JOBS_URL + travis.CURRENT_JOB_ID, function (data) {
// Write the result of current job in its log if it isn't
// written already (this needs to be done because there isn't
// any other way for the other jobs to know the result of the
// current job without it actually be finished).
logJobResult(data);
// Get the list of jobs for this build and start checking for
// their results.
getBuildData();
});
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Based on how this script is called,
// invoke `main` differentlly.
// 1) If this script is called directly:
if ( require.main === module ||
module.parent === undefined ) {
main();
// 2) If it's required as a module:
} else {
module.exports = main;
}