Skip to content

Commit

Permalink
feature: add link issues check (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWoodward authored Feb 7, 2020
1 parent 61a8de8 commit 6fc59f7
Show file tree
Hide file tree
Showing 15 changed files with 833 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ When someone mentions a channel in a message, that channel gets a link back to t
This is useful for notifying multiple groups, or casually asking for help.

### Other features
- [#74](https://github.com/openstax/staxly/pull/74) verify that PR descriptions contain a link to their issue
- [#73](https://github.com/openstax/staxly/pull/73) automatically update PRs when their base branch has new commits (that will naturally trigger the checks to run again)
- [#8](https://github.com/openstax/staxly/pull/8) Reminder to add a Pull Request Reviewer
![image](https://user-images.githubusercontent.com/253202/35791407-c04a6d56-0a15-11e8-8790-c2d0b4a73d0b.png)

Expand Down
2 changes: 1 addition & 1 deletion app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ default_permissions:

# Checks on code.
# https://developer.github.com/v3/apps/permissions/#permission-on-checks
# checks: read
checks: write

# Repository contents, commits, branches, downloads, releases, and merges.
# https://developer.github.com/v3/apps/permissions/#permission-on-contents
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = (robot) => {

require('./changelog')(robot)
require('./merge-bases')(robot)
require('./link-issues')(robot)

// Addons that are noisy during tests
if (!IGNORE_FOR_TESTING) {
Expand Down
74 changes: 74 additions & 0 deletions src/link-issues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Merge base into PR branch whenever updated
const getConnectedIssueForPR = require('./utils/getConnectedIssueForPR')
const addConnectedPRToIssue = require('./utils/addConnectedPRToIssue')
const removeConnectedPRFromIssue = require('./utils/removeConnectedPRFromIssue')

const repoWhitelist = [
'testrepo',
'rex-web',
'testing-stuff'
]

const name = 'has issue link'

module.exports = (robot) => {
const logger = robot.log.child({name: 'link-issues-check'})
robot.on([
'pull_request.opened',
'pull_request.edited',
'pull_request.synchronize'
], checkPR)

async function checkPR (context) {
const pullRequest = context.payload.pull_request
if (!repoWhitelist.includes(context.payload.repository.name)) {
return
}
logger.info(`checking pr ${pullRequest.number}`)

const check = await context.github.checks.create(context.repo({
name,
head_sha: context.payload.pull_request.head.sha,
status: 'in_progress',
output: {title: name, summary: 'processing'}
}))

const linkedIssueParams = getConnectedIssueForPR(pullRequest)
const linkedIssue = linkedIssueParams && await context.github.issues.get(linkedIssueParams)
.then(response => response.data)
.catch(() => null)

logger.info(`pr ${pullRequest.number} ${linkedIssue ? 'passed' : 'failed'}`)

await context.github.checks.update(context.repo({
check_run_id: check.data.id,
status: 'completed',
conclusion: linkedIssue ? 'success' : 'failure',
output: linkedIssue
? {
title: 'all is as it should be',
summary: 'good job linking to that issue! :+1:'
}
: {
title: 'please add an issue reference',
summary: 'please add a link to the issue this PR is for to the PR description',
text: 'for example `for: openstax/cool-repo#5`. `for: <github or zenhub url>` also work'
}
}))

if (context.payload.action === 'edited' && context.payload.changes.body) {
const previousIssueParams = getConnectedIssueForPR({...pullRequest, body: context.payload.changes.body.from})
const previousIssue = previousIssueParams && await context.github.issues.get(previousIssueParams)
.then(response => response.data)
.catch(() => null)

if (previousIssue && (!linkedIssue || previousIssue.number !== linkedIssue.number)) {
await removeConnectedPRFromIssue(context.github, previousIssueParams, previousIssue, pullRequest)
}
}

if (linkedIssue) {
await addConnectedPRToIssue(context.github, linkedIssueParams, linkedIssue, pullRequest)
}
};
}
34 changes: 34 additions & 0 deletions src/utils/addConnectedPRToIssue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const {prBlockRegex} = require('./connectedPRRegexes')
const getConnectedPRsForIssue = require('./getConnectedPRsForIssue')

/*
* @argument context.github
* @argument IssueData
* @argument PullRequestData
*
* @returns Promise<void>
*/
module.exports = (github, issueParams, issue, pullRequest) => {
const prs = getConnectedPRsForIssue(issue)

const pullNumber = pullRequest.number
const repo = pullRequest.base.repo.name
const owner = pullRequest.base.repo.owner.login

const existing = prs.find(pr => Number(pr.pull_number) === pullNumber && pr.repo === repo && pr.owner === owner)

if (existing) {
return
}

const newLink = `\n- [ ] ${owner}/${repo}#${pullNumber}`
const blockMatch = issue.body.match(new RegExp(prBlockRegex, 'i'))
const newBody = blockMatch
? issue.body.replace(blockMatch[0], blockMatch[0] + newLink)
: issue.body + '\n\npull requests:' + newLink

return github.issues.update({
...issueParams,
body: newBody
})
}
23 changes: 23 additions & 0 deletions src/utils/connectedPRRegexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const {
githubRefGroups, githubPullRequestLinkGroups, zenhubLinkGroups,
githubRef, githubPullRequestLink, zenhubLink,
whitespace, beginningOfStringOrNewline
} = require('./regexes')

const anyLink = `((${githubRef})|(${githubPullRequestLink})|(${zenhubLink}))`

const listPrefix = '\\n\\- \\[( |x)\\] '
const prBlockRegex = `${beginningOfStringOrNewline}#* ?\\*{0,2}pull requests:?\\*{0,2}:?(${whitespace}*${listPrefix}${anyLink})*`

const anyLinkGroups = [
githubRefGroups,
githubPullRequestLinkGroups,
zenhubLinkGroups
]

module.exports = {
listPrefix,
anyLinkGroups,
anyLink,
prBlockRegex
}
26 changes: 26 additions & 0 deletions src/utils/getConnectedIssueForPR.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const {
beginningOfStringOrWhitespace, endOfStringOrWhitespace,
githubRefGroups, githubIssueLinkGroups, zenhubLinkGroups
} = require('./regexes')

const targetRegexes = [
`${beginningOfStringOrWhitespace}for: ${githubRefGroups}${endOfStringOrWhitespace}`,
`${beginningOfStringOrWhitespace}for: ${githubIssueLinkGroups}${endOfStringOrWhitespace}`,
`${beginningOfStringOrWhitespace}for: ${zenhubLinkGroups}${endOfStringOrWhitespace}`
]

/*
* @argument PullRequestData
*
* @returns IssueParams | null
*/
module.exports = (pullRequest) => {
const target = targetRegexes.reduce((result, regex) => result || pullRequest.body.match(new RegExp(regex, 'i')), null)

if (target && target.groups) {
const {number, ...params} = target.groups
return {...params, issue_number: number}
}

return null
}
18 changes: 18 additions & 0 deletions src/utils/getConnectedPRsForIssue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const {anyLink, anyLinkGroups, prBlockRegex} = require('./connectedPRRegexes')

/*
* @argument IssueData
*
* @returns PullRequestParams | null
*/
module.exports = (issue) => {
const blockMatch = issue.body.match(new RegExp(prBlockRegex, 'i'))
const links = blockMatch && blockMatch[0].match(new RegExp(anyLink, 'gi'))

return (links || []).map(link => {
const result = anyLinkGroups.reduce((result, regex) => result || link.match(new RegExp(regex, 'i')), null)
return result ? result.groups : null
})
.filter(params => !!params)
.map(({number, ...params}) => ({...params, pull_number: number}))
}
34 changes: 34 additions & 0 deletions src/utils/regexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const whitespace = '(\\s|\\r)'
const beginningOfStringOrNewline = '^(.*[\\n\\r]+)*'
const beginningOfStringOrWhitespace = '^(.*[\\s\\r]+)*'
const endOfStringOrWhitespace = '([\\s\\r]+.*)*$'

/* eslint-disable-next-line */
const githubRefGroups = '(?<owner>[a-z\-]+)\/(?<repo>[a-z\-]+)#(?<number>[0-9]+)'
/* eslint-disable-next-line */
const githubIssueLinkGroups = 'https:\/\/github.com\/(?<owner>[a-z\-]+)\/(?<repo>[a-z\-]+)\/issues\/(?<number>[0-9]+)'
/* eslint-disable-next-line */
const githubPullRequestLinkGroups = 'https:\/\/github.com\/(?<owner>[a-z\-]+)\/(?<repo>[a-z\-]+)\/pulls\/(?<number>[0-9]+)'
/* eslint-disable-next-line */
const zenhubLinkGroups = 'https:\/\/app.zenhub.com\/workspaces\/[0-9a-z\-]+\/issues\/(?<owner>[a-z\-]+)\/(?<repo>[a-z\-]+)\/(?<number>[0-9]+)'

/* eslint-disable-next-line */
const githubRef = '[a-z\-]+\/[a-z\-]+#[0-9]+'
/* eslint-disable-next-line */
const githubPullRequestLink = 'https:\/\/github.com\/[a-z\-]+\/[a-z\-]+\/pulls\/[0-9]+'
/* eslint-disable-next-line */
const zenhubLink = 'https:\/\/app.zenhub.com\/workspaces\/[0-9a-z\-]+\/issues\/[a-z\-]+\/[a-z\-]+\/[0-9]+'

module.exports = {
whitespace,
beginningOfStringOrNewline,
beginningOfStringOrWhitespace,
endOfStringOrWhitespace,
githubRefGroups,
githubIssueLinkGroups,
githubPullRequestLinkGroups,
zenhubLinkGroups,
githubRef,
githubPullRequestLink,
zenhubLink
}
40 changes: 40 additions & 0 deletions src/utils/removeConnectedPRFromIssue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const {anyLink, listPrefix, anyLinkGroups, prBlockRegex} = require('./connectedPRRegexes')

/*
* @argument context.github
* @argument IssueData
* @argument PullRequestData
*
* @returns Promise<void>
*/
module.exports = (github, issueParams, issue, pullRequest) => {
const pullNumber = pullRequest.number
const repo = pullRequest.base.repo.name
const owner = pullRequest.base.repo.owner.login

const blockMatch = issue.body.match(new RegExp(prBlockRegex, 'i'))

if (!blockMatch) {
return
}

const lines = blockMatch[0].match(new RegExp(listPrefix + anyLink, 'gi'))

const linesToRemove = lines.filter(line => {
const match = anyLinkGroups.reduce((result, regex) => result || line.match(new RegExp(regex, 'i')), null)
const params = match && match.groups
return params && Number(params.number) === pullNumber && params.repo === repo && params.owner === owner
})

if (!linesToRemove.length) {
return
}

const newPRBlock = linesToRemove.reduce((result, line) => result.replace(line, ''), blockMatch[0])
const newBody = issue.body.replace(blockMatch[0], newPRBlock)

return github.issues.update({
...issueParams,
body: newBody
})
}
Loading

0 comments on commit 6fc59f7

Please sign in to comment.