status-github-bot/bot_scripts/trigger-automation-test-bui...

244 lines
8.9 KiB
JavaScript

// 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"
// probot-config: "^1.0.0"
//
// Author:
// PombeirP
const getConfig = require('probot-config')
const createScheduler = require('probot-scheduler')
const jenkins = require('jenkins')({ baseUrl: process.env.JENKINS_URL, crumbIssuer: true, promisify: true })
const HashMap = require('hashmap')
const HashSet = require('hashset')
const defaultConfig = require('../lib/config')
const gitHubHelpers = require('../lib/github-helpers')
const botName = 'trigger-automation-test-build'
const pendingPullRequests = new HashMap()
const existingProjectsProcessed = new HashSet()
module.exports = (robot) => {
if (!process.env.JENKINS_URL) {
robot.log.info(`${botName} - Jenkins is not configured, not loading script`)
return
}
robot.log.info(`${botName} - Starting up`)
const { stop } = createScheduler(robot, { interval: 1 * 60 * 1000, delay: false })
robot.on('schedule.repository', context => processExistingProjectCards(robot, context, stop))
setInterval(checkPendingPullRequests, 5 * 1000 * 60, robot)
registerForRelevantCardEvents(robot)
}
function registerForRelevantCardEvents (robot) {
robot.on(['project_card.created', 'project_card.moved'], context => processChangedProjectCard(robot, context, undefined))
}
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) {
const { github, payload } = context
const repo = payload.repository
const repoInfo = context.repo()
if (!repoInfo.repo) {
robot.log.debug(`${botName} - Repository info is not present in payload, ignoring`, context)
return
}
if (!card) {
card = payload.project_card
}
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
}
robot.log.debug(`${botName} - Processing changed project card`, card)
if (card.note) {
robot.log.trace(`${botName} - Card is a note, ignoring`)
return
}
const projectBoardName = projectBoardConfig['name']
const kickoffColumnName = automatedTestsConfig['kickoff-column-name']
if (repo.full_name !== automatedTestsConfig['repo-full-name']) {
robot.log.trace(`${botName} - Pull request project doesn't match watched repo, exiting`, repo.full_name, automatedTestsConfig['repo-full-name'])
return
}
let targetKickoffColumn
try {
const columnPayload = await github.projects.getColumn({ column_id: card.column_id })
if (columnPayload.data.name !== kickoffColumnName) {
robot.log.trace(`${botName} - Card column name doesn't match watched column name, exiting`, columnPayload.data.name, kickoffColumnName)
return
}
targetKickoffColumn = columnPayload.data
} catch (error) {
robot.log.warn(`${botName} - Error while fetching project column`, card.column_id, error)
return
}
try {
const projectId = last(targetKickoffColumn.project_url.split('/'), -1)
const projectPayload = await github.projects.get({ project_id: projectId })
const project = projectPayload.data
if (project.name !== projectBoardName) {
robot.log.trace(`${botName} - Project board name doesn't match watched project board, exiting`, project.name, projectBoardName)
return
}
} catch (error) {
robot.log.warn(`${botName} - Error while fetching project column`, card.column_id, error)
return
}
const prNumber = last(card.content_url.split('/'), -1)
const fullJobName = automatedTestsConfig['job-full-name']
await processPullRequest(context, robot, { ...repoInfo, number: prNumber }, fullJobName)
}
async function processPullRequest (context, robot, prInfo, fullJobName) {
const { github } = context
// Remove the PR from the pending PR list, if it is there
pendingPullRequests.delete(prInfo.number)
robot.log.debug(`${botName} - Removed PR #${prInfo.number} from queue, current queue length is ${pendingPullRequests.size}`)
try {
// 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
}
if (pullRequest.state === 'closed') {
robot.log.info(`${botName} - PR is closed, discarded`, prInfo)
return
}
const statusContext = 'jenkins/prs/android-e2e'
const currentStatus = await gitHubHelpers.getPullRequestCurrentStatusForContext(context, statusContext, pullRequest)
switch (currentStatus) {
case undefined:
case 'pending':
case 'failure':
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)
return
case 'error':
robot.log.debug(`${botName} - Status for ${statusContext} is '${currentStatus}', exiting`, prInfo)
return
case 'success':
robot.log.debug(`${botName} - Status for ${statusContext} is '${currentStatus}', proceeding`, prInfo)
break
default:
robot.log.warn(`${botName} - Status for ${statusContext} is '${currentStatus}', ignoring`, prInfo)
return
}
} catch (err) {
robot.log.error(`Couldn't calculate the PR approval state: ${err}`, prInfo)
return
}
try {
const args = { parameters: { pr_id: prInfo.number, apk: `--apk=${prInfo.number}.apk` } }
if (process.env.DRY_RUN) {
robot.log(`${botName} - Would start ${fullJobName} job in Jenkins`, prInfo, args)
} else {
robot.log(`${botName} - Starting ${fullJobName} job in Jenkins`, prInfo, args)
const buildId = await jenkins.job.build(fullJobName, args)
robot.log(`${botName} - Started job in Jenkins`, prInfo, buildId)
}
} catch (error) {
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)
}
}
async function checkPendingPullRequests (robot) {
const _pendingPullRequests = pendingPullRequests.clone()
robot.log.debug(`${botName} - Processing ${_pendingPullRequests.size} pending PRs`)
for (const { context, prInfo, fullJobName } of _pendingPullRequests.values()) {
await processPullRequest(context, robot, prInfo, fullJobName)
}
robot.log.debug(`${botName} - Finished processing ${_pendingPullRequests.size} pending PRs`)
}