Skip to content

Commit

Permalink
Merge pull request #68 from bobvanderlinden/pr-report-status
Browse files Browse the repository at this point in the history
Add status reporting
  • Loading branch information
probot-auto-merge[bot] committed Oct 13, 2018
2 parents 8db2563 + 6ba01bd commit ab9daad
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 68 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
27 changes: 24 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Raven from 'raven'
import { RepositoryWorkers } from './repository-workers'
import sentryStream from 'bunyan-sentry-stream'
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 @@ -104,9 +105,21 @@ export = (app: Application) => {

app.on([
'check_run.created',
'check_run.rerequested',
'check_run.requested_action',
'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 handlePullRequests(app, context, {
owner: context.payload.repository.owner.login,
Expand All @@ -115,7 +128,6 @@ export = (app: Application) => {
})

app.on([
'check_suite.completed',
'check_suite.requested',
'check_suite.rerequested'
], async context => {
Expand All @@ -124,4 +136,13 @@ export = (app: Application) => {
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)
}
54 changes: 54 additions & 0 deletions src/status-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PullRequestContext } from './pull-request-handler'
import { PullRequestInfo } from './models'
import { ChecksCreateParams } from '@octokit/rest'

import myAppId from './myappid'

export async function updateStatusReportCheck (
context: PullRequestContext,
pullRequestInfo: PullRequestInfo,
title: string,
summary: string
) {
const myCheckRun = pullRequestInfo.checkRuns
.filter(checkRun => checkRun.app.id === myAppId)[0]

const checkOptions: {
conclusion: 'neutral',
status: 'completed',
name: string,
completed_at: string,
output: ChecksCreateParams['output'],
owner: string,
repo: string
} = {
conclusion: 'neutral',
status: 'completed',
name: 'auto-merge',
completed_at: new Date().toISOString(),
output: {
title,
summary
},
owner: pullRequestInfo.baseRef.repository.owner.login,
repo: pullRequestInfo.baseRef.repository.name
}

if (myCheckRun) {
// Whenever we find an existing check_run from this app,
// we will update that check_run.
await context.github.checks.update({
check_run_id: myCheckRun.id.toString(),
...checkOptions
})
} else if (context.config.reportStatus) {
// Whenever we did not find an existing check_run we will
// only create a new one if reportStatus is enabled
// in their repository.

await context.github.checks.create({
head_sha: pullRequestInfo.headRefOid,
...checkOptions
})
}
}
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AnyResponse } from '@octokit/rest'
import { PullRequestInfo } from './models'

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

export type DeepPartial<T> = { [Key in keyof T]?: DeepPartial<T[Key]>; }
export type ElementOf<TArray> = TArray extends Array<infer TElement> ? TElement : never

Expand Down
Loading

0 comments on commit ab9daad

Please sign in to comment.