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+ } ) ;
0 commit comments