Skip to content

Commit

Permalink
Merge d925d1e into b593d3c
Browse files Browse the repository at this point in the history
  • Loading branch information
bobvanderlinden committed Oct 13, 2018
2 parents b593d3c + d925d1e commit 7c1b84c
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 78 deletions.
2 changes: 2 additions & 0 deletions src/conditions/blockingChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { ConditionConfig } from './../config'
import { PullRequestInfo } from '../models'
import { ConditionResult } from '../condition'
import { groupByLastMap } from '../utils'
import myAppId from '../myappid'

export default function doesNotHaveBlockingChecks (
config: ConditionConfig,
pullRequestInfo: PullRequestInfo
): ConditionResult {
const checkRuns = pullRequestInfo.checkRuns
.filter(checkRun => checkRun.app.id !== myAppId)
const allChecksCompleted = checkRuns.every(
checkRun => checkRun.status === 'completed'
)
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type Config = {
updateBranch: boolean,
deleteBranchAfterMerge: boolean,
mergeMethod: 'merge' | 'rebase' | 'squash'
reportStatus: boolean
} & ConditionConfig

export const defaultRuleConfig: ConditionConfig = {
Expand All @@ -56,6 +57,7 @@ export const defaultConfig: Config = {
updateBranch: false,
deleteBranchAfterMerge: false,
mergeMethod: 'merge',
reportStatus: false,
...defaultRuleConfig
}

Expand Down Expand Up @@ -86,6 +88,7 @@ const configDecoder: Decoder<Config> = object({
blockingTitleRegex: optional(string()),
updateBranch: boolean(),
deleteBranchAfterMerge: boolean(),
reportStatus: boolean(),
mergeMethod: oneOf(
constant<'merge'>('merge'),
constant<'rebase'>('rebase'),
Expand Down
22 changes: 21 additions & 1 deletion src/github-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,31 @@ export interface PullRequestReference extends RepositoryReference {
}

export interface CheckRun {
id: number,
name: string
head_sha: string
external_id: string
status: 'queued' | 'in_progress' | 'completed'
conclusion: 'success' | 'failure' | 'neutral' | 'cancelled' | 'timed_out' | 'action_required'
conclusion: 'success' | 'failure' | 'neutral' | 'cancelled' | 'timed_out' | 'action_required',
app: {
id: number,
owner: {
login: string
}
name: string
},
pull_requests: Array<{
id: number,
number: number,
head: {
ref: string,
sha: string
},
base: {
ref: string,
sha: string
}
}>
}

export interface PullRequestQueryResult {
Expand Down
81 changes: 48 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { HandlerContext } from './models'
import Raven from 'raven'
import { RepositoryWorkers } from './repository-workers'
import sentryStream from 'bunyan-sentry-stream'
import { PullRequestReference } from './github-models'
import { RepositoryReference, PullRequestReference } from './github-models'
import myAppId from './myappid'

async function getHandlerContext (options: {app: Application, context: Context}): Promise<HandlerContext> {
const config = await loadConfig(options.context)
Expand Down Expand Up @@ -72,6 +73,18 @@ export = (app: Application) => {
console.error(`Error while processing pull request ${pullRequestName}:`, error)
}

async function handlePullRequests (app: Application, context: Context, repository: RepositoryReference, headSha: string, pullRequestNumbers: number[]) {
await useHandlerContext({ app, context }, async (handlerContext) => {
for (let pullRequestNumber of pullRequestNumbers) {
repositoryWorkers.queue(handlerContext, {
owner: repository.owner,
repo: repository.repo,
number: pullRequestNumber
})
}
})
}

app.on([
'pull_request.opened',
'pull_request.edited',
Expand All @@ -84,50 +97,52 @@ export = (app: Application) => {
'pull_request_review.edited',
'pull_request_review.dismissed'
], async context => {
await useHandlerContext({ app, context }, async (handlerContext) => {
repositoryWorkers.queue(handlerContext, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
number: context.payload.pull_request.number
})
})
await handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name
}, context.payload.pull_request.head_sha, [context.payload.pull_request.number])
})

app.on([
'check_run.created',
'check_run.completed'
], async context => {
if (context.payload.check_run.check_suite.app.id === myAppId) {
return
}

await handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name
}, context.payload.check_run.head_sha, context.payload.check_run.pull_requests.map((pullRequest: any) => pullRequest.number))
})

app.on([
'check_run.rerequested',
'check_run.requested_action'
], async context => {
await Raven.context({
extra: {
event: context.event
}
}, async () => {
await useHandlerContext({ app, context }, async (handlerContext) => {
for (const pullRequest of context.payload.check_run.pull_requests) {
repositoryWorkers.queue(handlerContext, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
number: pullRequest.number
})
}
})
})
await handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name
}, context.payload.check_run.head_sha, context.payload.check_run.pull_requests.map((pullRequest: any) => pullRequest.number))
})

app.on([
'check_suite.completed',
'check_suite.requested',
'check_suite.rerequested'
], async context => {
await useHandlerContext({ app, context }, async (handlerContext) => {
for (const pullRequest of context.payload.check_suite.pull_requests) {
repositoryWorkers.queue(handlerContext, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
number: pullRequest.number
})
}
})
await handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name
}, context.payload.check_suite.head_sha, context.payload.check_suite.pull_requests.map((pullRequest: any) => pullRequest.number))
})

app.on([
'check_suite.completed'
], async context => {
await handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name
}, context.payload.check_suite.head_sha, context.payload.check_suite.pull_requests.map((pullRequest: any) => pullRequest.number))
})
}
11 changes: 11 additions & 0 deletions src/myappid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function getMyAppId () {
if (process.env.APP_ID) {
return parseInt(process.env.APP_ID, 10)
}
if (process.env.NODE_ENV === 'production') {
throw new Error('No APP_ID defined!')
}
return 1
}

export default getMyAppId()
111 changes: 93 additions & 18 deletions src/pull-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { result } from './utils'
import { getPullRequestStatus, PullRequestStatus } from './pull-request-status'
import { queryPullRequest } from './pull-request-query'
import { requiresBranchUpdate } from './pull-request-uptodate'
import { updateStatusReportCheck } from './status-report'

export interface PullRequestContext extends HandlerContext {
reschedulePullRequest: () => void
Expand Down Expand Up @@ -51,45 +52,106 @@ export async function handlePullRequest (

export type PullRequestAction = 'reschedule' | 'update_branch' | 'merge' | 'delete_branch'
export type PullRequestActions
= []
= (
[]
| ['reschedule']
| ['update_branch', 'reschedule']
| ['merge']
| ['merge', 'delete_branch']
) & Array<PullRequestAction>

export type PullRequestPlan = {
code: 'mergeable_unknown' | 'pending_condition' | 'failing_condition' | 'out_of_date_on_fork' | 'update_branch' | 'merge_and_delete' | 'merge',
message: string,
actions: PullRequestActions
}

function getChecksMarkdown (pullRequestStatus: PullRequestStatus) {
return Object.entries(pullRequestStatus)
.map(([name, result]) => {
switch (result.status) {
case 'success':
return `* ✓ \`${name}\``
case 'pending':
return `* ○ \`${name}\`${result.message && `: ${result.message}`}`
case 'fail':
return `* ✘ \`${name}\`: ${result.message}`
default:
throw new Error(`Unknown status in result: ${JSON.stringify(result)}`)
}
})
.join('\n')
}

/**
* Determines which actions to take based on the pull request and the condition results
*/
export function getPullRequestActions (
export function getPullRequestPlan (
context: HandlerContext,
pullRequestInfo: PullRequestInfo,
pullRequestStatus: PullRequestStatus
): PullRequestActions {
): PullRequestPlan {
const { config } = context
const pending = Object.values(pullRequestStatus)
.some(conditionResult => conditionResult.status === 'pending')
const success = Object.values(pullRequestStatus)
.every(conditionResult => conditionResult.status === 'success')
const pendingConditions = Object.entries(pullRequestStatus)
.filter(([conditionName, conditionResult]) => conditionResult.status === 'pending')
const failingConditions = Object.entries(pullRequestStatus)
.filter(([conditionName, conditionResult]) => conditionResult.status === 'fail')

if (pending) {
return ['reschedule']
if (pullRequestInfo.mergeable === 'UNKNOWN') {
return {
code: 'mergeable_unknown',
message: `GitHub is determining whether the pull request is mergeable`,
actions: ['reschedule']
}
}

if (!success) {
return []
if (pendingConditions.length > 0) {
return {
code: 'pending_condition',
message: `There are pending conditions:\n\n${getChecksMarkdown(pullRequestStatus)}`,
actions: ['reschedule']
}
}

if (failingConditions.length > 0) {
return {
code: 'failing_condition',
message: `There are failing conditions:\n\n${getChecksMarkdown(pullRequestStatus)}`,
actions: []
}
}

// If the pull request is not up-to-date failed and we have updateBranch enabled,
// update the branch of the PR.
if (requiresBranchUpdate(pullRequestInfo) && config.updateBranch) {
return isInFork(pullRequestInfo)
? []
: ['update_branch', 'reschedule']
if (isInFork(pullRequestInfo)) {
return {
code: 'out_of_date_on_fork',
message: 'The pull request is out-of-date, but the head is located in another repository',
actions: []
}
} else {
return {
code: 'update_branch',
message: 'The pull request is out-of-date. Will update it now.',
actions: ['update_branch', 'reschedule']
}
}
}

return config.deleteBranchAfterMerge && !isInFork(pullRequestInfo)
? ['merge', 'delete_branch']
: ['merge']
if (config.deleteBranchAfterMerge && !isInFork(pullRequestInfo)) {
return {
code: 'merge_and_delete',
message: 'Will merge the pull request and delete its branch',
actions: ['merge', 'delete_branch']
}
} else {
return {
code: 'merge',
message: 'Will merge the pull request',
actions: ['merge']
}
}
}

function isInFork (pullRequestInfo: PullRequestInfo): boolean {
Expand Down Expand Up @@ -193,7 +255,20 @@ export async function handlePullRequestStatus (
pullRequestInfo: PullRequestInfo,
pullRequestStatus: PullRequestStatus
) {
const actions = getPullRequestActions(context, pullRequestInfo, pullRequestStatus)
const plan = getPullRequestPlan(context, pullRequestInfo, pullRequestStatus)

await updateStatusReportCheck(context, pullRequestInfo,
plan.actions.some(action => action === 'merge')
? 'Merging'
: plan.actions.some(action => action === 'update_branch')
? 'Updating branch'
: plan.actions.some(action => action === 'reschedule')
? 'Waiting'
: 'Not merging',
plan.message
)

const { actions } = plan
context.log.debug('Actions:', actions)
await executeActions(context, pullRequestInfo, actions)
}
Loading

0 comments on commit 7c1b84c

Please sign in to comment.