11/**
22 * @author Miles Wells <[email protected] > 33 * @requires ./queue.js
4+ * @requires ./coverage.js
45 * @requires module:dotenv
56 * @requires module:"@octokit/app"
67 * @requires module:"@octokit/request"
78 * @requires module:express
89 * @requires module:github-webhook-handler
9- * @requires module:smee-client
1010 */
1111const fs = require ( 'fs' ) ;
1212const express = require ( 'express' )
1313const srv = express ( ) ;
1414const cp = require ( 'child_process' ) ;
1515const queue = new ( require ( './queue.js' ) ) ( )
16+ const Coverage = require ( './coverage' ) ;
1617const { App } = require ( '@octokit/app' ) ;
1718const { request } = require ( "@octokit/request" ) ;
1819
1920const id = process . env . GITHUB_APP_IDENTIFIER ;
2021const secret = process . env . GITHUB_WEBHOOK_SECRET ;
2122
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- } )
23+ // Configure ssh tunnel
24+ const cmd = 'ssh -tt -R 80:localhost:3000 serveo.net' ;
25+ const sh = String ( cp . execFileSync ( 'where' , [ 'git' ] ) ) . replace ( 'cmd\\git.exe' , 'bin\\sh.exe' ) ;
26+ const tunnel = ( ) => {
27+ let ssh = cp . spawn ( sh , [ '-c' , cmd ] )
28+ ssh . stdout . on ( 'data' , ( data ) => { console . log ( `stdout: ${ data } ` ) ; } ) ;
29+ ssh . on ( 'exit' , ( ) => { console . log ( 'Reconnecting to Serveo' ) ; tunnel ( ) ; } ) ;
30+ }
2931
3032// Create handler to verify posts signed with webhook secret. Content type must be application/json
3133var createHandler = require ( 'github-webhook-handler' ) ;
@@ -75,22 +77,63 @@ srv.post('/github', async (req, res, next) => {
7577 */
7678function loadTestRecords ( id ) {
7779 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- } ;
80+ if ( ! Array . isArray ( obj ) ) obj = [ obj ] ; // Ensure array
81+ return obj . find ( o => o . commit === id ) ;
8582} ;
8683
87- /*
8884// Serve the test results for requested commit id
8985srv . get ( '/github/:id' , function ( req , res ) {
9086 console . log ( req . params . id )
9187 const record = loadTestRecords ( req . params . id ) ;
9288 res . send ( record [ 'results' ] ) ;
93- }); */
89+ } ) ;
90+
91+ // Serve the coverage results
92+ srv . get ( '/coverage/:repo/:branch' , function ( req , res ) {
93+ // Find head commit of branch
94+ try {
95+ const result = await request ( 'GET /repos/:owner/:repo/git/refs/heads/:branch' , {
96+ owner : 'cortex-lab' , // @todo Generalize repo owner
97+ repo : req . params . repo ,
98+ branch : req . params . branch
99+ } ) ;
100+ if result . data . ref . endsWith ( '/' + req . params . branch ) {
101+ console . log ( 'Request for ' + req . params . branch + ' coverage' )
102+ let id = result . data . object . sha ;
103+ var report = { 'schemaVersion' : 1 , 'label' : 'coverage' } ;
104+ try { // Try to load coverage record
105+ record = await loadTestRecords ( id ) ;
106+ if ( typeof record == 'undefined' || record [ 'coverage' ] == [ ] ) { throw 404 } ; // Test not found for commit
107+ if ( record [ 'status' ] === 'error' ) { throw 500 } ; // Test found for commit but errored
108+ report [ 'message' ] = record [ 'coverage' ] + '%' ;
109+ report [ 'color' ] = ( record [ 'coverage' ] > 75 ? 'green' : 'red' ) ;
110+ } catch ( err ) { // No coverage value
111+ report [ 'message' ] = ( err === 404 ? 'pending' : 'unknown' ) ;
112+ report [ 'color' ] = 'orange' ;
113+ // Check test isn't already on the pile
114+ let onPile = false ;
115+ for ( let job of queue . pile ) { if job . id === id { onPile = true ; break ; } } ;
116+ if ! onPile { // Add test to queue
117+ queue . add ( {
118+ sha : id ,
119+ owner : 'cortex-lab' , // @todo Generalize repo owner
120+ repo : req . params . repo ,
121+ status : '' ,
122+ context : '' } ) ;
123+ }
124+ } finally { // Send report
125+ res . setHeader ( 'Content-Type' , 'application/json' ) ;
126+ console . log ( report )
127+ res . end ( JSON . stringify ( report ) ) ; }
128+ }
129+ } else { throw 404 } ; // Specified repo or branch not found
130+ catch ( error ) {
131+ let msg = ( error === 404 ? `${ req . params . repo } /${ req . params . branch } not found` : error ) ;
132+ console . error ( msg )
133+ res . statusCode = 401 ; // If not found, send 401 for security reasons
134+ res . send ( msg ) ;
135+ }
136+ } ) ;
94137
95138// Define how to process tests. Here we checkout git and call MATLAB
96139queue . process ( async ( job , done ) => {
@@ -114,7 +157,7 @@ queue.process(async (job, done) => {
114157 console . log ( stdout )
115158 // Go ahead with MATLAB tests
116159 var runTests ;
117- const timer = setTimeout ( function ( ) {
160+ const timer = setTimeout ( function ( ) {
118161 console . log ( 'Max test time exceeded' )
119162 job . data [ 'status' ] = 'error' ;
120163 job . data [ 'context' ] = 'Tests stalled after ~2 min' ;
@@ -161,6 +204,31 @@ queue.on('finish', job => { // On job end post result to API
161204 } ) ;
162205} ) ;
163206
207+ /**
208+ * Callback triggered when job completes. Called when all tests run to end.
209+ * @param {Object } job - Job object which has finished being processed.
210+ * @todo Save full coverage object for future inspection
211+ */
212+ queue . on ( 'complete' , job => { // On job end post result to API
213+ console . log ( 'Updating coverage for ' + job . id )
214+ Coverage ( './CoverageResults.xml' , job . data . repo , job . data . id , obj => {
215+ // Digest and save percentage coverage
216+ let misses = 0 , hits = 0 ;
217+ for ( let file of job . source_files ) {
218+ misses += file . coverage . filter ( x => x == 0 ) . length ;
219+ hits += file . coverage . filter ( x => x > 0 ) . length ;
220+ }
221+ // Load data and save
222+ let records = JSON . parse ( fs . readFileSync ( './db.json' , 'utf8' ) ) ;
223+ if ( ! Array . isArray ( records ) ) records = [ records ] ; // Ensure array
224+ for ( let o of records ) { if o . commit === job . data . id { o . coverage = hits / ( hits + misses ) } ; break ; } // Add percentage
225+ // Save object
226+ fs . writeFile ( './db.json' , JSON . stringify ( records ) , function ( err ) {
227+ if ( err ) { console . log ( err ) ; }
228+ } ) ;
229+ } ) ;
230+ } ) ;
231+
164232// Let fail silently: we report error via status
165233queue . on ( 'error' , err => { return ; } ) ;
166234// Log handler errors
@@ -193,7 +261,7 @@ handler.on('push', async function (event) {
193261 // Add a new test job to the queue
194262 queue . add ( {
195263 sha : commit [ 'id' ] ,
196- owner : 'cortex-lab' ,
264+ owner : 'cortex-lab' , // @todo Generalize repo owner field
197265 repo : event . payload . repository . name ,
198266 status : '' ,
199267 context : ''
@@ -210,7 +278,7 @@ var server = srv.listen(3000, function () {
210278 console . log ( "Handler listening at http://%s:%s" , host , port )
211279} ) ;
212280// Start tunnel
213- const events = smee . start ( )
281+ tunnel ( ) ;
214282
215283// Log any unhandled errors
216284process . on ( 'unhandledRejection' , ( reason , p ) => {
0 commit comments