UNPKG

nci

Version:

Flexible, open source continuous integration server written in node.js

371 lines (316 loc) 8.19 kB
'use strict'; var Steppy = require('twostep').Steppy, _ = require('underscore'), EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, killTree = require('tree-kill'); function Executor(params) { this.project = params.project; this.setParams(params); } exports.Executor = Executor; inherits(Executor, EventEmitter); // set optional params Executor.prototype.setParams = function(params) { if (params.env) { this.env = params.env; } if (params.envVars) { this.envVars = params.envVars; } }; Executor.prototype._getSources = function(params, callback) { var self = this, scm; Steppy( function() { self._getChanges(params, this.slot()); }, function(err, data) { scm = data.scm; this.pass(data.changes); scm.update(data.rev, this.slot()); }, function(err, changes) { scm.getCurrent(this.slot()); this.pass(changes); scm.getRev(params.rev, this.slot()); }, function(err, currentRev, changes, latestRev) { this.pass({ rev: currentRev, changes: changes, isLatest: currentRev.id === latestRev.id }); }, callback ); }; Executor.prototype._listenCommand = function(command) { var self = this; command.on('stdin', function(data) { self.emit('data', '> ' + String(data)); }); command.on('stdout', function(data) { self.emit('data', String(data)); }); command.on('stderr', function(data) { self.emit('data', 'stderr: ' + String(data)); }); return command; }; Executor.prototype._runStep = function(step, callback) { var params = _(step).clone(); _(params).defaults( _(this.project).pick('shell', 'shellCmdArg', 'shellExtraArgs') ); if (params.type !== 'shell') { throw new Error('Unknown step type: ' + params.type); } // set command cwd to executor cwd params.cwd = this.cwd; var command = this._createCommand( _({ emitIn: true, emitOut: true, emitErr: true, attachStderr: true }).extend(params) ); this._listenCommand(command); return command.run(_(params).extend({envVars: this.envVars}), callback); }; Executor.prototype._getChanges = function(params, callback) { var self = this, scm, isFirstRun, oldRev; Steppy( function() { self._isCloned(this.slot()); }, function(err, cloned) { var scmParams = {type: params.type}; if (cloned) { scmParams.cwd = self.cwd; isFirstRun = false; } else { scmParams.repository = params.repository; isFirstRun = true; } scm = self._createScm(scmParams); scm.on('stdin', function(data) { self.emit('data', '> ' + String(data)); }); if (isFirstRun) { this.pass(null); } else { scm.getCurrent(this.slot()); } }, function(err, id) { oldRev = id; if (isFirstRun) { scm.clone(self.cwd, params.rev, this.slot()); } else { scm.pull(params.rev, this.slot()); } }, function() { scm.getChanges(oldRev && oldRev.id, params.rev, this.slot()); }, function(err, changes) { var target = self._getTarget(params.rev, changes); this.pass({ scm: scm, oldRev: oldRev, rev: target.rev, changes: target.changes }); }, callback ); }; // Does current project scm has new changes to build Executor.prototype.hasScmChanges = function(callback) { var self = this; // trigger all run hooks (before run, etc) coz get changes requires full // node specific environment Steppy( function() { self._beforeRun(this.slot()); }, function() { // check is repository cloned to prevent implicit clone by // get changes (which case empty changes for first build) self._isCloned(this.slot()); }, function(err, cloned) { if (cloned) { self._getChanges(self.project.scm, this.slot()); } else { // if repository is not cloned pass some fake change for // convert to true below this.pass({changes: [{}]}); } }, function(err, data) { if (err) { self._onRunError(err, callback); } else { self._afterRun(function(err) { callback(err, !err && data.changes.length > 0); }); } } ); }; Executor.prototype._beforeRun = function(callback) { callback(); }; Executor.prototype._afterRun = function(callback) { callback(); }; Executor.prototype._onRunError = function(originalErr, callback) { callback(originalErr); }; Executor.prototype.run = function(callback) { var self = this, project = self.project, beforeRunTiming = {name: 'prepare executor'}, getSourcesTiming = {name: 'get sources'}, afterRunTiming = {name: 'cleanup executor'}, stepTimings = []; Steppy( function() { self.emit('currentStep', beforeRunTiming.name); var beforeRunStart = Date.now(); this.pass(beforeRunStart); self._beforeRun(this.slot()); }, function(err, beforeRunStart) { beforeRunTiming.duration = Date.now() - beforeRunStart; stepTimings.push(beforeRunTiming); self.emit('currentStep', getSourcesTiming.name); var getSourcesStart = Date.now(); this.pass(getSourcesStart); self._getSources(project.scm, this.slot()); }, function(err, getSourcesStart, scmData) { getSourcesTiming.duration = Date.now() - getSourcesStart; stepTimings.push(getSourcesTiming); self.emit('scmData', scmData); var funcs = project.steps.map(function(step) { return function() { var start = Date.now(), stepCallback = this.slot(); self.emit('currentStep', step.name); var timing = {name: step.name}; self.currentCmd = self._runStep(step, function(err) { timing.duration = Date.now() - start; stepTimings.push(timing); self.emit('stepTimingsChange', stepTimings); stepCallback(err); }); }; }); var stepCallback = this.slot(); funcs.push(function(err) { if (err) { err.projectStepError = true; } stepCallback(err); }); Steppy.apply(this, funcs); }, function() { self.emit('currentStep', afterRunTiming.name); var afterRunStart = Date.now(); this.pass(afterRunStart); self._afterRun(this.slot()); }, function(err, afterRunStart) { if (err) { self._onRunError(err, callback); } else { afterRunTiming.duration = Date.now() - afterRunStart; stepTimings.push(afterRunTiming); self.emit('stepTimingsChange', stepTimings); callback(); } } ); }; Executor.prototype.cancel = function(callback) { var self = this; Steppy( function() { if (!self.currentCmd) { throw new Error('Can`t cancel build: no current cmd'); } if (!self.currentCmd.pid) { throw new Error('Can`t cancel build: current cmd doesn`t have pid'); } // TODO: check that program is running before kill killTree(self.currentCmd.pid, 'SIGKILL', this.slot()); }, function() { self.canceled = true; this.pass(null); }, callback ); }; // Returns target rev and filtered changes according to `catchRev` Executor.prototype._getTarget = function(rev, changes) { var result = {rev: rev, changes: changes}, catchRev = this.project.catchRev, targetRev = this.project.scm.rev; var onRevMatched = false; if (catchRev) { if (catchRev.onRev) { if (_(catchRev.onRev).isRegExp()) { onRevMatched = catchRev.onRev.test(targetRev); } else { onRevMatched = (catchRev.onRev === targetRev); } } else { onRevMatched = true; } } if (onRevMatched) { // reverse before search changes = changes.reverse(); var index = -1; var comment = catchRev.comment; if (comment) { index = _(changes).findIndex(function(change) { if (_(comment).isRegExp()) { return comment.test(change.comment); } else { return comment === change.comment; } }); } var tag = catchRev.tag; if (tag) { index = _(changes).findIndex(function(change) { if (change.tags) { if (_(tag).isRegExp()) { return _(change.tags).find(function(changeTag) { return tag.test(changeTag); }); } else { return _(change.tags).contains(tag); } } }); } if (index !== -1) { result.rev = changes[index].id; result.changes = changes.slice(0, index + 1); result.changes.reverse(); } // reverse back before return changes = changes.reverse(); } return result; };