Skip to content

Commit 31cecfa

Browse files
unknownunknown
authored andcommitted
Initial commit
0 parents  commit 31cecfa

File tree

3 files changed

+379
-0
lines changed

3 files changed

+379
-0
lines changed

checkout.BAT

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
::===============================================================
2+
:: Checkout Rigbox to provided commit
3+
::
4+
:: 2019-06 MW created
5+
::===============================================================
6+
@ECHO OFF
7+
:: Check parameters
8+
IF %1.==. (GOTO Err1)
9+
IF %2.==. (GOTO Err2)
10+
11+
PUSHD %2
12+
git fetch -a
13+
git reset --hard HEAD
14+
git checkout %1
15+
git submodule update --init --recursive
16+
git submodule foreach git reset --hard HEAD
17+
:: git pull --recurse-submodules --strategy-option=theirs
18+
:: ECHO Checked out %1
19+
:: git status
20+
POPD
21+
EXIT /B %ERRORLEVEL%
22+
23+
:Err1
24+
ECHO No SHA param defined 1>&2
25+
EXIT /B 1
26+
27+
:Err2
28+
ECHO No PATH param defined 1>&2
29+
EXIT /B 1

index.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/**
2+
* @author Miles Wells <[email protected]>
3+
* @requires ./queue.js
4+
* @requires module:dotenv
5+
* @requires module:"@octokit/app"
6+
* @requires module:"@octokit/request"
7+
* @requires module:express
8+
* @requires module:github-webhook-handler
9+
* @requires module:smee-client
10+
*/
11+
const fs = require('fs');
12+
const express = require('express')
13+
const srv = express();
14+
const cp = require('child_process');
15+
const queue = new (require('./queue.js'))()
16+
const { App } = require('@octokit/app');
17+
const { request } = require("@octokit/request");
18+
19+
const id = process.env.GITHUB_APP_IDENTIFIER;
20+
const secret = process.env.GITHUB_WEBHOOK_SECRET;
21+
22+
// Create new tunnel to receive hooks
23+
const SmeeClient = require('smee-client')
24+
const smee = new SmeeClient({
25+
source: process.env.WEBHOOK_PROXY_URL,
26+
target: 'http://localhost:3000/github',
27+
logger: console
28+
})
29+
30+
// Create handler to verify posts signed with webhook secret. Content type must be application/json
31+
var createHandler = require('github-webhook-handler');
32+
var handler = createHandler({ path: '/github', secret: process.env.GITHUB_WEBHOOK_SECRET });
33+
var installationAccessToken;
34+
35+
const app = new App({
36+
id: process.env.GITHUB_APP_IDENTIFIER,
37+
privateKey: fs.readFileSync(process.env.GITHUB_PRIVATE_KEY),
38+
webhooks: {secret}
39+
});
40+
// Authenticate app by exchanging signed JWT for access token
41+
var token = app.getSignedJsonWebToken();
42+
43+
/**
44+
* Callback to deal with POST requests to /github endpoint
45+
* @param {Object} req - Request object.
46+
* @param {Object} res - Response object.
47+
* @param {Function} next - Handle to next callback in stack.
48+
*/
49+
srv.post('/github', async (req, res, next) => {
50+
console.log('Post received')
51+
try {
52+
token = await app.getSignedJsonWebToken();
53+
//getPayloadRequest(req) GET /orgs/:org/installation
54+
const { data } = await request("GET /repos/:owner/:repo/installation", {
55+
owner: "cortex-lab",
56+
repo: "Rigbox",
57+
headers: {
58+
authorization: `Bearer ${token}`,
59+
accept: "application/vnd.github.machine-man-preview+json"
60+
}
61+
});
62+
// contains the installation id necessary to authenticate as an installation
63+
const installationId = data.id;
64+
installationAccessToken = await app.getInstallationAccessToken({ installationId });
65+
handler(req, res, () => res.end('ok'))
66+
next();
67+
} catch (error) {
68+
next(error);
69+
}
70+
});
71+
72+
/**
73+
* Load MATLAB test results from db.json file.
74+
* @param {string} id - Function to call with job and done callback when.
75+
*/
76+
function loadTestRecords(id) {
77+
let obj = JSON.parse(fs.readFileSync('./db.json', 'utf8'));
78+
if (!Array.isArray(obj)) obj = [obj];
79+
let record;
80+
for (record of obj) {
81+
if (record['commit'] === id) {
82+
return record;
83+
}
84+
};
85+
};
86+
87+
/*
88+
// Serve the test results for requested commit id
89+
srv.get('/github/:id', function (req, res) {
90+
console.log(req.params.id)
91+
const record = loadTestRecords(req.params.id);
92+
res.send(record['results']);
93+
}); */
94+
95+
// Define how to process tests. Here we checkout git and call MATLAB
96+
queue.process(async (job, done) => {
97+
// job.data contains the custom data passed when the job was created
98+
// job.id contains id of this job.
99+
var sha = job.data['sha']; // Retrieve commit hash
100+
// If the repo is a submodule, modify path
101+
var path = process.env.RIGBOX_REPO_PATH;
102+
if (job.data['repo'] === 'alyx-matlab' || job.data['repo'] === 'signals') {
103+
path = path + '\\' + job.data['repo'];}
104+
if (job.data['repo'] === 'alyx') { sha = 'dev' }; // For Alyx checkout master
105+
// Checkout commit
106+
checkout = cp.execFile('checkout.bat ', [sha, path], (error, stdout, stderr) => {
107+
if (error) { // Send error status
108+
console.error('Checkout failed: ', stderr);
109+
job.data['status'] = 'error';
110+
job.data['context'] = 'Failed to checkout code: ' + stderr;
111+
done(error); // Propagate error
112+
return;
113+
}
114+
console.log(stdout)
115+
// Go ahead with MATLAB tests
116+
var runTests;
117+
const timer = setTimeout(function() {
118+
console.log('Max test time exceeded')
119+
job.data['status'] = 'error';
120+
job.data['context'] = 'Tests stalled after ~2 min';
121+
runTests.kill();
122+
done(new Error('Job stalled')) }, 5*60000);
123+
let args = ['-r', `runAllTests (""${job.data.sha}"",""${job.data.repo}"")`, '-wait', '-log', '-nosplash'];
124+
runTests = cp.execFile('matlab', args, (error, stdout, stderr) => {
125+
clearTimeout(timer);
126+
if (error) { // Send error status
127+
// Isolate error from log
128+
let errStr = stderr.split(/\r?\n/).filter((str) =>
129+
{return str.startsWith('Error in \'')}).join(';');
130+
job.data['status'] = 'error';
131+
job.data['context'] = errStr;
132+
done(error); // Propagate
133+
} else {
134+
const rec = loadTestRecords(job.data['sha']); // Load test result from json log
135+
job.data['status'] = rec['status'];
136+
job.data['context'] = rec['description'];
137+
done();
138+
}
139+
});
140+
});
141+
});
142+
143+
/**
144+
* Callback triggered when job finishes. Called both on complete and error.
145+
* @param {Object} job - Job object which has finished being processed.
146+
*/
147+
queue.on('finish', job => { // On job end post result to API
148+
console.log(`Job ${job.id} complete`)
149+
request("POST /repos/:owner/:repo/statuses/:sha", {
150+
owner: job.data['owner'],
151+
repo: job.data['repo'],
152+
headers: {
153+
authorization: `token ${installationAccessToken}`,
154+
accept: "application/vnd.github.machine-man-preview+json"},
155+
sha: job.data['sha'],
156+
state: job.data['status'],
157+
target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${job.data.sha}`, // FIXME replace url
158+
description: job.data['context'],
159+
context: 'continuous-integration/ZTEST'
160+
});
161+
});
162+
163+
// Let fail silently: we report error via status
164+
queue.on('error', err => {return;});
165+
// Log handler errors
166+
handler.on('error', function (err) {
167+
console.error('Error:', err.message)
168+
})
169+
170+
// Handle push events
171+
handler.on('push', async function (event) {
172+
// Log the event
173+
console.log('Received a push event for %s to %s',
174+
event.payload.repository.name,
175+
event.payload.ref)
176+
for (commit of event.payload.commits) { // For each commit pushed...
177+
try {
178+
// Post a 'pending' status while we do our tests
179+
await request('POST /repos/:owner/:repo/statuses/:sha', {
180+
owner: 'cortex-lab',
181+
repo: event.payload.repository.name,
182+
headers: {
183+
authorization: `token ${installationAccessToken}`,
184+
accept: 'application/vnd.github.machine-man-preview+json'
185+
},
186+
sha: commit['id'],
187+
state: 'pending',
188+
target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${commit.id}`, // fail
189+
description: 'Tests error',
190+
context: 'continuous-integration/ZTEST'
191+
});
192+
// Add a new test job to the queue
193+
queue.add({
194+
sha: commit['id'],
195+
owner: 'cortex-lab',
196+
repo: event.payload.repository.name,
197+
status: '',
198+
context: ''
199+
});
200+
} catch (error) {console.log(error)}
201+
};
202+
});
203+
204+
// Start the server in the port 3000
205+
var server = srv.listen(3000, function () {
206+
var host = server.address().address
207+
var port = server.address().port
208+
209+
console.log("Handler listening at http://%s:%s", host, port)
210+
});
211+
// Start tunnel
212+
const events = smee.start()
213+
214+
// Log any unhandled errors
215+
process.on('unhandledRejection', (reason, p) => {
216+
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
217+
console.log(reason.stack)
218+
});

queue.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
var EventEmitter = require('events').EventEmitter
2+
3+
/**
4+
* Queue module allows one to add tasks to a queue which are processed sequentially as FILO.
5+
* @module ./queue
6+
* @example
7+
* // create queue
8+
* const queue = new require(./queue.js).Queue()
9+
* queue.process((job, done) => {
10+
* console.log('Job with id ' + job.id + ' is being processed');
11+
* setTimeout(done, 3000);
12+
* });
13+
* var data = {key: 'value'};
14+
* queue.add(data);
15+
* @version 0.9.0
16+
* @author Miles Wells [<[email protected]>]
17+
* @license Apache-2.0
18+
*/
19+
20+
/** Class representing a Queue API. */
21+
class Queue extends EventEmitter {
22+
23+
/**
24+
* Create queue to add jobs to.
25+
* @param {string} path - Path to saved queue object (TODO).
26+
* @param {Array} pile - Array of queued job objects.
27+
* @param (Function) _process - Handle to job process function.
28+
* @event module:Queue~finish
29+
* @event module:Queue~error
30+
* @event module:Queue~complete
31+
* @listens module:Queue~event:finish
32+
* @see {@link Job}
33+
*/
34+
constructor(timeout, path) {
35+
super();
36+
// Initialize properties
37+
this.pile = [];
38+
this.path = typeof path == 'undefined' ? './queue.json' : path; //TODO Implement
39+
this.on('finish', function () { // Each time a job finishes...
40+
this.pile.shift(); // take off pile
41+
this.next()}); // start next job
42+
}
43+
44+
/**
45+
* Create new job and add to queue.
46+
* @param {Object} data - Data object to be stored in {@link Job}.
47+
*/
48+
add(data) {
49+
var id = this.pile.length + 1; // generate job id
50+
this.pile.push(new Job(id, data)); // add to bottom of pile
51+
console.log('Job added (' + this.pile.length + ' on pile)')
52+
this.next(); // Start next job if idle
53+
}
54+
55+
/**
56+
* Process next job if any are on pile.
57+
*/
58+
next() {
59+
if (this.pile.length > 0 && this.pile[0].running === false) {
60+
console.log('Starting next job')
61+
this._process(this.pile[0])
62+
}
63+
}
64+
65+
/**
66+
* Create callback to be triggered when process function completes.
67+
* @param {Object} job - {@link Job} object.
68+
* @todo Change 'incomplete' => 'error'
69+
* @returns {function} 'done' callback to be called by process function
70+
*/
71+
createDoneCallback(job) {
72+
const obj = this;
73+
return function( err ) {
74+
job.isRunning = false; // set false (will emit 'end')
75+
if( err ) { obj.emit('error', err, job); }
76+
else {obj.emit('complete', job)}
77+
obj.emit('finish', job);
78+
}
79+
80+
}
81+
82+
/**
83+
* Create callback to be triggered when process function completes.
84+
* @param {Function} func - Function to call with job and done callback when.
85+
*/
86+
process(func) {
87+
this._process = async (job) => {
88+
var done = this.createDoneCallback(job);
89+
job.isRunning = true;
90+
setImmediate(func, job, done);
91+
console.log('Job running')
92+
};
93+
}
94+
};
95+
96+
/** Class representing a job in the Queue. */
97+
class Job extends EventEmitter {
98+
99+
/**
100+
* Create a job object with associated data.
101+
* @param {number} id - Job ID (unique in current Queue pile).
102+
* @param {Object} data - Data to hold in object, may be used by Queue process function.
103+
* @param {boolean} running - Indicates whether job is currently being processed.
104+
* @event module:Job~end
105+
*/
106+
constructor(id, data) {
107+
super();
108+
//console.log('Job ' + id + ' constructor called')
109+
// Initialize properties
110+
this.id = id;
111+
this.data = data;
112+
this.running = false;
113+
}
114+
115+
/**
116+
* Set running attribute. If setting to false from true, emit 'end' event.
117+
* @param {boolean} bool - Value to set running.
118+
*/
119+
set isRunning(bool) {
120+
if (bool == false && this.running === true) {
121+
this.running = false;
122+
this.emit('end');
123+
} else {
124+
if (bool == true) {
125+
this.running = true;
126+
}
127+
}
128+
}
129+
130+
};
131+
132+
module.exports = Queue; // Export Queue

0 commit comments

Comments
 (0)