2019-01-15 17:15:25 +00:00
// Checks API example
// See: https://developer.github.com/v3/checks/ to learn more
2019-01-16 18:45:59 +00:00
const pendingChecks = [ ]
2019-01-16 22:45:16 +00:00
const Humanize = require ( 'humanize-plus' )
2019-01-15 17:15:25 +00:00
module . exports = app => {
2019-01-16 18:45:59 +00:00
app . on ( [ 'check_suite.requested' ] , checkSuiteRequested )
app . on ( [ 'check_run.rerequested' ] , checkRunRerequested )
async function checkSuiteRequested ( context ) {
if ( context . isBot ) {
return
}
await checkSuiteAsync ( context , context . payload . check _suite )
}
async function checkRunRerequested ( context ) {
if ( context . isBot ) {
return
}
const { check _suite } = context . payload . check _run
await checkSuiteAsync ( context , check _suite )
}
2019-01-15 17:15:25 +00:00
2019-01-16 18:45:59 +00:00
async function checkSuiteAsync ( context , check _suite ) {
2019-01-15 17:15:25 +00:00
// Do stuff
2019-01-16 18:45:59 +00:00
try {
const { head _branch , head _sha } = check _suite
if ( pendingChecks [ head _sha ] ) {
// Already running, ignore
return
2019-01-15 17:15:25 +00:00
}
2019-01-16 18:45:59 +00:00
// Probot API note: context.repo() => {username: 'hiimbex', repo: 'testing-things'}
const check = context . repo ( {
name : 'packages-check-bot' ,
head _branch : head _branch ,
head _sha : head _sha ,
status : 'in_progress' ,
started _at : ( new Date ( ) ) . toISOString ( ) ,
output : {
title : 'package.json check' ,
summary : 'Checking any new/updated dependencies...'
}
} )
if ( context . payload . action === 'rerequested' ) {
pendingChecks [ head _sha ] = { ... check , check _run _id : context . payload . check _run . id }
queueCheckAsync ( context , check _suite )
return { statusCode : 200 }
} else {
pendingChecks [ head _sha ] = { ... check }
const createResponse = await context . github . checks . create ( check )
context . log . debug ( ` create checks status: ${ createResponse . status } ` )
pendingChecks [ head _sha ] = { ... check , check _run _id : createResponse . data . id }
queueCheckAsync ( context , check _suite )
return createResponse
}
} catch ( e ) {
context . log . error ( e )
// TODO: Check what is the right way to exit here, since it seems this causes the bot to not be responsive afterward
throw e
}
2019-01-15 17:15:25 +00:00
}
// For more information on building apps:
// https://probot.github.io/docs/
// To get your app running against GitHub, see:
// https://probot.github.io/docs/development/
}
2019-01-16 18:45:59 +00:00
async function queueCheckAsync ( context , check _suite ) {
try {
const { before , head _sha } = check _suite
const compareResponse = await context . github . repos . compareCommits ( context . repo ( {
base : before ,
head : head _sha
} ) )
context . log . debug ( ` compare commits status: ${ compareResponse . status } , ${ compareResponse . data . files . length } file(s) ` )
let check = pendingChecks [ head _sha ]
2019-01-16 22:45:16 +00:00
let checkedDepCount = 0
let packageJsonFilenames = [ ]
2019-01-16 18:45:59 +00:00
const packageFilenameRegex = /^package\.json(.orig)?$/g
check . output . annotations = undefined
for ( const file of compareResponse . data . files ) {
switch ( file . status ) {
case 'added' :
case 'modified' :
if ( packageFilenameRegex . test ( file . filename ) ) {
2019-01-16 22:45:16 +00:00
packageJsonFilenames . push ( file . filename )
checkedDepCount += await checkPackageFileAsync ( check , context , file , head _sha )
2019-01-16 18:45:59 +00:00
}
break
}
}
check . status = 'completed'
check . completed _at = ( new Date ( ) ) . toISOString ( )
if ( ! check . output . annotations ) {
check . conclusion = 'neutral'
check . output . summary = 'No changes to dependencies'
} else if ( check . output . annotations . length === 0 ) {
check . conclusion = 'success'
check . output . summary = 'All dependencies are good!'
} else {
check . conclusion = 'failure'
2019-01-16 22:45:16 +00:00
const warnings = check . output . annotations . filter ( a => a . annotation _level === 'warning' ) . length
const failures = check . output . annotations . filter ( a => a . annotation _level === 'failure' ) . length
const uniqueProblemDependencies = [ ... new Set ( check . output . annotations . map ( a => a . dependency ) ) ]
check . output . summary = ` Checked ${ checkedDepCount } ${ Humanize . pluralize ( checkedDepCount , 'dependency' , 'dependencies' ) } in ${ Humanize . oxford ( packageJsonFilenames . map ( f => ` \` ${ f } \` ` ) , 3 ) } .
$ { Humanize . boundedNumber ( failures , 10 ) } $ { Humanize . pluralize ( failures , 'failure' ) } , $ { Humanize . boundedNumber ( warnings , 10 ) } $ { Humanize . pluralize ( warnings , 'warning' ) } in $ { Humanize . oxford ( uniqueProblemDependencies . map ( f => ` \` ${ f } \` ` ) , 3 ) } need your attention ! `
2019-01-16 18:45:59 +00:00
}
2019-01-16 22:45:16 +00:00
// Remove helper data from annotation objects
const annotations = check . output . annotations
for ( const annotation of annotations ) {
delete annotation [ 'dependency' ]
}
for ( let annotationIndex = 0 ; annotationIndex < annotations . length ; annotationIndex += 50 ) {
const annotationsSlice = annotations . length > 50 ? annotations . slice ( annotationIndex , annotationIndex + 50 ) : annotations
check . output . annotations = annotationsSlice
const updateResponse = await context . github . checks . update ( {
owner : check . owner ,
repo : check . repo ,
check _run _id : check . check _run _id ,
name : check . name ,
//details_url: check.details_url,
external _id : check . external _id ,
started _at : check . started _at ,
status : check . status ,
conclusion : check . conclusion ,
completed _at : check . completed _at ,
output : check . output
} ) // TODO: Handle error
context . log . debug ( ` update checks status: ${ updateResponse . status } ` )
}
check . output . annotations = annotations
2019-01-16 18:45:59 +00:00
delete pendingChecks [ head _sha ]
} catch ( error ) {
context . log . error ( error )
throw error
}
}
async function checkPackageFileAsync ( check , context , file , head _sha ) {
const contentsResponse = await context . github . repos . getContents ( context . repo ( {
path : file . filename ,
ref : head _sha
} ) )
context . log . debug ( ` get contents response: ${ contentsResponse . status } ` )
if ( contentsResponse . status >= 300 ) {
throw new ` HTTP error ${ contentsResponse . status } ( ${ contentsResponse . statusText } ) fetching ${ file . filename } `
}
const contents = Buffer . from ( contentsResponse . data . content , 'base64' ) . toString ( 'utf8' )
const contentsJSON = JSON . parse ( contents )
2019-01-16 22:45:16 +00:00
let dependencyCount = 0
dependencyCount += checkDependencies ( contents , contentsJSON . dependencies , file , check )
dependencyCount += checkDependencies ( contents , contentsJSON . devDependencies , file , check )
dependencyCount += checkDependencies ( contents , contentsJSON . optionalDependencies , file , check )
2019-01-16 18:45:59 +00:00
2019-01-16 22:45:16 +00:00
return dependencyCount
2019-01-16 18:45:59 +00:00
}
function checkDependencies ( contents , dependencies , file , check ) {
if ( ! dependencies ) {
2019-01-16 22:45:16 +00:00
return 0
2019-01-16 18:45:59 +00:00
}
const urlRegex = /^(http:\/\/|https:\/\/|git\+http:\/\/|git\+https:\/\/|ssh:\/\/|git\+ssh:\/\/|github:)([a-zA-Z0-9_\-./]+)(#(.*))?$/gm
const requiredProtocol = 'git+https://'
2019-01-16 22:45:16 +00:00
let dependencyCount = 0
2019-01-16 18:45:59 +00:00
for ( const dependency in dependencies ) {
if ( dependencies . hasOwnProperty ( dependency ) ) {
2019-01-16 22:45:16 +00:00
++ dependencyCount
2019-01-16 18:45:59 +00:00
const url = dependencies [ dependency ]
const match = urlRegex . exec ( url )
if ( ! match ) {
continue
}
const protocol = match [ 1 ]
const address = match [ 2 ]
const tag = match . length > 4 ? match [ 4 ] : null
const { line } = findLineColumn ( contents , contents . indexOf ( url ) )
const optimalAddress = address . endsWith ( '.git' ) ? address : address . concat ( '.git' )
const optimalTag = isTag ( tag ) ? tag : '#<release-tag>'
const suggestedUrl = ` ${ requiredProtocol } ${ optimalAddress } ${ optimalTag } `
2019-01-16 22:45:16 +00:00
const annotationSource = {
check : check ,
dependency : dependency ,
file : file ,
line : line
}
2019-01-16 18:45:59 +00:00
if ( protocol !== requiredProtocol ) {
2019-01-16 22:45:16 +00:00
createAnnotation ( annotationSource , suggestedUrl , {
2019-01-16 18:45:59 +00:00
annotation _level : 'warning' ,
title : ` Found protocol ${ protocol } being used in dependency ` ,
2019-01-16 22:45:16 +00:00
message : ` Protocol should be ${ requiredProtocol } . `
2019-01-16 18:45:59 +00:00
} )
}
if ( protocol !== 'github:' && ! address . endsWith ( '.git' ) ) {
2019-01-16 22:45:16 +00:00
createAnnotation ( annotationSource , suggestedUrl , {
2019-01-16 18:45:59 +00:00
annotation _level : 'warning' ,
title : 'Address should end with .git for consistency.' ,
2019-01-16 22:45:16 +00:00
message : 'Android builds have been known to fail when dependency addresses don\'t end with .git.'
2019-01-16 18:45:59 +00:00
} )
}
if ( ! tag ) {
2019-01-16 22:45:16 +00:00
createAnnotation ( annotationSource , suggestedUrl , {
annotation _level : 'failure' ,
2019-01-16 18:45:59 +00:00
title : 'Dependency is not locked with a tag/release.' ,
2019-01-16 22:45:16 +00:00
message : ` ${ url } is not a deterministic dependency locator. \r \n If the branch advances, it will be impossible to rebuild the same output in the future. `
2019-01-16 18:45:59 +00:00
} )
} else if ( ! isTag ( tag ) ) {
2019-01-16 22:45:16 +00:00
createAnnotation ( annotationSource , suggestedUrl , {
annotation _level : 'failure' ,
2019-01-16 18:45:59 +00:00
title : 'Dependency is locked with a branch, instead of a tag/release.' ,
2019-01-16 22:45:16 +00:00
message : ` ${ url } is not a deterministic dependency locator. \r \n If the branch advances, it will be impossible to rebuild the same output in the future. `
2019-01-16 18:45:59 +00:00
} )
}
}
}
2019-01-16 22:45:16 +00:00
return dependencyCount
2019-01-16 18:45:59 +00:00
}
function findLineColumn ( contents , index ) {
const lines = contents . split ( '\n' )
const line = contents . substr ( 0 , index ) . split ( '\n' ) . length
const startOfLineIndex = ( ( ) => {
const x = lines . slice ( 0 )
x . splice ( line - 1 )
return x . join ( '\n' ) . length + ( x . length > 0 )
} ) ( )
const col = index - startOfLineIndex
return { line , col }
}
2019-01-16 22:45:16 +00:00
function createAnnotation ( annotationSource , suggestedUrl , annotation ) {
const { check , dependency , file , line } = annotationSource
2019-01-16 18:45:59 +00:00
if ( ! check . output . annotations ) {
check . output . annotations = [ ]
}
annotation . message = annotation . message . concat ( ` \r \n \r \n Suggested URL: ${ suggestedUrl } ` )
check . output . annotations . push ( {
... annotation ,
2019-01-16 22:45:16 +00:00
dependency : dependency ,
2019-01-16 18:45:59 +00:00
path : file . filename ,
start _line : line ,
end _line : line ,
raw _details : ` {suggestedUrl: ${ suggestedUrl } } `
} )
}
function isTag ( tag ) {
// TODO: We need to check the actual repo to see if it is a branch or a tag
return tag && tag !== 'master' && tag !== 'develop'
}