2018-01-23 14:31:10 +00:00
// Description:
// Script that listens to GitHub pull reviews
// and assigns the PR to TO TEST column on the "Pipeline for QA" project
//
// 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
const getConfig = require ( 'probot-config' )
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' )
let slackClient = null
module . exports = function ( robot ) {
// robot.on('slack.connected', ({ slack }) => {
Slack ( robot , ( slack ) => {
robot . log . trace ( "Connected, assigned slackClient" )
slackClient = slack
} )
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-23 18:38:25 +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-23 18:38:25 +00:00
2018-01-24 11:01:16 +00:00
var finalReviews = await getPullRequestReviewStates ( github , repo , pullRequest )
2018-01-23 18:38:25 +00:00
if ( process . env . DRY _RUN _PR _TO _TEST ) {
robot . log . debug ( finalReviews )
}
const approvedReviews = finalReviews . filter ( reviewState => reviewState === 'APPROVED' )
if ( approvedReviews . length >= threshold ) {
const reviewsWithChangesRequested = finalReviews . filter ( reviewState => reviewState === 'CHANGES_REQUESTED' )
2018-01-23 16:06:28 +00:00
if ( reviewsWithChangesRequested . length == 0 ) {
2018-01-25 15:04:37 +00:00
// Get detailed pull request
const fullPullRequest = await github . pullRequests . get ( { owner : repo . owner . login , repo : repo . name , number : pullRequest . number } )
pullRequest = fullPullRequest . data
if ( pullRequest . mergeable !== null && pullRequest . mergeable !== undefined && ! pullRequest . mergeable ) {
if ( process . env . DRY _RUN _PR _TO _TEST ) {
robot . log . debug ( ` pullRequest.mergeable is ${ pullRequest . mergeable } , considering as failed ` )
}
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
}
if ( process . env . DRY _RUN _PR _TO _TEST ) {
robot . log . debug ( ` pullRequest.mergeable_state is ${ pullRequest . mergeable _state } , considering state as ${ state } ` )
}
return state
2018-01-23 16:06:28 +00:00
}
2018-01-23 14:31:10 +00:00
}
return 'pending'
}
2018-01-23 18:38:25 +00:00
2018-01-24 11:01:16 +00:00
async function getPullRequestReviewStates ( github , repo , pullRequest ) {
var finalReviewsMap = new Map ( )
const ghreviews = await github . paginate (
github . pullRequests . getReviews ( { owner : repo . owner . login , repo : repo . name , number : pullRequest . number } ) ,
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-23 18:38:25 +00:00
async function getProjectFromName ( github , ownerName , repoName , projectBoardName ) {
ghprojects = await github . projects . getRepoProjects ( {
owner : ownerName ,
repo : repoName ,
state : "open"
} )
return ghprojects . data . find ( p => p . name === projectBoardName )
}
2018-01-23 14:31:10 +00:00
2018-01-24 13:37:48 +00:00
async function getProjectCardForPullRequest ( github , columnId , pullRequestUrl ) {
2018-01-23 14:31:10 +00:00
const ghcards = await github . projects . getProjectCards ( { column _id : columnId } )
ghcard = ghcards . data . find ( c => c . content _url === pullRequestUrl )
return ghcard
}
2018-01-23 18:38:25 +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-23 14:31:10 +00:00
//const config = await getConfig(context, 'github-bot.yml', defaultConfig(robot, '.github/github-bot.yml'))
const config = defaultConfig ( robot , '.github/github-bot.yml' )
2018-01-23 18:38:25 +00:00
const projectBoardConfig = config [ 'project-board' ]
2018-01-23 14:31:10 +00:00
2018-01-23 18:38:25 +00:00
if ( ! projectBoardConfig ) {
return
2018-01-23 14:31:10 +00:00
}
2018-01-23 18:38:25 +00:00
const reviewColumnName = projectBoardConfig [ 'review-column-name' ]
const testColumnName = projectBoardConfig [ 'test-column-name' ]
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
try {
// Find "Pipeline for QA" project
2018-01-23 18:38:25 +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
}
robot . log . debug ( ` Fetched ${ project . name } project ( ${ project . id } ) ` )
// Fetch column IDs
try {
ghcolumns = await github . projects . getProjectColumns ( { project _id : project . id } )
2018-01-23 18:38:25 +00:00
} catch ( err ) {
robot . log . error ( ` Couldn't fetch the github columns for project: ${ err } ` , ownerName , repoName , project . id )
return
}
const reviewColumn = ghcolumns . data . find ( c => c . name === reviewColumnName )
if ( ! reviewColumn ) {
robot . log . error ( ` Couldn't find ${ reviewColumnName } column in project ${ project . name } ` )
return
}
const testColumn = ghcolumns . data . find ( c => c . name === testColumnName )
if ( ! testColumn ) {
robot . log . error ( ` Couldn't find ${ testColumnName } column in project ${ project . name } ` )
return
}
robot . log . debug ( ` Fetched ${ reviewColumn . name } ( ${ reviewColumn . id } ), ${ testColumn . name } ( ${ testColumn . id } ) columns ` )
// Gather all open PRs in this repo
const allPullRequests = await github . paginate (
github . pullRequests . getAll ( { owner : ownerName , repo : repoName } ) ,
res => res . data
)
// And make sure they are assigned to the correct prject column
for ( var pullRequest of allPullRequests ) {
await assignPullRequestToCorrectColumn ( github , robot , repo , pullRequest , reviewColumn , testColumn , config . slack . notification . room )
}
} catch ( err ) {
robot . log . error ( ` Couldn't fetch the github projects for repo: ${ err } ` , ownerName , repoName )
}
}
async function assignPullRequestToCorrectColumn ( github , robot , repo , pullRequest , reviewColumn , testColumn , room ) {
const ownerName = repo . owner . login
const repoName = repo . name
const prNumber = pullRequest . number
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-25 15:04:37 +00:00
switch ( state ) {
case 'approved' :
srcColumn = reviewColumn
dstColumn = testColumn
break ;
case 'failed' :
srcColumn = testColumn
dstColumn = reviewColumn
break ;
default :
return ;
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 ` )
// Look for PR card in source column
let ghcard = null
try {
2018-01-24 13:37:48 +00:00
ghcard = await getProjectCardForPullRequest ( github , srcColumn . id , pullRequest . issue _url )
2018-01-23 18:38:25 +00:00
} catch ( err ) {
robot . log . error ( ` Failed to retrieve project card for the PR, aborting: ${ err } ` , srcColumn . id , pullRequest . issue _url )
return
}
if ( ghcard ) {
// Move PR card to the destination column
try {
robot . log . trace ( ` Found card in source column ` , ghcard . id , srcColumn . id )
2018-01-23 14:31:10 +00:00
2018-01-23 18:38:25 +00:00
if ( ! process . env . DRY _RUN && ! process . env . DRY _RUN _PR _TO _TEST ) {
// Found in the source column, let's move it to the destination column
await github . projects . moveProjectCard ( { id : ghcard . id , position : 'bottom' , column _id : dstColumn . id } )
2018-01-23 14:31:10 +00:00
}
2018-01-23 18:38:25 +00:00
robot . log . info ( ` Moved card ${ ghcard . id } to ${ dstColumn . name } for PR # ${ prNumber } ` )
} catch ( err ) {
const slackHelper = require ( '../lib/slack' )
2018-01-23 14:31:10 +00:00
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 )
if ( ! process . env . DRY _RUN _PR _TO _TEST ) {
slackHelper . sendMessage ( robot , slackClient , room , ` I couldn't move the PR to ${ dstColumnName } column :confused: \n ${ pullRequest . html _url } ` )
}
return
}
} else {
try {
robot . log . debug ( ` Didn't find card in source column ` , srcColumn . id )
2018-01-23 14:31:10 +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-01-24 13:37:48 +00:00
ghcard = await getProjectCardForPullRequest ( github , dstColumn . id , pullRequest . issue _url )
2018-01-23 18:38:25 +00:00
if ( ghcard ) {
robot . log . trace ( ` Found card in target column, ignoring ` , ghcard . id , dstColumn . id )
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-23 16:06:28 +00:00
2018-01-23 18:38:25 +00:00
if ( ! process . env . DRY _RUN && ! process . env . DRY _RUN _PR _TO _TEST ) {
// It wasn't in either the source nor the destination columns, let's create a new card for it in the destination column
ghcard = await github . projects . createProjectCard ( {
column _id : dstColumn . id ,
content _type : 'PullRequest' ,
content _id : pullRequest . id
} )
robot . log . info ( ` Created card ${ ghcard . 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 )
return
2018-01-23 14:31:10 +00:00
}
2018-01-23 18:38:25 +00:00
}
if ( ! process . env . DRY _RUN _PR _TO _TEST ) {
const slackHelper = require ( '../lib/slack' )
slackHelper . sendMessage ( robot , slackClient , room , ` Assigned PR to ${ dstColumn . name } column \n ${ pullRequest . html _url } ` )
2018-01-23 14:31:10 +00:00
}
}