2018-01-23 14:31:10 +00:00
// Description:
2018-02-05 18:12:32 +00:00
// Script that periodically checks to GitHub pull request reviews
// and assigns the PR to CONTRIBUTOR/REVIEW/TO TEST columns on the 'Pipeline for QA' project
2018-01-23 14:31:10 +00:00
//
// Dependencies:
// github: "^13.1.0"
2019-01-31 15:07:54 +00:00
// probot-config: "^1.0.0"
2018-01-23 18:38:25 +00:00
// probot-scheduler: "^1.0.3"
2018-01-23 14:31:10 +00:00
//
// Author:
// PombeirP
2018-01-23 18:38:25 +00:00
const createScheduler = require ( 'probot-scheduler' )
2018-02-08 12:09:59 +00:00
const getConfig = require ( 'probot-config' )
const defaultConfig = require ( '../lib/config' )
const gitHubHelpers = require ( '../lib/github-helpers' )
2018-10-10 09:02:26 +00:00
// const slackHelper = require('../lib/slack')
2018-01-23 14:31:10 +00:00
2018-02-13 14:51:00 +00:00
const botName = 'assign-approved-pr-to-test'
2018-01-23 14:31:10 +00:00
2018-01-28 08:24:30 +00:00
module . exports = robot => {
2018-04-18 09:53:38 +00:00
createScheduler ( robot , { interval : 10 * 60 * 1000 , delay : ! process . env . DISABLE _DELAY } )
2018-01-23 18:38:25 +00:00
robot . on ( 'schedule.repository' , context => checkOpenPullRequests ( robot , context ) )
2019-01-22 13:00:23 +00:00
robot . on ( 'pull_request.opened' , context => handleOpenedPullRequest ( robot , context ) )
}
// This method creates a sentinel status in new PRs so that they can't be merged before an e2e test run has successfully completed
async function handleOpenedPullRequest ( robot , 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
}
await context . github . repos . createStatus ( context . repo ( {
context : 'Mobile e2e tests' ,
2019-02-25 20:08:21 +00:00
description : ` Tests will run once the PR is moved to the ${ automatedTestsConfig [ 'kickoff-column-name' ] } column ` ,
2019-01-22 13:00:23 +00:00
sha : context . payload . pull _request . head . sha ,
state : 'error'
} ) )
2018-01-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
async function checkOpenPullRequests ( robot , context ) {
2019-01-22 13:00:23 +00:00
const { github , payload : { repository : repo } } = context
2019-01-22 11:26:05 +00:00
const repoInfo = context . repo ( )
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
2018-01-28 08:24:30 +00:00
2018-01-23 18:38:25 +00:00
if ( ! projectBoardConfig ) {
2018-02-16 20:35:49 +00:00
robot . log . debug ( ` ${ botName } - Project board not configured in repo ${ repoInfo . owner } / ${ repoInfo . repo } , ignoring ` )
2018-01-23 18:38:25 +00:00
return
2018-01-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
2018-03-14 13:06:59 +00:00
const testedPullRequestLabelName = projectBoardConfig [ 'tested-pr-label-name' ]
2018-02-05 09:30:08 +00:00
const contributorColumnName = projectBoardConfig [ 'contributor-column-name' ]
2018-01-23 18:38:25 +00:00
const reviewColumnName = projectBoardConfig [ 'review-column-name' ]
const testColumnName = projectBoardConfig [ 'test-column-name' ]
2019-08-05 09:25:55 +00:00
const minReviewers = projectBoardConfig [ 'min-reviewers' ] || 1
2018-01-28 08:24:30 +00:00
2018-02-22 15:37:53 +00:00
// Find 'Pipeline for QA' project
const project = await gitHubHelpers . getRepoProjectByName ( github , robot , repoInfo , projectBoardConfig . name , botName )
if ( ! project ) {
2018-02-05 09:30:08 +00:00
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
// Fetch column IDs
let ghcolumns
try {
2019-01-22 17:21:34 +00:00
const ghcolumnsPayload = await github . projects . listColumns ( { project _id : project . id } )
2018-02-05 16:12:16 +00:00
ghcolumns = ghcolumnsPayload . data
2018-02-05 09:30:08 +00:00
} catch ( err ) {
2018-02-16 20:35:49 +00:00
robot . log . error ( ` ${ botName } - Couldn't fetch the github columns for project: ${ err } ` , repoInfo , project . id )
2018-02-05 09:30:08 +00:00
return
}
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
try {
const contributorColumn = findColumnByName ( ghcolumns , contributorColumnName )
const reviewColumn = findColumnByName ( ghcolumns , reviewColumnName )
const testColumn = findColumnByName ( ghcolumns , testColumnName )
2018-04-18 10:50:29 +00:00
const columns = { contributor : contributorColumn , review : reviewColumn , test : testColumn }
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
robot . log . debug ( ` ${ botName } - Fetched ${ contributorColumn . name } ( ${ contributorColumn . id } ), ${ reviewColumn . name } ( ${ reviewColumn . id } ), ${ testColumn . name } ( ${ testColumn . id } ) columns ` )
2018-02-05 09:30:08 +00:00
2018-02-16 20:35:49 +00:00
try {
// Gather all open PRs in this repo
const allPullRequests = await github . paginate (
2019-01-22 17:21:34 +00:00
github . pullRequests . list ( context . repo ( { per _page : 100 } ) ) ,
2018-02-16 20:35:49 +00:00
res => res . data
)
// And make sure they are assigned to the correct project column
for ( const pullRequest of allPullRequests ) {
try {
2019-08-05 09:25:55 +00:00
await assignPullRequestToCorrectColumn ( context , robot , repo , pullRequest , minReviewers , testedPullRequestLabelName , columns , config . slack . notification . room )
2018-02-16 20:35:49 +00:00
} catch ( err ) {
robot . log . error ( ` ${ botName } - Unhandled exception while processing PR: ${ err } ` , repoInfo )
}
2018-02-05 09:30:08 +00:00
}
2018-02-16 20:35:49 +00:00
} catch ( err ) {
robot . log . error ( ` ${ botName } - Couldn't fetch the github pull requests for repo: ${ err } ` , repoInfo )
2018-01-23 18:38:25 +00:00
}
} catch ( err ) {
2018-02-16 20:35:49 +00:00
robot . log . error ( err . message , project . name )
2018-01-23 18:38:25 +00:00
}
}
2019-08-05 09:25:55 +00:00
async function assignPullRequestToCorrectColumn ( context , robot , repo , pullRequest , minReviewers , testedPullRequestLabelName , columns , room ) {
2019-01-22 11:26:05 +00:00
const { github } = context
2018-02-16 20:35:49 +00:00
const prInfo = { owner : repo . owner . login , repo : repo . name , number : pullRequest . number }
2018-01-28 08:24:30 +00:00
let state = null
2018-01-23 18:38:25 +00:00
try {
2019-01-22 13:00:23 +00:00
// Ignore statuses created by us
const filterFn = ( status ) => ! ( status . context === 'Mobile e2e tests' &&
status . creator &&
( status . creator . login === 'status-github-bot[bot]' || status . creator . login === 'e2e-tests-check-bot[bot]' ) )
2019-08-05 09:25:55 +00:00
state = await gitHubHelpers . getReviewApprovalState ( context , robot , prInfo , minReviewers , testedPullRequestLabelName , filterFn )
2018-01-23 18:38:25 +00:00
} catch ( err ) {
2018-02-16 20:35:49 +00:00
robot . log . error ( ` ${ botName } - Couldn't calculate the PR approval state: ${ err } ` , prInfo )
2018-01-23 18:38:25 +00:00
}
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
const { srcColumns , dstColumn } = getColumns ( state , columns )
if ( ! dstColumn ) {
2018-04-18 09:53:38 +00:00
robot . log . debug ( ` ${ botName } - No dstColumn, state= ${ state } , columns= ${ JSON . stringify ( columns ) } , srcColumns= ${ srcColumns } ` )
2018-02-16 20:35:49 +00:00
return
2018-01-23 18:38:25 +00:00
}
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
robot . log . debug ( ` ${ botName } - Handling Pull Request # ${ prInfo . number } on repo ${ prInfo . owner } / ${ prInfo . repo } . PR should be in ${ dstColumn . name } column ` )
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
// Look for PR card in source column(s)
2018-02-05 16:12:16 +00:00
let existingGHCard = null
2018-02-05 09:30:08 +00:00
let srcColumn = null
for ( const c of srcColumns ) {
try {
2018-02-07 16:59:55 +00:00
existingGHCard = await gitHubHelpers . getProjectCardForIssue ( github , c . id , pullRequest . issue _url )
2018-02-05 16:12:16 +00:00
if ( existingGHCard ) {
2018-02-05 09:30:08 +00:00
srcColumn = c
break
}
} catch ( err ) {
2018-02-13 14:51:00 +00:00
robot . log . error ( ` ${ botName } - Failed to retrieve project card for the PR, aborting: ${ err } ` , c . id , pullRequest . issue _url )
2018-02-05 09:30:08 +00:00
return
}
2018-01-23 18:38:25 +00:00
}
2018-01-28 08:24:30 +00:00
2018-02-05 16:12:16 +00:00
if ( existingGHCard ) {
2018-01-23 18:38:25 +00:00
// Move PR card to the destination column
try {
2018-02-13 14:51:00 +00:00
robot . log . trace ( ` ${ botName } - Found card in source column ${ srcColumn . name } ` , existingGHCard . id , srcColumn . id )
2018-01-28 08:24:30 +00:00
2018-02-05 17:52:07 +00:00
if ( dstColumn === srcColumn ) {
return
}
2018-01-28 08:24:30 +00:00
if ( process . env . DRY _RUN || process . env . DRY _RUN _PR _TO _TEST ) {
2018-02-16 20:35:49 +00:00
robot . log . info ( ` ${ botName } - Would have moved card ${ existingGHCard . id } to ${ dstColumn . name } for PR # ${ prInfo . number } ` )
2018-01-28 08:24:30 +00:00
} else {
2018-01-23 18:38:25 +00:00
// Found in the source column, let's move it to the destination column
2018-02-05 16:12:16 +00:00
await github . projects . moveProjectCard ( { id : existingGHCard . id , position : 'bottom' , column _id : dstColumn . id } )
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
robot . log . info ( ` ${ botName } - Moved card ${ existingGHCard . id } to ${ dstColumn . name } for PR # ${ prInfo . number } ` )
2018-02-05 18:12:32 +00:00
}
2018-01-28 08:24:30 +00:00
2018-10-10 09:02:26 +00:00
// slackHelper.sendMessage(robot, room, `Assigned PR to ${dstColumn.name} column\n${pullRequest.html_url}`)
2018-02-05 09:30:08 +00:00
} catch ( err ) {
2018-02-13 14:51:00 +00:00
robot . log . error ( ` ${ botName } - Couldn't move project card for the PR: ${ err } ` , srcColumn . id , dstColumn . id , pullRequest . id )
2018-10-10 09:02:26 +00:00
// slackHelper.sendMessage(robot, room, `I couldn't move the PR to ${dstColumn.name} column :confused:\n${pullRequest.html_url}`)
2018-01-23 18:38:25 +00:00
}
} else {
try {
2018-02-13 14:51:00 +00:00
robot . log . debug ( ` ${ botName } - Didn't find card in source column(s) ` , srcColumns . map ( c => c . id ) )
2018-01-28 08:24:30 +00:00
2018-01-23 18:38:25 +00:00
// Look for PR card in destination column
2018-01-23 14:31:10 +00:00
try {
2018-02-07 16:59:55 +00:00
const existingGHCard = await gitHubHelpers . getProjectCardForIssue ( github , dstColumn . id , pullRequest . issue _url )
2018-02-05 16:12:16 +00:00
if ( existingGHCard ) {
2018-02-13 14:51:00 +00:00
robot . log . trace ( ` ${ botName } - Found card in target column, ignoring ` , existingGHCard . id , dstColumn . id )
2018-01-23 18:38:25 +00:00
return
}
2018-01-23 14:31:10 +00:00
} catch ( err ) {
2018-02-13 14:51:00 +00:00
robot . log . error ( ` ${ botName } - Failed to retrieve project card for the PR, aborting: ${ err } ` , dstColumn . id , pullRequest . issue _url )
2018-01-23 14:31:10 +00:00
return
}
2018-01-28 08:24:30 +00:00
if ( process . env . DRY _RUN || process . env . DRY _RUN _PR _TO _TEST ) {
2018-02-16 20:35:49 +00:00
robot . log . info ( ` Would have created card in ${ dstColumn . name } column for PR # ${ prInfo . number } ` )
2018-01-28 08:24:30 +00:00
} else {
2018-01-23 18:38:25 +00:00
// It wasn't in either the source nor the destination columns, let's create a new card for it in the destination column
2019-01-31 14:50:23 +00:00
const ghcardPayload = await github . projects . createCard ( {
2018-01-23 18:38:25 +00:00
column _id : dstColumn . id ,
content _type : 'PullRequest' ,
content _id : pullRequest . id
} )
2018-01-28 08:24:30 +00:00
2018-02-16 20:35:49 +00:00
robot . log . info ( ` ${ botName } - Created card ${ ghcardPayload . data . id } in ${ dstColumn . name } for PR # ${ prInfo . number } ` )
2018-01-23 14:31:10 +00:00
}
} catch ( err ) {
2018-01-23 18:38:25 +00:00
// We normally arrive here because there is already a card for the PR in another column
2018-02-13 14:51:00 +00:00
robot . log . debug ( ` ${ botName } - Couldn't create project card for the PR: ${ err } ` , dstColumn . id , pullRequest . id )
2018-01-23 14:31:10 +00:00
}
2018-01-23 18:38:25 +00:00
}
2018-01-23 14:31:10 +00:00
}
2018-02-16 20:35:49 +00:00
function getColumns ( state , columns ) {
switch ( state ) {
case 'awaiting_reviewers' :
return { srcColumns : [ columns . contributor , columns . test ] , dstColumn : columns . review }
case 'changes_requested' :
return { srcColumns : [ columns . review , columns . test ] , dstColumn : columns . contributor }
case 'failed' :
return { srcColumns : [ columns . review , columns . test ] , dstColumn : columns . contributor }
case 'approved' :
return { srcColumns : [ columns . contributor , columns . review ] , dstColumn : columns . test }
default :
return { srcColumns : [ ] , dstColumn : null }
}
}
function findColumnByName ( ghcolumns , columnName ) {
const column = ghcolumns . find ( c => c . name === columnName )
if ( ! column ) {
throw new Error ( ` ${ botName } - Couldn't find ${ columnName } column ` )
}
return column
}