nci
Version:
Flexible, open source continuous integration server written in node.js
371 lines (316 loc) • 8.19 kB
JavaScript
;
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;
};