2018-02-07 16:59:55 +00:00
// Description:
// Script that listens for PRs moving into the 'TO TEST' column
// and triggers a Jenkins build.
//
// Dependencies:
// github: "^13.1.0"
// jenkins: "^0.20.1"
2019-01-31 15:07:54 +00:00
// probot-config: "^1.0.0"
2018-02-07 16:59:55 +00:00
//
// Author:
// PombeirP
2018-02-08 12:09:59 +00:00
const getConfig = require ( 'probot-config' )
2019-03-26 17:16:07 +00:00
const createScheduler = require ( 'probot-scheduler' )
2018-02-07 16:59:55 +00:00
const jenkins = require ( 'jenkins' ) ( { baseUrl : process . env . JENKINS _URL , crumbIssuer : true , promisify : true } )
const HashMap = require ( 'hashmap' )
2019-03-26 17:16:07 +00:00
const HashSet = require ( 'hashset' )
2018-02-07 16:59:55 +00:00
2018-02-08 12:09:59 +00:00
const defaultConfig = require ( '../lib/config' )
const gitHubHelpers = require ( '../lib/github-helpers' )
2018-02-13 14:51:00 +00:00
const botName = 'trigger-automation-test-build'
2018-02-07 16:59:55 +00:00
const pendingPullRequests = new HashMap ( )
2019-03-26 17:16:07 +00:00
const existingProjectsProcessed = new HashSet ( )
2018-02-07 16:59:55 +00:00
module . exports = ( robot ) => {
if ( ! process . env . JENKINS _URL ) {
2018-02-13 14:51:00 +00:00
robot . log . info ( ` ${ botName } - Jenkins is not configured, not loading script ` )
2018-02-07 16:59:55 +00:00
return
}
2019-03-26 16:47:46 +00:00
robot . log . info ( ` ${ botName } - Starting up ` )
2019-03-26 17:16:07 +00:00
const { stop } = createScheduler ( robot , { interval : 1 * 60 * 1000 , delay : false } )
robot . on ( 'schedule.repository' , context => processExistingProjectCards ( robot , context , stop ) )
2018-02-08 12:09:59 +00:00
setInterval ( checkPendingPullRequests , 5 * 1000 * 60 , robot )
registerForRelevantCardEvents ( robot )
2018-02-07 16:59:55 +00:00
}
2018-02-08 12:09:59 +00:00
function registerForRelevantCardEvents ( robot ) {
2019-03-26 17:16:07 +00:00
robot . on ( [ 'project_card.created' , 'project_card.moved' ] , context => processChangedProjectCard ( robot , context , undefined ) )
2018-02-07 16:59:55 +00:00
}
2019-03-26 17:16:07 +00:00
const last = ( a , index ) => {
return a [ a . length + index ]
}
async function processExistingProjectCards ( robot , context , stopScheduler ) {
stopScheduler ( context . payload . repository )
if ( existingProjectsProcessed . contains ( context . payload . repository . id ) ) {
return
}
existingProjectsProcessed . add ( context . payload . repository . id )
const { github } = context
const config = await getConfig ( context , 'github-bot.yml' , defaultConfig ( robot , '.github/github-bot.yml' ) )
const projectBoardConfig = config ? config [ 'project-board' ] : null
const automatedTestsConfig = config ? config [ 'automated-tests' ] : null
if ( ! projectBoardConfig || ! automatedTestsConfig ) {
return
}
const repoInfo = context . repo ( )
const projectBoardName = projectBoardConfig [ 'name' ]
const kickoffColumnName = automatedTestsConfig [ 'kickoff-column-name' ]
// Find 'Pipeline for QA' project
const project = await gitHubHelpers . getRepoProjectByName ( github , robot , repoInfo , projectBoardName , botName )
if ( ! project ) {
robot . log . trace ( ` ${ botName } - Project doesn't have the specified project board ` , repoInfo , projectBoardName )
return
}
const allColumns = await github . paginate (
github . projects . listColumns ( { project _id : project . id } ) ,
res => res . data
)
const kickoffColumn = allColumns . find ( c => c . name === kickoffColumnName )
if ( ! kickoffColumn ) {
robot . log . debug ( ` ${ botName } - Kickoff column not found in project ` , kickoffColumnName , allColumns , projectBoardName )
return
}
const columnCards = await github . paginate (
github . projects . listCards ( { column _id : kickoffColumn . id } ) ,
res => res . data
)
robot . log . debug ( ` ${ botName } - Fetched ${ columnCards . length } cards ` , columnCards )
for ( const { url } of columnCards ) {
try {
const cardId = last ( url . split ( '/' ) , - 1 )
const { data : card } = await github . projects . getCard ( { card _id : cardId } )
await processChangedProjectCard ( robot , context , { ... card , column _id : kickoffColumn . id } )
} catch ( err ) {
robot . log . error ( ` ${ botName } - Unhandled exception while processing card: ${ err } ` , url )
}
}
}
async function processChangedProjectCard ( robot , context , card ) {
2018-02-09 21:50:23 +00:00
const { github , payload } = context
const repo = payload . repository
2019-03-26 17:16:07 +00:00
const repoInfo = context . repo ( )
if ( ! repoInfo . repo ) {
robot . log . debug ( ` ${ botName } - Repository info is not present in payload, ignoring ` , context )
2018-02-09 21:50:23 +00:00
return
}
2019-03-26 17:16:07 +00:00
if ( ! card ) {
card = payload . project _card
}
2018-02-09 21:50:23 +00:00
2018-02-08 12:09:59 +00:00
const config = await getConfig ( context , 'github-bot.yml' , defaultConfig ( robot , '.github/github-bot.yml' ) )
const projectBoardConfig = config ? config [ 'project-board' ] : null
const automatedTestsConfig = config ? config [ 'automated-tests' ] : null
if ( ! projectBoardConfig || ! automatedTestsConfig ) {
return
}
2019-03-26 17:16:07 +00:00
robot . log . debug ( ` ${ botName } - Processing changed project card ` , card )
2019-03-26 16:47:46 +00:00
2019-03-26 17:16:07 +00:00
if ( card . note ) {
2018-02-13 14:51:00 +00:00
robot . log . trace ( ` ${ botName } - Card is a note, ignoring ` )
2018-02-07 16:59:55 +00:00
return
}
const projectBoardName = projectBoardConfig [ 'name' ]
2019-02-25 12:27:13 +00:00
const kickoffColumnName = automatedTestsConfig [ 'kickoff-column-name' ]
2018-02-07 16:59:55 +00:00
2018-02-08 12:09:59 +00:00
if ( repo . full _name !== automatedTestsConfig [ 'repo-full-name' ] ) {
2018-02-13 14:51:00 +00:00
robot . log . trace ( ` ${ botName } - Pull request project doesn't match watched repo, exiting ` , repo . full _name , automatedTestsConfig [ 'repo-full-name' ] )
2018-02-07 16:59:55 +00:00
return
}
2019-02-25 12:27:13 +00:00
let targetKickoffColumn
2018-02-07 16:59:55 +00:00
try {
2019-03-26 17:16:07 +00:00
const columnPayload = await github . projects . getColumn ( { column _id : card . column _id } )
2018-02-07 16:59:55 +00:00
2019-02-25 12:27:13 +00:00
if ( columnPayload . data . name !== kickoffColumnName ) {
robot . log . trace ( ` ${ botName } - Card column name doesn't match watched column name, exiting ` , columnPayload . data . name , kickoffColumnName )
2018-02-07 16:59:55 +00:00
return
}
2019-02-25 12:27:13 +00:00
targetKickoffColumn = columnPayload . data
2018-02-07 16:59:55 +00:00
} catch ( error ) {
2019-03-26 17:16:07 +00:00
robot . log . warn ( ` ${ botName } - Error while fetching project column ` , card . column _id , error )
2018-02-07 16:59:55 +00:00
return
}
try {
2019-02-25 12:27:13 +00:00
const projectId = last ( targetKickoffColumn . project _url . split ( '/' ) , - 1 )
2019-01-31 14:50:23 +00:00
const projectPayload = await github . projects . get ( { project _id : projectId } )
2018-02-16 20:35:49 +00:00
const project = projectPayload . data
2018-02-07 16:59:55 +00:00
if ( project . name !== projectBoardName ) {
2018-03-14 13:24:11 +00:00
robot . log . trace ( ` ${ botName } - Project board name doesn't match watched project board, exiting ` , project . name , projectBoardName )
2018-02-07 16:59:55 +00:00
return
}
} catch ( error ) {
2019-03-26 17:16:07 +00:00
robot . log . warn ( ` ${ botName } - Error while fetching project column ` , card . column _id , error )
2018-02-07 16:59:55 +00:00
return
}
2019-03-26 17:16:07 +00:00
const prNumber = last ( card . content _url . split ( '/' ) , - 1 )
2018-02-08 12:09:59 +00:00
const fullJobName = automatedTestsConfig [ 'job-full-name' ]
2018-02-07 16:59:55 +00:00
2019-01-10 12:57:39 +00:00
await processPullRequest ( context , robot , { ... repoInfo , number : prNumber } , fullJobName )
2018-02-07 16:59:55 +00:00
}
2019-01-10 12:57:39 +00:00
async function processPullRequest ( context , robot , prInfo , fullJobName ) {
const { github } = context
2018-02-07 16:59:55 +00:00
// Remove the PR from the pending PR list, if it is there
2018-02-16 20:35:49 +00:00
pendingPullRequests . delete ( prInfo . number )
2019-03-26 17:16:07 +00:00
robot . log . debug ( ` ${ botName } - Removed PR # ${ prInfo . number } from queue, current queue length is ${ pendingPullRequests . size } ` )
2018-02-07 16:59:55 +00:00
try {
2019-01-10 12:57:39 +00:00
// Get detailed pull request
const pullRequestPayload = await github . pullRequests . get ( prInfo )
const pullRequest = pullRequestPayload . data
if ( ! pullRequest ) {
robot . log . warn ( ` ${ botName } - Could not find PR ` , prInfo )
return
}
2019-02-25 20:54:43 +00:00
if ( pullRequest . state === 'closed' ) {
robot . log . info ( ` ${ botName } - PR is closed, discarded ` , prInfo )
return
}
2019-01-10 12:57:39 +00:00
const statusContext = 'jenkins/prs/android-e2e'
const currentStatus = await gitHubHelpers . getPullRequestCurrentStatusForContext ( context , statusContext , pullRequest )
2018-02-07 16:59:55 +00:00
2019-01-10 12:57:39 +00:00
switch ( currentStatus ) {
2019-02-25 20:54:43 +00:00
case undefined :
2019-01-10 12:57:39 +00:00
case 'pending' :
2019-02-25 20:54:43 +00:00
case 'failure' :
2019-03-26 17:16:07 +00:00
pendingPullRequests . set ( prInfo . number , { context : context , prInfo , fullJobName : fullJobName } )
robot . log . debug ( ` ${ botName } - Status for ${ statusContext } is ' ${ currentStatus } ', adding to backlog to check periodically, current queue length is ${ pendingPullRequests . size } ` , prInfo )
2018-02-07 16:59:55 +00:00
return
2019-01-10 12:57:39 +00:00
case 'error' :
robot . log . debug ( ` ${ botName } - Status for ${ statusContext } is ' ${ currentStatus } ', exiting ` , prInfo )
2018-02-07 16:59:55 +00:00
return
2019-01-10 12:57:39 +00:00
case 'success' :
robot . log . debug ( ` ${ botName } - Status for ${ statusContext } is ' ${ currentStatus } ', proceeding ` , prInfo )
2018-02-07 16:59:55 +00:00
break
default :
2019-01-10 12:57:39 +00:00
robot . log . warn ( ` ${ botName } - Status for ${ statusContext } is ' ${ currentStatus } ', ignoring ` , prInfo )
2018-02-07 16:59:55 +00:00
return
}
} catch ( err ) {
2018-02-16 20:35:49 +00:00
robot . log . error ( ` Couldn't calculate the PR approval state: ${ err } ` , prInfo )
2018-02-07 16:59:55 +00:00
return
}
try {
2018-02-16 20:35:49 +00:00
const args = { parameters : { pr _id : prInfo . number , apk : ` --apk= ${ prInfo . number } .apk ` } }
2018-02-07 16:59:55 +00:00
if ( process . env . DRY _RUN ) {
2018-02-16 20:35:49 +00:00
robot . log ( ` ${ botName } - Would start ${ fullJobName } job in Jenkins ` , prInfo , args )
2018-02-07 16:59:55 +00:00
} else {
2018-02-16 20:35:49 +00:00
robot . log ( ` ${ botName } - Starting ${ fullJobName } job in Jenkins ` , prInfo , args )
2018-02-07 16:59:55 +00:00
const buildId = await jenkins . job . build ( fullJobName , args )
2018-02-16 20:35:49 +00:00
robot . log ( ` ${ botName } - Started job in Jenkins ` , prInfo , buildId )
2018-02-07 16:59:55 +00:00
}
} catch ( error ) {
2019-03-26 17:16:07 +00:00
pendingPullRequests . set ( prInfo . number , { context : context , prInfo : prInfo , fullJobName : fullJobName } )
robot . log . error ( ` ${ botName } - Error while triggering Jenkins build. Will retry later, current queue length is ${ pendingPullRequests . size } ` , prInfo , error )
2018-02-07 16:59:55 +00:00
}
}
async function checkPendingPullRequests ( robot ) {
const _pendingPullRequests = pendingPullRequests . clone ( )
2019-03-26 16:47:46 +00:00
robot . log . debug ( ` ${ botName } - Processing ${ _pendingPullRequests . size } pending PRs ` )
2018-02-07 16:59:55 +00:00
2019-03-26 17:16:07 +00:00
for ( const { context , prInfo , fullJobName } of _pendingPullRequests . values ( ) ) {
await processPullRequest ( context , robot , prInfo , fullJobName )
2018-02-07 16:59:55 +00:00
}
2019-03-26 16:47:46 +00:00
robot . log . debug ( ` ${ botName } - Finished processing ${ _pendingPullRequests . size } pending PRs ` )
2018-02-07 16:59:55 +00:00
}