Add support for installing as a GitHub app. Part of #1

This commit is contained in:
Pedro Pombeiro 2018-01-17 18:49:14 +01:00
parent abb544feed
commit 6bc6142fad
No known key found for this signature in database
GPG Key ID: A65DEB11E4BBC647
6 changed files with 197 additions and 44 deletions

5
package-lock.json generated
View File

@ -1192,6 +1192,11 @@
} }
} }
}, },
"jwt-simple": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/jwt-simple/-/jwt-simple-0.5.1.tgz",
"integrity": "sha1-eeoBiRth3mto4T5nwLS1vak3spQ="
},
"lodash": { "lodash": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",

View File

@ -21,7 +21,8 @@
"hubot-rules": "^0.1.2", "hubot-rules": "^0.1.2",
"hubot-scripts": "^2.17.2", "hubot-scripts": "^2.17.2",
"hubot-shipit": "^0.2.1", "hubot-shipit": "^0.2.1",
"hubot-slack": "^4.4.0" "hubot-slack": "^4.4.0",
"jwt-simple": "^0.5.1"
}, },
"engines": { "engines": {
"node": "0.10.x" "node": "0.10.x"

View File

@ -1,6 +1,19 @@
# Description: # Description:
# Script that listens to new GitHub pull requests # Script that listens to new GitHub pull requests
# and assigns them to the REVIEW column on the "Pipeline for QA" project # and assigns them to the REVIEW column on the "Pipeline for QA" project
#
# Dependencies:
# github: "^13.1.0"
# hubot-github-webhook-listener: "^0.9.1"
# hubot-slack: "^4.4.0"
#
# Notes:
# The hard-coded names for the project board and review column are just below.
# These could be read from a config file (e.g. YAML)
# TODO: Rewrite this file with ES6 to benefit from async/await
#
# Author:
# PombeirP
projectBoardName = "Pipeline for QA" projectBoardName = "Pipeline for QA"
reviewColumnName = "REVIEW" reviewColumnName = "REVIEW"
@ -8,32 +21,32 @@ notifyRoomName = "core"
module.exports = (robot) -> module.exports = (robot) ->
context = require("./github-context.coffee") context = require('./github-context.coffee')
robot.on "github-repo-event", (repo_event) -> robot.on "github-repo-event", (repo_event) ->
githubPayload = repo_event.payload githubPayload = repo_event.payload
switch(repo_event.eventType) switch(repo_event.eventType)
when "pull_request" when "pull_request"
context.initialize(robot, robot.brain.get "github-app_id")
# Make sure we don't listen to our own messages # Make sure we don't listen to our own messages
return if context.equalsRobotName(robot, githubPayload.pull_request.user.login) return if context.equalsRobotName(robot, githubPayload.pull_request.user.login)
return console.error "No Github token provided to Hubot" unless process.env.HUBOT_GITHUB_TOKEN
action = githubPayload.action action = githubPayload.action
if action == "opened" if action == "opened"
# A new PR was opened # A new PR was opened
context.initialize() assignPullRequestToReview context.github(), githubPayload, robot
assignPullRequestToReview context.github, githubPayload, robot
assignPullRequestToReview = (github, githubPayload, robot) -> assignPullRequestToReview = (github, githubPayload, robot) ->
ownerName = githubPayload.repository.owner.login ownerName = githubPayload.repository.owner.login
repoName = githubPayload.repository.name repoName = githubPayload.repository.name
prNumber = githubPayload.pull_request.number prNumber = githubPayload.pull_request.number
robot.logger.info "assignPullRequestToReview - Handling Pull Request ##{prNumber} on repo #{ownerName}/#{repoName}" robot.logger.info "assignPullRequestToReview - " +
"Handling Pull Request ##{prNumber} on repo #{ownerName}/#{repoName}"
# Fetch repo projects # Fetch repo projects
# TODO: The repo project and project column info should be cached in order to improve performance and reduce roundtrips # TODO: The repo project and project column info should be cached
# in order to improve performance and reduce roundtrips
github.projects.getRepoProjects { github.projects.getRepoProjects {
owner: ownerName, owner: ownerName,
repo: repoName, repo: repoName,
@ -73,7 +86,7 @@ assignPullRequestToReview = (github, githubPayload, robot) ->
column_id: column.id, column_id: column.id,
content_type: 'PullRequest', content_type: 'PullRequest',
content_id: githubPayload.pull_request.id content_id: githubPayload.pull_request.id
}, (err, ghcard) -> }, (err, ghcard) ->
if err if err
robot.logger.error "Couldn't create project card for the PR: #{err}", robot.logger.error "Couldn't create project card for the PR: #{err}",
column.id, githubPayload.pull_request.id column.id, githubPayload.pull_request.id
@ -86,7 +99,6 @@ assignPullRequestToReview = (github, githubPayload, robot) ->
"Moved PR #{githubPayload.pull_request.number} to " + "Moved PR #{githubPayload.pull_request.number} to " +
"#{reviewColumnName} in #{projectBoardName} project" "#{reviewColumnName} in #{projectBoardName} project"
findProject = (projects, name) -> findProject = (projects, name) ->
for idx, project of projects for idx, project of projects
return project if project.name == name return project if project.name == name

View File

@ -1,36 +1,81 @@
# Description: # Description:
# Script that keeps GitHub-related context to be shared among scripts # Script that keeps GitHub-related context to be shared among scripts
#
# Dependencies:
# github: "^13.1.0"
# jwt-simple: "^0.5.1"
#
# Author:
# PombeirP
GitHubApi = require("github") GitHubApi = require('github')
RegExp cachedRobotNameRegex = null RegExp cachedRobotNameRegex = null
initialized = false initialized = false
github = new GitHubApi { version: "3.0.0" } githubAPI = new GitHubApi { version: "3.0.0" }
module.exports.github = github module.exports = {
module.exports.initialize = -> github: -> githubAPI
initialize: (robot, integrationID) ->
return if initialized return if initialized
initialized = true token = robot.brain.get('github-token')
github.authenticate({ if token
type: "token", initialized = true
token: process.env.HUBOT_GITHUB_TOKEN process.env.HUBOT_GITHUB_TOKEN = token
robot.logger.debug "Reused cached GitHub token"
githubAPI.authenticate({ type: 'token', token: token })
return
pemFilePath = './status-github-bot.pem'
jwt = require('jwt-simple')
# Private key contents
privateKey = ''
try
fs = require('fs')
privateKey = fs.readFileSync pemFilePath
catch err
robot.logger.error "Couldn't read #{pemFilePath} file contents: #{err}"
return
now = Math.round(Date.now() / 1000)
# Generate the JWT
payload = {
# issued at time
iat: now,
# JWT expiration time (10 minute maximum)
exp: now + (1 * 60),
# GitHub App's identifier
iss: integrationID
}
jwt = jwt.encode(payload, privateKey, 'RS256')
githubAPI.authenticate({
type: 'integration',
token: jwt
}) })
robot.logger.debug "Configured integration authentication with JWT", jwt
module.exports.equalsRobotName = (robot, str) -> initialized = true
return module.exports.getRegexForRobotName(robot).test(str)
module.exports.getRegexForRobotName = (robot) -> equalsRobotName: (robot, str) ->
# This comes straight out of Hubot's Robot.coffee return getRegexForRobotName(robot).test(str)
# - they didn't get a nice way of extracting that method though }
if !cachedRobotNameRegex
name = robot.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
if robot.alias getRegexForRobotName = (robot) ->
alias = robot.alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') # This comes straight out of Hubot's Robot.coffee
namePattern = "^\\s*[@]?(?:#{alias}|#{name})" # - they didn't get a nice way of extracting that method though
else if !cachedRobotNameRegex
namePattern = "^\\s*[@]?#{name}" name = robot.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
cachedRobotNameRegex = new RegExp(namePattern, 'i')
return cachedRobotNameRegex if robot.alias
alias = robot.alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
namePattern = "^\\s*[@]?(?:#{alias}|#{name})"
else
namePattern = "^\\s*[@]?#{name}"
cachedRobotNameRegex = new RegExp(namePattern, 'i')
return cachedRobotNameRegex

View File

@ -0,0 +1,76 @@
# Description:
# Script that handles the installation of the GitHub app
#
# Dependencies:
# github: "^13.1.0"
# hubot-github-webhook-listener: "^0.9.1"
#
# Author:
# PombeirP
module.exports = (robot) ->
context = require('./github-context.coffee')
robot.on "github-repo-event", (repo_event) ->
githubPayload = repo_event.payload
switch(repo_event.eventType)
when "integration_installation"
# Make sure we don't listen to our own messages
return if context.equalsRobotName(robot, githubPayload.sender.login)
action = githubPayload.action
switch action
when "created"
# App was installed on an organization
robot.logger.info "Initializing installation for app with ID " +
"#{githubPayload.installation.app_id} and " +
"installation ID #{githubPayload.installation.id}"
robot.brain.set 'github-app_install-payload', JSON.stringify(githubPayload)
robot.brain.set 'github-app_id', githubPayload.installation.app_id
robot.brain.set 'github-app_repositories',
(x.full_name for x in githubPayload.repositories).join()
context.initialize(robot, githubPayload.installation.app_id)
perms = githubPayload.installation.permissions
robot.logger.error formatPermMessage('repository_projects', 'write') unless perms.repository_projects == 'write'
robot.logger.error formatPermMessage('metadata', 'read') unless perms.metadata == 'read'
robot.logger.error formatPermMessage('issues', 'read') unless perms.issues == 'read'
robot.logger.error formatPermMessage('pull_requests', 'write') unless perms.pull_requests == 'write'
robot.logger.error "Please enable 'pull_request' events " +
"in the app configuration on github.com" unless 'pull_request' in githubPayload.installation.events
createAccessToken robot, context.github(), githubPayload.installation.id
when "deleted"
# App was uninstalled from an organization
robot.logger.info "Removing installation for app " +
"with ID #{githubPayload.installation.app_id} and " +
"installation ID #{githubPayload.installation.id}"
robot.brain.set 'github-app_id', null
robot.brain.set 'github-app_install-payload', null
robot.brain.set 'github-app_repositories', null
robot.brain.set 'github-token', null
process.env.HUBOT_GITHUB_TOKEN = null
createAccessToken = (robot, github, id) ->
github.apps.createInstallationToken { installation_id: id }, (err, response) ->
if err
robot.logger.error "Couldn't create installation token: #{err}", id
return
console.error response.data.token
robot.brain.set 'github-token', response.data.token
# TODO: Set Redis expiration date to value from response.data.expires_at
process.env.HUBOT_GITHUB_TOKEN = response.data.token
github.authenticate({
type: 'token',
token: response.data.token
})
formatPermMessage = (permName, perm) ->
"Please enable '#{permName}' #{perm} permission in the app configuration on github.com"

View File

@ -1,33 +1,43 @@
# Description: # Description:
# Script that listens to new GitHub pull requests # Script that listens to new GitHub pull requests
# and assigns them to the REVIEW column on the "Pipeline for QA" project # and greets the user if it is their first PR on the repo
#
# Dependencies:
# github: "^13.1.0"
# hubot-github-webhook-listener: "^0.9.1"
#
# Notes:
# TODO: Rewrite this file with ES6 to benefit from async/await
#
# Author:
# PombeirP
module.exports = (robot) -> module.exports = (robot) ->
context = require("./github-context.coffee") context = require('./github-context.coffee')
robot.on "github-repo-event", (repo_event) -> robot.on "github-repo-event", (repo_event) ->
githubPayload = repo_event.payload githubPayload = repo_event.payload
switch(repo_event.eventType) switch(repo_event.eventType)
when "pull_request" when "pull_request"
context.initialize(robot, robot.brain.get "github-app_id")
# Make sure we don't listen to our own messages # Make sure we don't listen to our own messages
return if context.equalsRobotName(robot, githubPayload.pull_request.user.login) return if context.equalsRobotName(robot, githubPayload.pull_request.user.login)
return console.error "No Github token provided to Hubot" unless process.env.HUBOT_GITHUB_TOKEN
action = githubPayload.action action = githubPayload.action
if action == "opened" if action == "opened"
# A new PR was opened # A new PR was opened
context.initialize() greetNewContributor context.github(), githubPayload, robot
greetNewContributor context.github, githubPayload, robot
greetNewContributor = (github, githubPayload, robot) -> greetNewContributor = (github, githubPayload, robot) ->
welcomeMessage = "Thanks for making your first PR here!" # TODO: Read the welcome message from a (per-repo?) file (e.g. status-react.welcome-msg.md) # TODO: Read the welcome message from a (per-repo?) file (e.g. status-react.welcome-msg.md)
welcomeMessage = "Thanks for making your first PR here!"
ownerName = githubPayload.repository.owner.login ownerName = githubPayload.repository.owner.login
repoName = githubPayload.repository.name repoName = githubPayload.repository.name
prNumber = githubPayload.pull_request.number prNumber = githubPayload.pull_request.number
robot.logger.info "greetNewContributor - Handling Pull Request ##{prNumber} on repo #{ownerName}/#{repoName}" robot.logger.info "greetNewContributor - " +
"Handling Pull Request ##{prNumber} on repo #{ownerName}/#{repoName}"
github.issues.getForRepo { github.issues.getForRepo {
owner: ownerName, owner: ownerName,
@ -48,8 +58,12 @@ greetNewContributor = (github, githubPayload, robot) ->
number: prNumber, number: prNumber,
body: welcomeMessage body: welcomeMessage
}, (err, result) -> }, (err, result) ->
if err if err
robot.logger.error "Couldn't fetch the github projects for repo: #{err}", ownerName, repoName unless err.code == 404 robot.logger.error("Couldn't fetch the github projects for repo: #{err}",
robot.logger.info "Commented on PR with welcome message", ownerName, repoName ownerName, repoName) unless err.code == 404
return
robot.logger.info "Commented on PR with welcome message", ownerName, repoName
else else
robot.logger.debug "This is not the user's first PR on the repo, ignoring", ownerName, repoName, githubPayload.pull_request.user.login robot.logger.debug(
"This is not the user's first PR on the repo, ignoring",
ownerName, repoName, githubPayload.pull_request.user.login)