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,
@ -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,27 +1,72 @@
# 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
token = robot.brain.get('github-token')
if token
initialized = true initialized = true
github.authenticate({ process.env.HUBOT_GITHUB_TOKEN = token
type: "token", robot.logger.debug "Reused cached GitHub token"
token: process.env.HUBOT_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) ->
return getRegexForRobotName(robot).test(str)
}
getRegexForRobotName = (robot) ->
# This comes straight out of Hubot's Robot.coffee # This comes straight out of Hubot's Robot.coffee
# - they didn't get a nice way of extracting that method though # - they didn't get a nice way of extracting that method though
if !cachedRobotNameRegex if !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,
@ -49,7 +59,11 @@ greetNewContributor = (github, githubPayload, robot) ->
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}",
ownerName, repoName) unless err.code == 404
return
robot.logger.info "Commented on PR with welcome message", ownerName, repoName 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)