235 lines
7.1 KiB
JavaScript
235 lines
7.1 KiB
JavaScript
// Description:
|
|
// GtHub-related helpers
|
|
//
|
|
// Dependencies:
|
|
// github: "^13.1.0"
|
|
//
|
|
// Author:
|
|
// PombeirP
|
|
|
|
module.exports = {
|
|
getPullRequestReviewStates: _getPullRequestReviewStates,
|
|
getReviewApprovalState: _getReviewApprovalState,
|
|
getProjectCardForIssue: _getProjectCardForIssue,
|
|
getOrgProjectByName: _getOrgProjectByName,
|
|
getRepoProjectByName: _getRepoProjectByName,
|
|
getProjectColumnByName: _getProjectColumnByName,
|
|
getPullRequestCurrentStatusForContext: _getPullRequestCurrentStatusForContext
|
|
}
|
|
|
|
async function _getPullRequestReviewStates (github, prInfo) {
|
|
let finalReviewsMap = new Map()
|
|
const ghreviews = await github.paginate(
|
|
github.pullRequests.listReviews({ ...prInfo, per_page: 100 }),
|
|
res => res.data)
|
|
for (const review of ghreviews) {
|
|
switch (review.state) {
|
|
case 'APPROVED':
|
|
case 'CHANGES_REQUESTED':
|
|
case 'PENDING':
|
|
finalReviewsMap.set(review.user.id, review.state)
|
|
break
|
|
}
|
|
}
|
|
|
|
return Array.from(finalReviewsMap.values())
|
|
}
|
|
|
|
async function _getReviewApprovalState (context, robot, prInfo, testedPullRequestLabelName, filterIgnoredStatusContextFn) {
|
|
const { github } = context
|
|
|
|
// Get detailed pull request
|
|
const pullRequestPayload = await github.pullRequests.get(prInfo)
|
|
const pullRequest = pullRequestPayload.data
|
|
context.payload.pull_request = pullRequest
|
|
if (pullRequest.mergeable !== null && pullRequest.mergeable !== undefined && !pullRequest.mergeable) {
|
|
robot.log.debug(`pullRequest.mergeable is ${pullRequest.mergeable}, considering as failed`)
|
|
return 'failed'
|
|
}
|
|
|
|
let state
|
|
switch (pullRequest.mergeable_state) {
|
|
case 'clean':
|
|
if (testedPullRequestLabelName !== null && pullRequest.labels.find(label => label.name === testedPullRequestLabelName)) {
|
|
robot.log.debug(`Pull request is labeled '${testedPullRequestLabelName}', ignoring`)
|
|
return null
|
|
}
|
|
|
|
state = 'approved'
|
|
break
|
|
case 'dirty':
|
|
state = 'failed'
|
|
break
|
|
case 'unstable':
|
|
if (filterIgnoredStatusContextFn) {
|
|
const isSuccess = await _isPullRequestStatusSuccessIgnoringContext(context, filterIgnoredStatusContextFn, pullRequest)
|
|
if (isSuccess) {
|
|
state = 'approved'
|
|
robot.log.debug(`All important statuses are successful, so considering state as ${state}`)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
robot.log.debug(`pullRequest.mergeable_state is ${pullRequest.mergeable_state}, considering state as ${state}`)
|
|
|
|
if (state !== 'approved') {
|
|
return state
|
|
}
|
|
|
|
const threshold = 2 // Minimum number of approvers
|
|
|
|
const finalReviews = await _getPullRequestReviewStates(github, prInfo)
|
|
robot.log.debug(finalReviews)
|
|
|
|
const approvedReviews = finalReviews.filter(reviewState => reviewState === 'APPROVED')
|
|
if (approvedReviews.length >= threshold) {
|
|
const reviewsWithChangesRequested = finalReviews.filter(reviewState => reviewState === 'CHANGES_REQUESTED')
|
|
if (reviewsWithChangesRequested.length === 0) {
|
|
robot.log.debug(`No changes requested, considering state as approved`)
|
|
return 'approved'
|
|
}
|
|
|
|
robot.log.debug(`${reviewsWithChangesRequested.length} changes requested, considering state as changes_requested`)
|
|
return 'changes_requested'
|
|
}
|
|
|
|
robot.log.debug(`Not enough reviewers yet, considering state as awaiting_reviewers`)
|
|
return 'awaiting_reviewers'
|
|
}
|
|
|
|
async function _getProjectCardForIssue (github, columnId, issueUrl) {
|
|
const ghcardsPayload = await github.projects.listCards({ column_id: columnId })
|
|
const ghcard = ghcardsPayload.data.find(c => c.content_url === issueUrl)
|
|
|
|
return ghcard
|
|
}
|
|
|
|
async function _getOrgProjectByName (github, robot, orgName, projectName, botName) {
|
|
if (!projectName) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
// Fetch org projects
|
|
// TODO: The org project and project column info should be cached
|
|
// in order to improve performance and reduce roundtrips
|
|
const ghprojectsPayload = await github.projects.getOrgProjects({
|
|
org: orgName,
|
|
state: 'open'
|
|
})
|
|
|
|
const project = ghprojectsPayload.data.find(p => p.name === projectName)
|
|
if (!project) {
|
|
robot.log.error(`${botName} - Couldn't find project ${projectName} in ${orgName} org`)
|
|
return null
|
|
}
|
|
|
|
robot.log.debug(`${botName} - Fetched ${project.name} project (${project.id})`)
|
|
|
|
return project
|
|
} catch (err) {
|
|
robot.log.error(`${botName} - Couldn't fetch the github projects for org`, orgName, err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function _getRepoProjectByName (github, robot, repoInfo, projectName, botName) {
|
|
if (!projectName) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const ghprojectsPayload = await github.projects.listForRepo({ ...repoInfo, state: 'open' })
|
|
const project = ghprojectsPayload.data.find(p => p.name === projectName)
|
|
if (!project) {
|
|
robot.log.error(`${botName} - Couldn't find project ${projectName} in repo ${repoInfo.owner}/${repoInfo.repo}`)
|
|
return null
|
|
}
|
|
|
|
robot.log.debug(`${botName} - Fetched ${project.name} project (${project.id})`)
|
|
|
|
return project
|
|
} catch (err) {
|
|
robot.log.error(`${botName} - Couldn't fetch the github projects for repo: ${err}`, repoInfo)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function _getProjectColumnByName (github, robot, project, columnName, botName) {
|
|
if (!project) {
|
|
return null
|
|
}
|
|
if (!columnName) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const ghcolumnsPayload = await github.projects.listColumns({ project_id: project.id })
|
|
const column = ghcolumnsPayload.data.find(c => c.name === columnName)
|
|
if (!column) {
|
|
robot.log.error(`${botName} - Couldn't find ${columnName} column in project ${project.name}`)
|
|
return null
|
|
}
|
|
|
|
robot.log.debug(`${botName} - Fetched ${column.name} column (${column.id})`)
|
|
|
|
return column
|
|
} catch (err) {
|
|
robot.log.error(`${botName} - Couldn't fetch the github columns for project: ${err}`, project.id)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function _getPullRequestCurrentStatusForContext (context, statusContext, pullRequest) {
|
|
if (!pullRequest) {
|
|
pullRequest = context.payload.pull_request
|
|
}
|
|
|
|
const { data: { statuses } } = await context.github.repos.getCombinedStatusForRef(context.repo({
|
|
ref: pullRequest.head.sha
|
|
}))
|
|
|
|
return (statuses.find(status => status.context === statusContext) || {}).state
|
|
}
|
|
|
|
async function _isPullRequestStatusSuccessIgnoringContext (context, filterIgnoredStatusContextFn, pullRequest) {
|
|
if (!pullRequest) {
|
|
pullRequest = context.payload.pull_request
|
|
}
|
|
|
|
const statuses = await context.github.paginate(
|
|
context.github.repos.listStatusesForRef(context.repo({
|
|
ref: pullRequest.head.sha,
|
|
per_page: 100
|
|
})),
|
|
res => res.data)
|
|
|
|
const contexts = {}
|
|
for (let i = statuses.length - 1; i >= 0; i--) {
|
|
const status = statuses[i]
|
|
if (filterIgnoredStatusContextFn(status)) {
|
|
contexts[status.context] = status.state
|
|
}
|
|
}
|
|
|
|
let isSuccess = true
|
|
for (const context in contexts) {
|
|
if (contexts.hasOwnProperty(context)) {
|
|
const state = contexts[context]
|
|
|
|
switch (state) {
|
|
case 'pending':
|
|
case 'error':
|
|
isSuccess = false
|
|
break
|
|
}
|
|
|
|
if (!isSuccess) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return isSuccess
|
|
}
|