UNPKG

5.44 kBtext/coffeescriptView Raw
1# Description
2# Let Hubot keep track of who is on AO duty for each team
3#
4# Configuration:
5# HUBOT_REVIEW_NEEDED_LABEL (Hubot messages all AOs when PR gets this label)
6# add GitHub webhook with issues events pointing to <hubot server>:8080/hubot/gh-issues
7#
8# Commands:
9# hubot show|list AOs - displays the current active owner for each team
10# hubot assign [user] as AO for [team] - assign a user as AO for a team
11# hubot I'm AO for [team]- assign yourself as AO for a team
12moment = require 'moment'
13_ = require 'lodash'
14GitHubApi = require 'github'
15
16module.exports = (robot) ->
17
18 robot.brain.data.teams ||= {}
19 robot.brain.data.prsForReview ||= []
20
21 # TODO: move review-* events to their own modules
22 robot.on 'review-needed', (pullRequest) ->
23 pr = new PullRequest(pullRequest)
24 robot.brain.data.prsForReview["#{pr.repo}/#{pr.number}"] = pr
25 message = "Rapid Response needs a review of #{pr.url}"
26 messageAOs(message)
27
28 robot.on 'review-no-longer-needed', (pr) ->
29 delete robot.brain.data.prsForReview["#{pr.repo}/#{pr.number}"]
30 message = "Review no longer needed for #{pr.url}. The PR either was closed or review label was removed."
31 messageAOs(message)
32
33 # TODO: move webhook listener and event emission logic to its own module
34 robot.router.post 'hubot/gh-issues', (req, res) ->
35 robot.logger.info "issue detected via GitHub webook: #{res}"
36 if res.action == 'labeled' && res.label.name == process.env.HUBOT_REVIEW_NEEDED_LABEL
37 robot.logger.info "emitting review-needed event: url: #{res.issue.url}"
38 robot.emit 'review-needed',
39 url: res.issue.url
40 repo: res.repository.full_name
41 number: res.issue.number
42 if reviewNoLongerNeeded res
43 robot.logger.info "emitting review-no-longer-needed event: url: #{res.issue.url}"
44 robot.emit 'review-no-longer-needed',
45 url: res.issue.url
46 repo: res.repository.full_name
47 number: res.issue.number
48
49 reviewNoLongerNeeded = (gitHubIssuesResponse) ->
50 labelRemoved = res.action == 'unlabeled' && res.label.name == process.env.HUBOT_REVIEW_NEEDED_LABEL
51 issueClosed = res.action == 'closed' && _.some(res.labels, (label) -> label.name == process.env.HUBOT_REVIEW_NEEDED_LABEL)
52 labelRemoved || issueClosed
53
54 robot.respond /(list|show) (active owners|AO's|AOs)/i, (msg) ->
55 teams = robot.brain.data['teams']
56 if Object.keys(teams).length == 0
57 response = "Sorry, I'm not keeping track of any teams or their AOs.\n" +
58 "Get started with 'Add <team name> to teams'."
59 return msg.send response
60 aoStatus = (team) ->
61 if team.aoUserId?
62 aoName = robot.brain.userForId(team.aoUserId).name
63 return "#{aoName} has been active owner on #{team.name} for #{moment(team.aoUserAssignedDt).fromNow(true)}"
64 else
65 "* #{team.name} has no active owner! Use: 'Assign <user> as AO for <team>'."
66 aoStatusList = (aoStatus(team) for teamName, team of teams)
67 msg.send "AOs:\n" + aoStatusList.join("\n")
68
69 robot.respond /(list|show) needed reviews/i, (msg) ->
70 prs = robot.brain.data['prsForReview']
71 if Object.keys(prs).length == 0
72 response = "Nothing needs review as far as I know."
73 return msg.send response
74 prDescription = (pr) ->
75 return "Added #{moment(pr.aoUserAssignedDt).fromNow()}: #{pr.url}"
76 prDescriptionList = (prDescription(pr) for prKey, pr of prs)
77 msg.send "PRs in need of review:\n" + prDescriptionList.join("\n")
78
79 robot.respond /add ([a-z0-9 ]+) to teams/i, (msg) ->
80 teamName = msg.match[1]
81 if getTeam(teamName)
82 return msg.send "#{teamName} already being tracked."
83 addTeam(teamName)
84 msg.send "#{teamName} added."
85
86 robot.respond /(delete|remove) ([a-z0-9 ]+) from teams/i, (msg) ->
87 teamName = msg.match[2]
88 if getTeam(teamName)
89 removeTeam(teamName)
90 return msg.send "Removed #{teamName} from tracked teams."
91 msg.send "I wasn't tracking #{teamName}."
92
93 robot.respond /assign ([a-z0-9 -@]+) as AO for ([a-z0-9 ]+)/i, (msg) ->
94 userId = robot.brain.userForName(msg.match[1])?.id
95 teamName = msg.match[2]
96 assignTeam userId, teamName, msg
97
98 robot.respond /I'm AO for ([a-z0-9 ]+)/i, (msg) ->
99 teamName = msg.match[1]
100 assignTeam msg.message.user.id, teamName, msg
101
102 assignTeam = (userId, teamName, msg) ->
103 team = getTeam(teamName)
104 return msg.send "Never heard of that team. You can add a team with 'Add <team name> to teams'." unless team
105 return msg.send "I have no idea who you're talking about." unless userId
106 team.assignAo(userId)
107 msg.send 'Got it.'
108
109 getTeam = (name) ->
110 return robot.brain.data.teams[name.toLowerCase()]
111
112 removeTeam = (name) ->
113 delete robot.brain.data.teams[name.toLowerCase()]
114
115 addTeam = (name) ->
116 robot.brain.data.teams[name.toLowerCase()] = new Team(name)
117
118 messageAOs = (message) ->
119 messageAO(team, message) for teamName, team of robot.brain.data.teams
120
121 messageAO = (team, message) ->
122 if team.aoUserId
123 aoUser = robot.brain.userForId(team.aoUserId)
124 robot.send(aoUser, message)
125
126class PullRequest
127 constructor: (pr) ->
128 @repo = pr.repo
129 @url = pr.url
130 @number = pr.number
131 @reviewNeededDt = new Date()
132
133class Team
134 constructor: (name) ->
135 @name = name
136 @members = []
137 @aoUserId = undefined
138 @aoUserAssignedDt = undefined
139
140 assignAo: (userId) ->
141 @aoUserId = userId
142 @aoUserAssignedDt = new Date()