nci
Version:
Flexible, open source continuous integration server written in node.js
334 lines (289 loc) • 9.11 kB
JavaScript
;
var Steppy = require('twostep').Steppy,
_ = require('underscore'),
EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits;
/**
* Facade entity which accumulates operations with currently running and
* db saved builds.
*/
function BuildsCollection(params) {
this.db = params.db;
}
exports.BuildsCollection = BuildsCollection;
inherits(BuildsCollection, EventEmitter);
BuildsCollection.prototype.setDistributor = function(distributor) {
this.distributor = distributor;
this._proxyDistributorEvent('buildUpdated', 'buildUpdated');
this._proxyDistributorEvent('buildQueued', 'buildQueued');
this._proxyDistributorEvent('buildStarted', 'buildStarted');
this._proxyDistributorEvent('buildStatusChanged', 'buildStatusChanged');
this._proxyDistributorEvent('buildCompleted', 'buildCompleted');
this._proxyDistributorEvent('buildCanceled', 'buildCanceled');
this._proxyDistributorEvent('buildLogLines', 'buildLogLines');
};
BuildsCollection.prototype._proxyDistributorEvent = function(source, dest) {
var self = this;
self.distributor.on(source, function() {
self.emit.apply(self, [dest].concat(_(arguments).toArray()));
});
};
/**
* Create build by running given project.
* - `params.projectName` - project to build
* - `params.withScmChangesOnly` - if true then build will be started only if
* there is scm changes for project
* - `params.queueQueued` - if true then currently queued project can be queued
* again
* - `params.initiator` - contains information about initiator of the build,
* must contain `type` property e.g. when one build triggers another:
* initiator: {type: 'build', id: 123, number: 10, project: {name: 'project1'}}
* - `params.buildParams` - params for current build (override project config)
* - `params.buildParams.scmRev` - target revision for the build
* - `params.env` - target environment for the build
*
* `result` will contain created `builds` if they were created, every build
* with at least following fields: id, status, completed, project, params,
* createDate.
*
* @param {Object} params
* @param {Function} [callback(err,result)]
*/
BuildsCollection.prototype.create = function(params, callback) {
this.distributor.run(params, callback);
};
/**
* Cancel build by id.
* Queued or running build can be canceled.
* - `params.buildId` - id of target build to cancel
* - `params.canceledBy` - contains information about initiator of cancel,
* must contain `type` property e.g. when canceled via http api:
* canceledBy: {type: 'httpApi'}
*
* @param {Object} params
* @param {Function} [callback(err)]
*/
BuildsCollection.prototype.cancel = function(params, callback) {
// fallback for old signature when only id were passed
// TODO: remove that fallback on next minor release
if (!_(params).isObject()) {
params = {buildId: params};
}
this.distributor.cancel(params, callback);
};
/**
* Get build by id.
*
* @param {Number} id
* @param {Function} callback(err,build)
*/
BuildsCollection.prototype.get = function(id, callback) {
this.db.builds.find({start: {id: id}}, function(err, builds) {
callback(err, builds && builds[0]);
});
};
/**
* Get log lines for the given build.
* - `params.buildId` - target build
* - `params.from` - if set then lines from that number will be returned
* - `params.to` - if set then lines to that number will be returned
*
* @param {Object} params
* @param {Function} callback(err,logLinesData)
*/
BuildsCollection.prototype.getLogLines = function(params, callback) {
var self = this;
Steppy(
function() {
var findParams = {
start: {buildId: params.buildId},
end: {buildId: params.buildId}
};
if (params.from) findParams.start.number = params.from;
if (params.to) findParams.end.number = params.to;
var count = params.from && params.to ? params.to - params.from + 1: 0;
self.db.logLines.find(findParams, this.slot());
this.pass(count);
},
function(err, logLines, count) {
this.pass({
lines: logLines,
isLast: count ? logLines.length < count : true
});
},
callback
);
};
BuildsCollection.prototype.getLogLinesTail = function(params, callback) {
var self = this;
Steppy(
function() {
var findParams = {
reverse: true,
start: {buildId: params.buildId},
limit: params.limit
};
self.db.logLines.find(findParams, this.slot());
},
function(err, logLines) {
var lines = logLines.reverse(),
total = logLines.length ? logLines[logLines.length - 1].number : 0;
this.pass({lines: lines, total: total});
},
callback
);
};
/**
* Calculate average build duration for the given builds.
*
* @param {Object[]} builds
*/
BuildsCollection.prototype.getAvgBuildDuration = function(builds) {
var durationsSum = _(builds).reduce(function(sum, build) {
return sum + (build.endDate - build.startDate);
}, 0);
return Math.round(durationsSum / builds.length);
};
/**
* Get builds sorted by date in descending order.
* - `params.projectName` - optional project filter
* - `params.status` - optional status filter, can be used only when
* `params.projectName` is set. When used builds in the result will contain
* only following fields: id, number, startDate, endDate
* - `params.filter` - custom filter function which accepts build and returns
* boolean
* - `params.limit` - maximum builds count to get
*
* @param {Object} params
* @param {Function} callback(err,builds)
*/
BuildsCollection.prototype.getRecent = function(params, callback) {
var self = this;
Steppy(
function() {
var findParams = {
start: {},
filter: params.filter,
limit: params.limit
};
// such condition for match one of projections:
// projectName, descCreateDate
// projectName, status, descCreateDate
// or just descCreateDate projection if project name is not set
if (params.projectName) {
findParams.start.projectName = params.projectName;
if (params.status) findParams.start.status = params.status;
}
findParams.start.descCreateDate = '';
self.db.builds.find(findParams, this.slot());
},
callback
);
};
/**
* Get info about current done builds streak.
* - `params.projectName` - optional project filter
*
* @param {Object} params
* @param {Function} callback(err,doneStreak)
*/
BuildsCollection.prototype.getDoneStreak = function(params, callback) {
var self = this;
Steppy(
function() {
var start = {};
if (params.projectName) start.projectName = params.projectName;
start.descCreateDate = '';
// tricky but effective streak counting inside filter goes below
var doneBuildsStreakCallback = _(this.slot()).once(),
doneBuildsStreak = {buildsCount: 0};
self.db.builds.find({
start: start,
filter: function(build) {
// error exits streak
if (build.status === 'error') {
doneBuildsStreakCallback(null, doneBuildsStreak);
return true;
}
if (build.status === 'done') {
doneBuildsStreak.buildsCount++;
}
},
limit: 1
}, function(err) {
doneBuildsStreakCallback(err, doneBuildsStreak);
});
},
callback
);
};
// complete uncompleted builds e.g. after server restart
BuildsCollection.prototype.completeUncompleted = function(params, callback) {
params = params || {};
var self = this;
Steppy(
function() {
self.db.builds.find({
start: {descCreateDate: ''},
limit: 1000
}, this.slot());
},
function(err, lastBuilds) {
var uncompletedBuilds = _(lastBuilds).filter(function(lastBuild) {
return !lastBuild.completed;
});
var completeGroup = this.makeGroup();
if (uncompletedBuilds.length) {
var queuedAndOtherUncompletedBuilds = _(uncompletedBuilds).partition(
function(uncompletedBuild) {
return uncompletedBuild.status === 'queued';
}
);
var queuedBuilds = queuedAndOtherUncompletedBuilds[0];
uncompletedBuilds = queuedAndOtherUncompletedBuilds[1];
if (queuedBuilds.length) {
if (params.logger) {
params.logger.log(
'Remove queued uncompleted builds: %s',
_(queuedBuilds).pluck('id').join(', ')
);
}
self.db.builds.del(queuedBuilds, completeGroup.slot());
}
if (uncompletedBuilds.length) {
if (params.logger) {
params.logger.log(
'Complete with interrupted error uncompleted ' +
'in-progress builds: %s',
_(uncompletedBuilds).pluck('id').join(', ')
);
}
_(uncompletedBuilds).each(function(uncompletedBuild) {
var endDate = (
uncompletedBuild.startDate ||
uncompletedBuild.createDate
);
var sumDuration = _(uncompletedBuild.stepTimings).reduce(
function(sum, timing) {
return sum + timing.duration;
},
0
) || 0;
endDate += sumDuration;
self.db.builds.update(
{id: uncompletedBuild.id},
{
endDate: endDate,
status: 'error',
completed: true,
error: {message: 'interrupted by server restart'}
},
completeGroup.slot()
);
});
}
}
},
callback
);
};