Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #10 from steves/phpcs

Add phpcs results processing
  • Loading branch information...
commit 05e67829c944730db2c2c2cefdbec7f754d7df98 2 parents 7ca5caa + 0b91b94
@steves steves authored
View
6 CHANGELOG.md
@@ -1,3 +1,9 @@
+v0.1.2
+=====
+* Add support for parsing PHP Code Sniffer results
+* Re-do which parameters are passed to event listeners
+* Merge the `jobs` and `pulls` MongoDB collections into a single collection
+
v0.1.1
=====
* Added support for GitHub Commit Statuses API
View
13 README.md
@@ -40,7 +40,8 @@ Mergeatron comes with multiple different plugins you can opt to use. By default
* `github.user` - The user whose GitHub repo Mergeatron will be checking for Pull Requests. Does not need to be the same as the `github.auth.user` user.
* `github.repo` - The repo you want Mergeatron to keep an eye on.
* `github.frequency` - The frequency, in milliseconds, with which to poll GitHub for new and updated Pull Requests. Be mindful of your [API rate limit](http://developer.github.com/v3/#rate-limiting) when setting this.
-
+ * `phpcs.artifact` - The name of the artifact file that contains PHP Code Sniffer results. If no artifact with this name is found the plugin won't do anything.
+
## Configuring Jenkins
To configure Jenkins you will need to make sure you have the appropriate git plugin installed. I'm assuming you already know how to do that and already have it up and running successfully. Once you do follow the below steps.
@@ -94,9 +95,11 @@ git remote prune origin
## Events
- * ''build.validate'' - This is the first event emitted in a builds life cycle. It allows any listening plugins to check the build to make sure it should be handled.
- * ''build.process'' - If a build should be acted upon this event will be emitted. It allows any listening plugins to setup the build for processing. This means persisting it to a temporary, or permenant, data store of their choice and doing any other setup work they need to.
- * ''build.triggered'' - Once a build has been pre-processed it is ready to be built. When that happens this event is emitted. Any listening plugins can start the build.
+ * ''pull.found'' - This is the first event emitted in a builds life cycle. It allows any listening plugins to check the build to make sure it should be handled.
+ * ''pull.validated'' - If a build should be acted upon this event will be emitted. It allows any listening plugins to setup the build for processing. This means persisting it to a temporary, or permenant, data store of their choice and doing any other setup work they need to.
+ * ''pull.processed'' - Once a build has been pre-processed it is ready to be built. When that happens this event is emitted. Any listening plugins can start the build.
* ''build.started'' - This event is emitted when the build has been started.
* ''build.succeeded'' - This event is emitted when a build was successful.
- * ''build.failed'' - This event is emitted when a build has failed.
+ * ''build.failed'' - This event is emitted when a build has failed.
+ * ''pull.inline_status'' - This event is emitted when a plugin is announcing that something was found on a specific line of a files diff within the build.
+ * ''build.artifact_found'' - This event is emitted once for each artifact found after the build has finished. Plugins receive the URL to the artifact and can download and act upon it if wanted.
View
3  config.sample.js
@@ -19,6 +19,9 @@ exports.config = {
project: 'project_name',
rules: [ new RegExp(/.php/g) ],
frequency: 2000
+ },
+ phpcs: {
+ artifact: 'artifacts/phpcs.csv'
}
}
};
View
4 mergeatron.js
@@ -3,10 +3,6 @@ var config = require('./config').config,
fs = require('fs'),
events = require('events');
-Array.prototype.randomValue = function() {
- return this[Math.floor(Math.random() * this.length)];
-};
-
var Mergeatron = function(mongo) {
this.mongo = mongo;
};
View
4 package.json
@@ -1,6 +1,6 @@
{
"name": "Mergeatron",
- "version": "0.1.1",
+ "version": "0.1.2",
"author": "Steven Surowiec<steven.surowiec@gmail.com>",
"description": "A helpful PR monitor that runs jenkins builds when Pull Requests are created and updated",
@@ -24,4 +24,4 @@
},
"main": "./mergeatron.js"
-}
+}
View
86 plugins/github.js
@@ -41,27 +41,27 @@ exports.init = function(config, mergeatron) {
}
});
- mergeatron.on('build.process', function(pull) {
+ mergeatron.on('pull.validated', function(pull) {
processPull(pull);
});
- mergeatron.on('build.started', function(job_id, job, build_url) {
+ mergeatron.on('build.started', function(job, pull, build_url) {
createStatus(job['head'], 'pending', build_url, 'Testing Pull Request');
});
- mergeatron.on('build.failed', function(job_id, job, build_url) {
+ mergeatron.on('build.failed', function(job, pull, build_url) {
createStatus(job['head'], 'failure', build_url, 'Build failed');
});
- mergeatron.on('build.succeeded', function(job_id, job, build_url) {
+ mergeatron.on('build.succeeded', function(job, pull, build_url) {
createStatus(job['head'], 'success', build_url, 'Build succeeded');
});
- mergeatron.on('line.violation', function(job_id, pull_number, sha, file, position) {
+ mergeatron.on('pull.inline_status', function(pull, sha, file, position, comment) {
GitHub.pullRequests.createComment({
user: config.user,
repo: config.repo,
- number: pull_number,
+ number: pull.number,
body: comment,
commit_id: sha,
path: file,
@@ -76,18 +76,56 @@ exports.init = function(config, mergeatron) {
return;
}
- var file_names = [];
- for (var x in files) {
- var file_name = files[x].filename;
- if (!file_name || file_name == 'undefined') {
- continue;
+ pull.files = [];
+ files.forEach(function(file) {
+ if (!file.filename || file.filename == 'undefined') {
+ return;
}
- file_names.push(file_name);
- }
+ var start = null,
+ length = null,
+ deletions = [],
+ modified_length,
+ offset = 0.
+ line_number = 0;
+
+ file.ranges = [];
+ file.reported = [];
+ file.sha = file.blob_url.match(/blob\/([^\/]+)/)[1];
+ file.patch.split('\n').forEach(function(line) {
+ var matches = line.match(/^@@ -\d+,\d+ \+(\d+),(\d+) @@/);
+ if (matches) {
+ if (start == null && length == null) {
+ start = parseInt(matches[1]);
+ length = parseInt(matches[2]);
+ line_number = start;
+ } else {
+ // The one is for the line in the diff block containing the line numbers
+ modified_length = 1 + length + deletions.length;
+ file.ranges.push([ start, start + length, modified_length, offset, deletions ]);
+
+ deletions = [];
+ start = parseInt(matches[1]);
+ length = parseInt(matches[2]);
+ offset += modified_length;
+ line_number = start;
+ }
+ } else if (line.indexOf('-') === 0) {
+ deletions.push(line_number);
+ } else {
+ line_number += 1;
+ }
+ });
+
+ if (start != null && length != null) {
+ file.ranges.push([ start, start + length, 1 + length + deletions.length, offset, deletions ]);
+ }
+
+ pull.files.push(file);
+ });
- if (file_names.length > 0) {
- mergeatron.emit('build.validate', pull, file_names);
+ if (pull.files.length > 0) {
+ mergeatron.emit('pull.found', pull);
}
});
}
@@ -100,23 +138,35 @@ exports.init = function(config, mergeatron) {
if (!item) {
new_pull = true;
- mergeatron.mongo.pulls.insert({ _id: pull.number, created_at: pull.created_at, updated_at: pull.updated_at, head: pull.head.sha }, function(err) {
+ mergeatron.mongo.pulls.insert({ _id: pull.number, number: pull.number, created_at: pull.created_at, updated_at: pull.updated_at, head: pull.head.sha, files: pull.files }, function(err) {
if (err) {
console.log(err);
process.exit(1);
}
});
+ pull.jobs = [];
+ } else {
+ // Before updating the list of files in mongo we need to make sure the set of reported lines is saved
+ item.files.forEach(function(file) {
+ pull.files.forEach(function(pull_file, i) {
+ if (pull_file.filename == file.filename) {
+ pull.files[i].reported = file.reported;
+ }
+ });
+ });
+ mergeatron.mongo.pulls.update({ _id: pull.number }, { $set: { files: pull.files } });
+ pull.jobs = item.jobs;
}
if (new_pull || pull.head.sha != item.head) {
- mergeatron.emit('build.triggered', pull.number, pull.head.sha, ssh_url, branch, pull.updated_at);
+ mergeatron.emit('pull.processed', pull, pull.number, pull.head.sha, ssh_url, branch, pull.updated_at);
return;
}
GitHub.issues.getComments({ user: config.user, repo: config.repo, number: pull.number, per_page: 100 }, function(error, resp) {
for (i in resp) {
if (resp[i].created_at > item.updated_at && resp[i].body.indexOf('@' + config.auth.user + ' retest') != -1) {
- mergeatron.emit('build.triggered', pull.number, pull.head.sha, ssh_url, branch, pull.updated_at, resp[i].user.login);
+ mergeatron.emit('pull.processed', pull, pull.number, pull.head.sha, ssh_url, branch, pull.updated_at, resp[i].user.login);
return;
}
}
View
165 plugins/jenkins.js
@@ -7,13 +7,14 @@ exports.init = function(config, mergeatron) {
async.parallel({
'jenkins': function() {
var run_jenkins = function() {
- mergeatron.mongo.jobs.find({ status: { $ne: 'finished' } }).forEach(function(err, item) {
+ mergeatron.mongo.pulls.find({ 'jobs.status': { $in: ['new', 'started'] }}).forEach(function(err, pull) {
if (err) {
+ console.log(err);
process.exit(1);
}
- if (!item) { return; }
- checkJob(item['_id']);
+ if (!pull) { return; }
+ checkJob(pull);
});
setTimeout(run_jenkins, config.frequency);
@@ -23,48 +24,51 @@ exports.init = function(config, mergeatron) {
}
});
- mergeatron.on('build.triggered', function(pull_number, sha, ssh_url, branch, updated_at, triggered_by) {
- buildPull(pull_number, sha, ssh_url, branch, updated_at);
+ mergeatron.on('pull.processed', function(pull, pull_number, sha, ssh_url, branch, updated_at, triggered_by) {
+ buildPull(pull, pull_number, sha, ssh_url, branch, updated_at);
});
- mergeatron.on('build.validate', function(pull, files) {
+ mergeatron.on('pull.found', function(pull) {
if (!config.rules) {
- mergeatron.emit('build.process', pull);
+ mergeatron.emit('pull.validated', pull);
return;
}
- for (var x in files) {
- if (!files[x] || typeof files[x] != 'string') {
+ for (var x in pull.files) {
+ if (!pull.files[x].filename || typeof pull.files[x].filename != 'string') {
continue;
}
for (var y in config.rules) {
- if (files[x].match(config.rules[y])) {
- mergeatron.emit('build.process', pull);
+ if (pull.files[x].filename.match(config.rules[y])) {
+ mergeatron.emit('pull.validated', pull);
return;
}
}
}
});
- function buildPull(number, sha, ssh_url, branch, updated_at) {
+ /**
+ * @todo Do we need to pass all these parameters, is just passing pull enough?
+ */
+ function buildPull(pull, number, sha, ssh_url, branch, updated_at) {
var job_id = uuid.v1(),
options = {
- url: url.format({
- protocol: config.protocol,
- host: config.host,
- pathname: '/job/' + config.project + '/buildWithParameters',
- query: {
- token: config.token,
- cause: 'Testing Pull Request: ' + number,
- REPOSITORY_URL: ssh_url,
- BRANCH_NAME: branch,
- JOB: job_id,
- PULL: number
- }
- }),
- method: 'GET',
- };
+ url: url.format({
+ protocol: config.protocol,
+ host: config.host,
+ pathname: '/job/' + config.project + '/buildWithParameters',
+ query: {
+ token: config.token,
+ cause: 'Testing Pull Request: ' + number,
+ REPOSITORY_URL: ssh_url,
+ BRANCH_NAME: branch,
+ JOB: job_id,
+ PULL: number
+ }
+ }),
+ method: 'GET',
+ };
request(options, function(error, response, body) {
if (error) {
@@ -72,25 +76,33 @@ exports.init = function(config, mergeatron) {
return;
}
- mergeatron.mongo.pulls.update({ _id: number }, { $set: { head: sha, updated_at: updated_at } });
- mergeatron.mongo.jobs.insert({ _id: job_id, pull: number, status: 'new', head: sha });
+ if (typeof pull.jobs == 'undefined') {
+ pull.jobs = [];
+ }
+
+ pull.jobs.push({
+ id: job_id,
+ status: 'new',
+ head: sha
+ });
- checkJob(job_id);
+ mergeatron.mongo.pulls.update({ _id: number }, { $set: { head: sha, updated_at: updated_at, jobs: pull.jobs } });
});
}
- function checkJob(job_id) {
- var options = {
- url: url.format({
- protocol: config.protocol,
- host: config.host,
- pathname: '/job/' + config.project + '/api/json',
- query: {
- tree: 'builds[number,url,actions[parameters[name,value]],building,result]'
- },
- }),
- json: true
- };
+ function checkJob(pull) {
+ var job = findUnfinishedJob(pull),
+ options = {
+ url: url.format({
+ protocol: config.protocol,
+ host: config.host,
+ pathname: '/job/' + config.project + '/api/json',
+ query: {
+ tree: 'builds[number,url,actions[parameters[name,value]],building,result]'
+ },
+ }),
+ json: true
+ };
request(options, function(error, response) {
response.body.builds.forEach(function(build) {
@@ -99,26 +111,63 @@ exports.init = function(config, mergeatron) {
}
build.actions[0].parameters.forEach(function(param) {
- if (param['name'] == 'JOB' && param['value'] == job_id) {
- mergeatron.mongo.jobs.findOne({ _id: job_id }, function(error, job) {
- if (job['status'] == 'new') {
- mergeatron.mongo.jobs.update({ _id: job_id }, { $set: { status: 'started' } });
- mergeatron.emit('build.started', job_id, job, build['url']);
+ if (param['name'] == 'JOB' && param['value'] == job.id) {
+ if (job.status == 'new') {
+ mergeatron.mongo.pulls.update({ 'jobs.id': job.id }, { $set: { 'jobs.$.status': 'started' } });
+ mergeatron.emit('build.started', job, pull, build['url']);
+ }
+
+ if (job.status != 'finished') {
+ if (build['result'] == 'FAILURE') {
+ mergeatron.mongo.pulls.update({ 'jobs.id': job.id }, { $set: { 'jobs.$.status': 'finished' } });
+ mergeatron.emit('build.failed', job, pull, build['url'] + 'console');
+
+ processArtifacts(build, pull);
+ } else if (build['result'] == 'SUCCESS') {
+ mergeatron.mongo.pulls.update({ 'jobs.id': job.id }, { $set: { 'jobs.$.status': 'finished' } });
+ mergeatron.emit('build.succeeded', job, pull, build['url']);
+
+ processArtifacts(build, pull);
}
-
- if (job['status'] != 'finished') {
- if (build['result'] == 'FAILURE') {
- mergeatron.mongo.jobs.update({ _id: job_id }, { $set: { status: 'finished' } });
- mergeatron.emit('build.failed', job_id, job, build['url'] + 'console');
- } else if (build['result'] == 'SUCCESS') {
- mergeatron.mongo.jobs.update({ _id: job_id }, { $set: { status: 'finished' } });
- mergeatron.emit('build.succeeded', job_id, job, build['url']);
- }
- }
- });
+ }
}
});
});
});
}
+
+ function processArtifacts(build, pull) {
+ var options = {
+ url: url.format({
+ protocol: config.protocol,
+ host: config.host,
+ pathname: '/job/' + config.project + '/' + build['number'] + '/api/json',
+ query: {
+ tree: 'artifacts[fileName,relativePath]'
+ },
+ }),
+ json: true
+ };
+
+ request(options, function(err, response) {
+ if (err) {
+ console.log(err);
+ return;
+ }
+
+ var artifacts = response.body.artifacts;
+ for (var i in artifacts) {
+ artifacts[i]['url'] = build['url'] + 'artifact/' + artifacts[i]['relativePath'];
+ mergeatron.emit('build.artifact_found', build, pull, artifacts[i]);
+ }
+ });
+ }
+
+ function findUnfinishedJob(pull) {
+ for (var x in pull.jobs) {
+ if (pull.jobs[x].status != 'finished') {
+ return pull.jobs[x];
+ }
+ }
+ }
};
View
90 plugins/phpcs.js
@@ -0,0 +1,90 @@
+var request = require('request');
+
+exports.init = function(config, mergeatron) {
+ mergeatron.on('build.artifact_found', function (build, pull, artifact) {
+ if (artifact['relativePath'] == config.artifact) {
+ process(build, pull, artifact['url']);
+ }
+ });
+
+ function process(build, pull, artifact_url) {
+ request({ url: artifact_url }, function(err, response) {
+ if (err) {
+ console.log(err);
+ return;
+ }
+
+ var violations = parseCsvFile(response.body);
+ pull.files.forEach(function(file, i) {
+ if (file.status != 'modified' && file.status != 'added') {
+ return;
+ }
+
+ violations.forEach(function(violation) {
+ if (violation.file.indexOf(file.filename) == -1) {
+ return;
+ }
+
+ var line_number = parseInt(violation.line);
+ if (file.reported.indexOf(line_number) != -1) {
+ return;
+ }
+
+ file.ranges.forEach(function(range) {
+ if (line_number >= range[0] && line_number <= range[1]) {
+ var diff_offset = range[3] + (line_number - range[0]) + 1;
+ if (range[4] && range[4].length > 0) {
+ range[4].forEach(function(deletion) {
+ if (deletion <= line_number) {
+ diff_offset += 1;
+ }
+ });
+ }
+
+ file.reported.push(line_number);
+ mergeatron.emit('pull.inline_status', pull, file.sha, file.filename, diff_offset, violation.message);
+ mergeatron.mongo.pulls.update({ _id: pull.number, 'files.filename': file.filename }, { $push: { 'files.$.reported': line_number }});
+ }
+ });
+ });
+ });
+ });
+ }
+
+ function parseCsvFile(data, callback){
+ var iteration = 0,
+ header = [],
+ records = [],
+ pattern = /(?:^|,)("(?:[^"]+)*"|[^,]*)/g,
+ parts = data.split('\n');
+
+ for (var x in parts) {
+ var line = parts[x];
+
+ if (!line) {
+ continue;
+ }
+
+ if (iteration++ == 0) {
+ header = line.split(pattern);
+ } else {
+ records.push(buildRecord(line));
+ }
+ }
+
+ function buildRecord(str){
+ var record = {},
+ fields = str.split(pattern);
+
+ for (var y in fields) {
+ if (header[y] != '') {
+ record[header[y].toLowerCase()] = fields[y].replace(/"/g, '');
+ }
+ }
+
+ return record;
+ }
+
+ return records;
+ }
+};
Please sign in to comment.
Something went wrong with that request. Please try again.