Slack, Bots, Coffeescript, DoD, and SMS
Like every developer team working in Scrum, our Team OneAPI has a clear ‘definition of done’ (DoD) for each task, so someone else can try out a new feature, review code, etc. So much so that we even have a dedicated page on Confluence.
Now, the concept of that someone else is the most difficult part of this process. You must find that “lucky” person. Why you try, suddenly everyone is busy, in a rush or turns deaf. So, we had to randomize it.
FIRST ITERATION – GROOVY SCRIPT
def list = Arrays.asList(Member.values())
Collections.shuffle(list)
print list.iterator().next()
public enum Member {
MATS,
DENIS,
PETAR,
MATO
}
1
2
3
4
5
6
7
8
9
10
…but was done in matter of seconds. You had to randomize and assign the task on JIRA. The main problem is that’s easy to compromise. As nobody likes to do DoD tasks, nobody will trust you.
SECOND ITERATION – BOOKMARKLET
…was cool but still didn’t solve the problem:
javascript:(function () {
function random(){
var devs = ["Matija Bruncic", "Denis Cutic", "Matija Matosevic"];
return devs[Math.floor(Math.random()*devs.length)]
}
var assignee = random();
document.querySelectorAll("[data-field-text='"+assignee+"']")[0].setAttribute("selected", "selected");
document.getElementById("assign-issue-submit").click()
})();
1
2
3
4
5
6
7
8
9
10
11
12
You had to go to that JIRA task in your browser and run bookmarklet. Still, nobody will trust you. And (same as the first iteration) the code had to differ for each member. Plus, it probably doesn’t work on IE.
THIRD ITERATION – SLACKBOT
…was very easy. And it solved the problem of biasing the randomness.
Slack has a default bot – Slackbot which can answer one of the predefined responses on predefined requests. But it’s very, very annoying when he picks you!
One of the reasons we adopted Slack is its easy integration with bots, and opportunity to write some cool and useful code. We decided to use Slack-Hubot. It has Hubot under the hood, it’s easy to setup and program. I won’t go into too many details, but you can find the documentation online.
And you must name it. Our bot is tough guy, so we named him Rambot. You program your logic in coffeescript, and it’s like you’re writing nodejs app. For example, you can list network interfaces:
module.exports = (robot) ->
robot.hear /rambot, where are you?/i, (res) ->
os = require('os');
ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach (ifname) ->
ifaces[ifname].forEach (iface) ->
if 'IPv4' != iface.family or iface.internal != false
# skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
return
# this interface has only one ipv4 adress
res.send 'here somewhere: ' + ifname, iface.address
return
return
1
2
3
4
5
6
7
8
9
10
11
12
13
But let’s return to the subject at hand!
We’ve created a simple script which recognizes the user who sent the request and randomly picks one of the remaining team members:
users = ["denis.cutic", "josip", "mmatosevic", "mbruncic", "pducic"]
module.exports = (robot) ->
robot.hear /dod/i, (response) ->
possible = (user for user in users when user != response.message.user.name)
lucky = possible[Math.floor(Math.random() * possible.length)]
response.send "Hi #{response.message.user.name}! Your dod is assigned to #{lucky}"
1
2
3
4
5
6
But then we could not stop programming, so we added a feature which enable Rambot to update task assignees through JIRA API:
jiraData = JSON.stringify({
fields: {
assignee: {
name: luckyJiraUsername
}
}
})
response.http("https://jira.infobip.com/rest/api/2/issue/#{task}")
.auth(jiraUsername, jiraPass)
.header('Content-Type', 'application/json')
.put(jiraData) (err, res, body) ->
response.send(body)
1
2
3
4
5
6
7
8
9
10
11
12
And of course, an Infobip SMS API powered feature to notify that lucky guy with an SMS:
ibData = JSON.stringify({
to:users[lucky].gsm,
text:"You're the lucky one: #{task}"
})
response.http("https://api.infobip.com/sms/1/text/single")
.auth(infobipUsername, infobipPass)
.header('Content-Type', 'application/json')
.post(ibData) (err, res, body) ->
response.send "sent SMS!"
1
2
3
4
5
6
7
8
9
The final code looks something like this:
jiraUsername = process.env.HUBOT_JIRA_USERNAME
jiraPass = process.env.HUBOT_JIRA_PASS
infobipUsername = process.env.HUBOT_IB_USERNAME
infobipPass = process.env.HUBOT_IB_PASS
users = {
"denis.cutic": {
jira: "dcutic",
gsm: "3859********"
},
josip: {
jira: "jantolis",
gsm: "3859********"
},
mmatosevic: {
jira: "mmatosevic",
gsm: "3859********"
},
mbruncic: {
jira: "mbruncic",
gsm: "3859********"
},
pducic: {
jira: "pducic",
gsm: "3859********"
},
}
module.exports = (robot) ->
robot.hear /dod (.*)/i, (response) ->
task = response.match[1]
possible = (user for user, value of users when user != response.message.user.name)
lucky = possible[Math.floor(Math.random() * possible.length)]
luckyJiraUsername = users[lucky].jira
if jiraUsername? && jiraPass?
jiraData = JSON.stringify({
fields: {
assignee: {
name: luckyJiraUsername
}
}
})
response.http("https://jira.infobip.com/rest/api/2/issue/#{task}")
.auth(jiraUsername, jiraPass)
.header('Content-Type', 'application/json')
.put(jiraData) (err, res, body) ->
response.send(body)
if infobipUsername? && infobipPass?
ibData = JSON.stringify({
to:users[lucky].gsm,
text:"You're the lucky one: #{task}"
})
response.http("https://api.infobip.com/sms/1/text/single")
.auth(infobipUsername, infobipPass)
.header('Content-Type', 'application/json')
.post(ibData) (err, res, body) ->
response.send "sent SMS!"
response.send "Hi #{response.message.user.name}! Your dod is assigned to #{lucky} "
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
That’s it! If you have some improvement ideas, we do accept pull requests. The entire code is available on GitHub.
By Petar Ducic, Software Engineer / Team Leader
Jun 16th, 2016
4 min read