2018-01-23 14:31:10 +00:00
// Description:
// Script that listens to GitHub pull reviews
2018-01-28 08:24:30 +00:00
// and assigns the PR to TO TEST column on the 'Pipeline for QA' project
2018-01-23 14:31:10 +00:00
//
// Dependencies:
// github: "^13.1.0"
2018-01-23 18:38:25 +00:00
// probot-config: "^0.1.0"
// probot-scheduler: "^1.0.3"
2018-01-23 14:31:10 +00:00
// probot-slack-status: "^0.2.2"
//
// Author:
// PombeirP
2018-01-28 08:24:30 +00:00
// const getConfig = require('probot-config')
2018-01-23 14:31:10 +00:00
const defaultConfig = require ( '../lib/config' )
2018-01-23 18:38:25 +00:00
const createScheduler = require ( 'probot-scheduler' )
2018-01-23 14:31:10 +00:00
const Slack = require ( 'probot-slack-status' )
2018-02-05 09:30:08 +00:00
const slackHelper = require ( '../lib/slack' )
2018-01-23 14:31:10 +00:00
let slackClient = null
2018-01-28 08:24:30 +00:00
module . exports = robot => {
2018-01-23 14:31:10 +00:00
// robot.on('slack.connected', ({ slack }) => {
Slack ( robot , ( slack ) => {
2018-01-28 08:24:30 +00:00
robot . log . trace ( 'Connected, assigned slackClient' )
2018-01-23 14:31:10 +00:00
slackClient = slack
} )
2018-01-28 08:24:30 +00:00
2018-01-23 18:38:25 +00:00
createScheduler ( robot , { interval : 10 * 60 * 1000 } )
robot . on ( 'schedule.repository' , context => checkOpenPullRequests ( robot , context ) )
2018-01-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
async function getReviewApprovalState ( github , robot , repo , pullRequest ) {
2018-01-24 11:01:16 +00:00
const threshold = 2 // Minimum number of approvers
2018-01-28 08:24:30 +00:00
2018-01-24 11:01:16 +00:00
var finalReviews = await getPullRequestReviewStates ( github , repo , pullRequest )
2018-02-05 16:14:48 +00:00
robot . log . debug ( finalReviews )
2018-01-23 18:38:25 +00:00
const approvedReviews = finalReviews . filter ( reviewState => reviewState === 'APPROVED' )
if ( approvedReviews . length >= threshold ) {
const reviewsWithChangesRequested = finalReviews . filter ( reviewState => reviewState === 'CHANGES_REQUESTED' )
2018-01-28 08:24:30 +00:00
if ( reviewsWithChangesRequested . length === 0 ) {
2018-01-25 15:04:37 +00:00
// Get detailed pull request
2018-02-05 16:12:16 +00:00
const fullPullRequestPayload = await github . pullRequests . get ( { owner : repo . owner . login , repo : repo . name , number : pullRequest . number } )
pullRequest = fullPullRequestPayload . data
2018-01-25 15:04:37 +00:00
if ( pullRequest . mergeable !== null && pullRequest . mergeable !== undefined && ! pullRequest . mergeable ) {
2018-02-05 16:14:48 +00:00
robot . log . debug ( ` pullRequest.mergeable is ${ pullRequest . mergeable } , considering as failed ` )
2018-01-25 15:04:37 +00:00
return 'failed'
2018-01-23 18:38:25 +00:00
}
2018-01-25 15:04:37 +00:00
let state = 'pending'
switch ( pullRequest . mergeable _state ) {
case 'clean' :
state = 'approved'
break
case 'dirty' :
state = 'failed'
break
}
2018-02-05 16:14:48 +00:00
robot . log . debug ( ` pullRequest.mergeable_state is ${ pullRequest . mergeable _state } , considering state as ${ state } ` )
2018-01-25 15:04:37 +00:00
return state
2018-01-23 16:06:28 +00:00
}
2018-01-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
2018-01-23 14:31:10 +00:00
return 'pending'
}
2018-01-28 08:24:30 +00:00
async function getPullRequestReviewStates ( github , repo , pullRequest ) {
2018-01-24 11:01:16 +00:00
var finalReviewsMap = new Map ( )
const ghreviews = await github . paginate (
2018-02-05 15:47:03 +00:00
github . pullRequests . getReviews ( { owner : repo . owner . login , repo : repo . name , number : pullRequest . number , per _page : 100 } ) ,
2018-01-24 11:01:16 +00:00
res => res . data )
for ( var review of ghreviews ) {
switch ( review . state ) {
case 'APPROVED' :
case 'CHANGES_REQUESTED' :
case 'PENDING' :
finalReviewsMap . set ( review . user . id , review . state )
break
}
}
2018-01-23 18:38:25 +00:00
2018-01-24 11:01:16 +00:00
return Array . from ( finalReviewsMap . values ( ) )
}
2018-01-28 08:24:30 +00:00
async function getProjectFromName ( github , ownerName , repoName , projectBoardName ) {
2018-02-05 16:12:16 +00:00
const ghprojectsPayload = await github . projects . getRepoProjects ( {
2018-01-23 18:38:25 +00:00
owner : ownerName ,
repo : repoName ,
2018-01-28 08:24:30 +00:00
state : 'open'
2018-01-23 18:38:25 +00:00
} )
2018-01-28 08:24:30 +00:00
2018-02-05 16:12:16 +00:00
return ghprojectsPayload . data . find ( p => p . name === projectBoardName )
2018-01-23 18:38:25 +00:00
}
2018-01-23 14:31:10 +00:00
2018-01-28 08:24:30 +00:00
async function getProjectCardForPullRequest ( github , columnId , pullRequestUrl ) {
2018-02-05 16:12:16 +00:00
const ghcardsPayload = await github . projects . getProjectCards ( { column _id : columnId } )
const ghcard = ghcardsPayload . data . find ( c => c . content _url === pullRequestUrl )
2018-01-28 08:24:30 +00:00
2018-01-23 14:31:10 +00:00
return ghcard
}
2018-01-28 08:24:30 +00:00
async function checkOpenPullRequests ( robot , context ) {
2018-01-23 14:31:10 +00:00
const github = context . github
2018-01-23 18:38:25 +00:00
const repo = context . payload . repository
const ownerName = repo . owner . login
const repoName = repo . name
2018-01-28 08:24:30 +00:00
// const config = await getConfig(context, 'github-bot.yml', defaultConfig(robot, '.github/github-bot.yml'))
2018-01-23 14:31:10 +00:00
const config = defaultConfig ( robot , '.github/github-bot.yml' )
2018-01-23 18:38:25 +00:00
const projectBoardConfig = config [ 'project-board' ]
2018-01-28 08:24:30 +00:00
2018-01-23 18:38:25 +00:00
if ( ! projectBoardConfig ) {
return
2018-01-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
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' ]
2018-01-28 08:24:30 +00:00
2018-01-23 14:31:10 +00:00
// Fetch repo projects
// TODO: The repo project and project column info should be cached
// in order to improve performance and reduce roundtrips
2018-02-05 09:30:08 +00:00
let project
2018-01-23 14:31:10 +00:00
try {
2018-01-28 08:24:30 +00:00
// Find 'Pipeline for QA' project
2018-02-05 09:30:08 +00:00
project = await getProjectFromName ( github , ownerName , repoName , projectBoardConfig . name )
2018-01-23 14:31:10 +00:00
if ( ! project ) {
2018-01-23 18:38:25 +00:00
robot . log . error ( ` Couldn't find project ${ projectBoardConfig . name } in repo ${ ownerName } / ${ repoName } ` )
2018-01-23 14:31:10 +00:00
return
}
2018-01-28 08:24:30 +00:00
2018-01-23 14:31:10 +00:00
robot . log . debug ( ` Fetched ${ project . name } project ( ${ project . id } ) ` )
2018-02-05 09:30:08 +00:00
} catch ( err ) {
robot . log . error ( ` Couldn't fetch the github projects for repo: ${ err } ` , ownerName , repoName )
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
// Fetch column IDs
let ghcolumns
try {
2018-02-05 16:12:16 +00:00
const ghcolumnsPayload = await github . projects . getProjectColumns ( { project _id : project . id } )
ghcolumns = ghcolumnsPayload . data
2018-02-05 09:30:08 +00:00
} catch ( err ) {
robot . log . error ( ` Couldn't fetch the github columns for project: ${ err } ` , ownerName , repoName , project . id )
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 15:47:03 +00:00
const contributorColumn = ghcolumns . find ( c => c . name === contributorColumnName )
2018-02-05 09:30:08 +00:00
if ( ! contributorColumn ) {
robot . log . error ( ` Couldn't find ${ contributorColumnName } column in project ${ project . name } ` )
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 15:47:03 +00:00
const reviewColumn = ghcolumns . find ( c => c . name === reviewColumnName )
2018-02-05 09:30:08 +00:00
if ( ! reviewColumn ) {
robot . log . error ( ` Couldn't find ${ reviewColumnName } column in project ${ project . name } ` )
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 15:47:03 +00:00
const testColumn = ghcolumns . find ( c => c . name === testColumnName )
2018-02-05 09:30:08 +00:00
if ( ! testColumn ) {
robot . log . error ( ` Couldn't find ${ testColumnName } column in project ${ project . name } ` )
return
}
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
robot . log . debug ( ` Fetched ${ contributorColumn . name } ( ${ contributorColumn . id } ), ${ reviewColumn . name } ( ${ reviewColumn . id } ), ${ testColumn . name } ( ${ testColumn . id } ) columns ` )
try {
2018-01-23 18:38:25 +00:00
// Gather all open PRs in this repo
const allPullRequests = await github . paginate (
2018-02-05 15:47:03 +00:00
github . pullRequests . getAll ( { owner : ownerName , repo : repoName , per _page : 100 } ) ,
2018-01-23 18:38:25 +00:00
res => res . data
)
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
// And make sure they are assigned to the correct project column
2018-01-23 18:38:25 +00:00
for ( var pullRequest of allPullRequests ) {
2018-02-05 09:30:08 +00:00
try {
await assignPullRequestToCorrectColumn ( github , robot , repo , pullRequest , contributorColumn , reviewColumn , testColumn , config . slack . notification . room )
} catch ( err ) {
robot . log . error ( ` Unhandled exception while processing PR: ${ err } ` , ownerName , repoName )
}
2018-01-23 18:38:25 +00:00
}
} catch ( err ) {
2018-02-05 09:30:08 +00:00
robot . log . error ( ` Couldn't fetch the github pull requests for repo: ${ err } ` , ownerName , repoName )
2018-01-23 18:38:25 +00:00
}
}
2018-02-05 09:30:08 +00:00
async function assignPullRequestToCorrectColumn ( github , robot , repo , pullRequest , contributorColumn , reviewColumn , testColumn , room ) {
2018-01-23 18:38:25 +00:00
const ownerName = repo . owner . login
const repoName = repo . name
const prNumber = pullRequest . number
2018-01-28 08:24:30 +00:00
let state = null
2018-01-23 18:38:25 +00:00
try {
state = await getReviewApprovalState ( github , robot , repo , pullRequest )
} catch ( err ) {
robot . log . error ( ` Couldn't calculate the PR approval state: ${ err } ` , ownerName , repoName , prNumber )
}
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
let srcColumns , dstColumn
2018-01-25 15:04:37 +00:00
switch ( state ) {
case 'approved' :
2018-02-05 16:27:00 +00:00
srcColumns = [ contributorColumn , reviewColumn ]
2018-01-25 15:04:37 +00:00
dstColumn = testColumn
2018-01-28 08:24:30 +00:00
break
2018-01-25 15:04:37 +00:00
case 'failed' :
2018-02-05 09:30:08 +00:00
srcColumns = [ reviewColumn , testColumn ]
2018-01-25 15:04:37 +00:00
dstColumn = reviewColumn
2018-01-28 08:24:30 +00:00
break
2018-02-05 09:30:08 +00:00
case 'pending' :
srcColumns = [ testColumn ]
dstColumn = contributorColumn
break
2018-01-25 15:04:37 +00:00
default :
2018-01-28 08:24:30 +00:00
return
2018-01-23 18:38:25 +00:00
}
2018-01-28 08:24:30 +00:00
2018-01-23 18:38:25 +00:00
robot . log . debug ( ` assignPullRequestToTest - Handling Pull Request # ${ prNumber } on repo ${ ownerName } / ${ repoName } . 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-05 16:12:16 +00:00
existingGHCard = await getProjectCardForPullRequest ( github , c . id , pullRequest . issue _url )
if ( existingGHCard ) {
2018-02-05 09:30:08 +00:00
srcColumn = c
break
}
} catch ( err ) {
robot . log . error ( ` Failed to retrieve project card for the PR, aborting: ${ err } ` , c . id , pullRequest . issue _url )
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-05 16:12:16 +00:00
robot . log . trace ( ` 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-05 16:12:16 +00:00
robot . log . info ( ` Would have moved card ${ existingGHCard . id } to ${ dstColumn . name } for PR # ${ prNumber } ` )
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-23 14:31:10 +00:00
}
2018-01-28 08:24:30 +00:00
2018-02-05 16:12:16 +00:00
robot . log . info ( ` Moved card ${ existingGHCard . id } to ${ dstColumn . name } for PR # ${ prNumber } ` )
2018-01-28 08:24:30 +00:00
2018-02-05 09:30:08 +00:00
slackHelper . sendMessage ( robot , slackClient , room , ` Assigned PR to ${ dstColumn . name } column \n ${ pullRequest . html _url } ` )
} catch ( err ) {
2018-01-23 18:38:25 +00:00
robot . log . error ( ` Couldn't move project card for the PR: ${ err } ` , srcColumn . id , dstColumn . id , pullRequest . id )
2018-01-28 08:24:30 +00:00
slackHelper . sendMessage ( robot , slackClient , 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-05 09:30:08 +00:00
robot . log . debug ( ` 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-05 16:12:16 +00:00
const existingGHCard = await getProjectCardForPullRequest ( github , dstColumn . id , pullRequest . issue _url )
if ( existingGHCard ) {
robot . log . trace ( ` 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-01-23 18:38:25 +00:00
robot . log . error ( ` 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-05 09:30:08 +00:00
robot . log . info ( ` Would have created card in ${ dstColumn . name } column for PR # ${ prNumber } ` )
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
2018-02-05 16:12:16 +00:00
const ghcardPayload = await github . projects . createProjectCard ( {
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-05 16:12:16 +00:00
robot . log . info ( ` Created card ${ ghcardPayload . data . id } in ${ dstColumn . name } for PR # ${ prNumber } ` )
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
robot . log . debug ( ` 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
}