opsask 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.yardoc/checksums +4 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +75 -0
- data/LICENSE +13 -0
- data/Rakefile +30 -0
- data/Readme.md +3 -0
- data/VERSION +1 -0
- data/bin/opsask +4 -0
- data/lib/opsask/app.rb +540 -0
- data/lib/opsask/main.rb +54 -0
- data/lib/opsask/metadata.rb +29 -0
- data/lib/opsask.rb +3 -0
- data/opsask.gemspec +29 -0
- data/public/css/foundation.css +5068 -0
- data/public/css/foundation.min.css +1 -0
- data/public/css/normalize.css +410 -0
- data/public/css/style.css +152 -0
- data/public/favicon.ico +0 -0
- data/public/fonts/DINMittelschriftStd.woff +0 -0
- data/public/img/.gitkeep +1 -0
- data/public/js/foundation/foundation.abide.js +256 -0
- data/public/js/foundation/foundation.accordion.js +49 -0
- data/public/js/foundation/foundation.alert.js +37 -0
- data/public/js/foundation/foundation.clearing.js +485 -0
- data/public/js/foundation/foundation.dropdown.js +208 -0
- data/public/js/foundation/foundation.equalizer.js +64 -0
- data/public/js/foundation/foundation.interchange.js +326 -0
- data/public/js/foundation/foundation.joyride.js +848 -0
- data/public/js/foundation/foundation.js +587 -0
- data/public/js/foundation/foundation.magellan.js +171 -0
- data/public/js/foundation/foundation.offcanvas.js +39 -0
- data/public/js/foundation/foundation.orbit.js +464 -0
- data/public/js/foundation/foundation.reveal.js +399 -0
- data/public/js/foundation/foundation.tab.js +58 -0
- data/public/js/foundation/foundation.tooltip.js +215 -0
- data/public/js/foundation/foundation.topbar.js +387 -0
- data/public/js/foundation/jquery.cookie.js +8 -0
- data/public/js/foundation.min.js +10 -0
- data/public/js/opsask.js +19 -0
- data/public/js/vendor/fastclick.js +9 -0
- data/public/js/vendor/jquery.cookie.js +8 -0
- data/public/js/vendor/jquery.js +26 -0
- data/public/js/vendor/modernizr.js +8 -0
- data/public/js/vendor/placeholder.js +2 -0
- data/tasks/generate-email-summary.rb +73 -0
- data/tasks/send-email-summary.sh +13 -0
- data/views/_components.erb +6 -0
- data/views/_count.erb +11 -0
- data/views/_flash.erb +3 -0
- data/views/_form.erb +37 -0
- data/views/_queue.erb +16 -0
- data/views/agile.erb +6 -0
- data/views/glance.erb +19 -0
- data/views/index.erb +27 -0
- data/views/layout.erb +43 -0
- data/views/stragglers.erb +6 -0
- data/views/untracked.erb +6 -0
- metadata +217 -0
data/lib/opsask/app.rb
ADDED
@@ -0,0 +1,540 @@
|
|
1
|
+
require 'curb'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra/partial'
|
4
|
+
require 'rack-flash'
|
5
|
+
require 'json'
|
6
|
+
require 'jira'
|
7
|
+
|
8
|
+
require_relative 'metadata'
|
9
|
+
|
10
|
+
|
11
|
+
module OpsAsk
|
12
|
+
class App < Sinatra::Base
|
13
|
+
set :root, OpsAsk::ROOT
|
14
|
+
|
15
|
+
# Add flash support
|
16
|
+
enable :sessions
|
17
|
+
use Rack::Flash
|
18
|
+
|
19
|
+
# Add partials support
|
20
|
+
register Sinatra::Partial
|
21
|
+
set :partial_template_engine, :erb
|
22
|
+
enable :partial_underscores
|
23
|
+
|
24
|
+
# Serve up our form
|
25
|
+
get '/' do
|
26
|
+
erb :index, locals: {
|
27
|
+
jiras_for_today: issues_for(today),
|
28
|
+
jiras_for_tomorrow: issues_for(tomorrow),
|
29
|
+
untracked_jiras: untracked_issues,
|
30
|
+
stragglers: straggling_issues
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/glance' do
|
35
|
+
erb :glance, locals: {
|
36
|
+
jiras_for_today: issues_for(today),
|
37
|
+
jiras_for_tomorrow: issues_for(tomorrow),
|
38
|
+
untracked_jiras: untracked_issues,
|
39
|
+
stragglers: straggling_issues
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/untracked' do
|
44
|
+
erb :untracked, locals: {
|
45
|
+
untracked_jiras: untracked_issues,
|
46
|
+
stragglers: straggling_issues
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
get '/stragglers' do
|
51
|
+
erb :stragglers, locals: {
|
52
|
+
untracked_jiras: untracked_issues,
|
53
|
+
stragglers: straggling_issues
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
get '/sprint/:sprint_num' do
|
58
|
+
erb :agile, locals: {
|
59
|
+
asks: asks_in_sprint(params[:sprint_num]),
|
60
|
+
sprint: get_sprint(params[:sprint_num]),
|
61
|
+
untracked_jiras: untracked_issues,
|
62
|
+
stragglers: straggling_issues
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
get '/agile' do
|
67
|
+
erb :agile, locals: {
|
68
|
+
asks: asks_in_current_sprint,
|
69
|
+
sprint: current_sprint([]),
|
70
|
+
untracked_jiras: untracked_issues,
|
71
|
+
stragglers: straggling_issues
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# I think everyone should do this
|
76
|
+
get '/version' do
|
77
|
+
content_type :txt
|
78
|
+
"opsask #{settings.config[:app_version]}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Try to create a JIRA
|
82
|
+
post '/' do
|
83
|
+
duedate = validate_room_for_new_jiras
|
84
|
+
component, summary, description, assign_to_me, epic, ops_only = validate_jira_params
|
85
|
+
jira = create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
86
|
+
if jira.nil? or !jira.has_key?('key')
|
87
|
+
flash[:error] = [ %Q| Failure!
|
88
|
+
JIRA had an issue processing your request. Try again?
|
89
|
+
| ]
|
90
|
+
else
|
91
|
+
flash[:notice] = [ %Q| Success!
|
92
|
+
<a href="#{settings.config[:jira_url]}/browse/#{jira['key']}">#{jira['key']}</a>
|
93
|
+
has been created on your behalf.
|
94
|
+
| ]
|
95
|
+
end
|
96
|
+
redirect '/'
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public assets
|
100
|
+
%w[ css img js fonts ].each do |asset|
|
101
|
+
get "/#{asset}/:file" do
|
102
|
+
send_file "public/#{asset}/#{params[:file]}", :disposition => 'inline'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
get '/favicon.ico' do
|
107
|
+
send_file 'public/favicon.ico', :disposition => 'inline'
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
# This section gets called before every request. Here, we set up the
|
113
|
+
# OAuth consumer details including the consumer key, private key,
|
114
|
+
# site uri, and the request token, access token, and authorize paths
|
115
|
+
before do
|
116
|
+
options = {
|
117
|
+
:site => settings.config[:jira_url],
|
118
|
+
:context_path => '',
|
119
|
+
:signature_method => 'RSA-SHA1',
|
120
|
+
:request_token_path => "#{settings.config[:jira_url]}/plugins/servlet/oauth/request-token",
|
121
|
+
:authorize_url => "#{settings.config[:jira_url]}/plugins/servlet/oauth/authorize",
|
122
|
+
:access_token_path => "#{settings.config[:jira_url]}/plugins/servlet/oauth/access-token",
|
123
|
+
:private_key_file => settings.config[:jira_private_key],
|
124
|
+
:rest_base_path => "#{settings.config[:jira_url]}/rest/api/latest",
|
125
|
+
:consumer_key => settings.config[:jira_consumer_key]
|
126
|
+
}
|
127
|
+
|
128
|
+
@jira_client = JIRA::Client.new(options)
|
129
|
+
# @jira_client.consumer.http.set_debug_output($stderr)
|
130
|
+
|
131
|
+
# Add AccessToken if authorised previously.
|
132
|
+
if session[:jira_auth]
|
133
|
+
@jira_client.set_access_token(
|
134
|
+
session[:jira_auth][:access_token],
|
135
|
+
session[:jira_auth][:access_key]
|
136
|
+
)
|
137
|
+
|
138
|
+
if @project.nil?
|
139
|
+
@project = @jira_client.Project.find(settings.config[:project_key])
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Keep a pointer to myself
|
144
|
+
begin
|
145
|
+
response = @jira_client.get(
|
146
|
+
@jira_client.options[:rest_base_path] + '/myself?expand=groups'
|
147
|
+
)
|
148
|
+
@myself = JSON::parse response.body
|
149
|
+
@me = @myself['name']
|
150
|
+
rescue JIRA::OauthClient::UninitializedAccessTokenError
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Retrieves the @access_token then stores it inside a session cookie. In a real app,
|
155
|
+
# you'll want to persist the token in a datastore associated with the user.
|
156
|
+
get '/callback/' do
|
157
|
+
request_token = @jira_client.set_request_token(
|
158
|
+
session[:request_token], session[:request_secret]
|
159
|
+
)
|
160
|
+
access_token = @jira_client.init_access_token(
|
161
|
+
:oauth_verifier => params[:oauth_verifier]
|
162
|
+
)
|
163
|
+
|
164
|
+
session[:jira_auth] = {
|
165
|
+
:access_token => access_token.token,
|
166
|
+
:access_key => access_token.secret
|
167
|
+
}
|
168
|
+
|
169
|
+
session.delete(:request_token)
|
170
|
+
session.delete(:request_secret)
|
171
|
+
|
172
|
+
redirect '/'
|
173
|
+
end
|
174
|
+
|
175
|
+
# Initialize the JIRA session
|
176
|
+
get '/login' do
|
177
|
+
request_token = @jira_client.request_token
|
178
|
+
session[:request_token] = request_token.token
|
179
|
+
session[:request_secret] = request_token.secret
|
180
|
+
redirect request_token.authorize_url
|
181
|
+
end
|
182
|
+
|
183
|
+
# Expire the JIRA session
|
184
|
+
get '/logout' do
|
185
|
+
session.delete(:jira_auth)
|
186
|
+
redirect '/'
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
private
|
192
|
+
def logged_in?
|
193
|
+
!!session[:jira_auth]
|
194
|
+
end
|
195
|
+
|
196
|
+
def ops?
|
197
|
+
return false unless logged_in?
|
198
|
+
@myself['groups']['items'].each do |i|
|
199
|
+
return true if i['name'] == settings.config[:ops_group]
|
200
|
+
end
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
|
204
|
+
def one_day
|
205
|
+
1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
|
206
|
+
end
|
207
|
+
|
208
|
+
def now
|
209
|
+
Time.now # + 3 * one_day # DEBUG
|
210
|
+
end
|
211
|
+
|
212
|
+
def todays_date offset=0
|
213
|
+
date = now + offset
|
214
|
+
date += one_day if date.saturday?
|
215
|
+
date += one_day if date.sunday?
|
216
|
+
return date
|
217
|
+
end
|
218
|
+
|
219
|
+
def asks_in_current_sprint
|
220
|
+
# TODO
|
221
|
+
end
|
222
|
+
|
223
|
+
def asks_in_sprint num
|
224
|
+
return [] unless logged_in?
|
225
|
+
issues = []
|
226
|
+
@jira_client.Issue.jql("project = #{settings.config[:project_name]} AND labels in (OpsAsk) and labels in (Sprint#{num})").each do |i|
|
227
|
+
issues << i.attrs
|
228
|
+
end
|
229
|
+
return issues
|
230
|
+
end
|
231
|
+
|
232
|
+
def sprints
|
233
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
|
234
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
235
|
+
curl.headers['Accept'] = 'application/json'
|
236
|
+
curl.headers['Content-Type'] = 'application/json'
|
237
|
+
curl.http_auth_types = :basic
|
238
|
+
curl.username = settings.config[:jira_user]
|
239
|
+
curl.password = settings.config[:jira_pass]
|
240
|
+
curl.verbose = true
|
241
|
+
end
|
242
|
+
|
243
|
+
raw_response = curl_request.body_str
|
244
|
+
begin
|
245
|
+
data = JSON::parse(raw_response)
|
246
|
+
return data['sprints']
|
247
|
+
rescue
|
248
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
249
|
+
end
|
250
|
+
return nil
|
251
|
+
end
|
252
|
+
|
253
|
+
def get_sprint num
|
254
|
+
sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
|
255
|
+
sprint_id = sprint.first['id']
|
256
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
|
257
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
258
|
+
curl.headers['Accept'] = 'application/json'
|
259
|
+
curl.headers['Content-Type'] = 'application/json'
|
260
|
+
curl.http_auth_types = :basic
|
261
|
+
curl.username = settings.config[:jira_user]
|
262
|
+
curl.password = settings.config[:jira_pass]
|
263
|
+
curl.verbose = true
|
264
|
+
end
|
265
|
+
|
266
|
+
raw_response = curl_request.body_str
|
267
|
+
begin
|
268
|
+
data = JSON::parse(raw_response)
|
269
|
+
contents = data.delete('contents')
|
270
|
+
data = data.delete('sprint')
|
271
|
+
return data.merge(contents)
|
272
|
+
rescue
|
273
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
274
|
+
end
|
275
|
+
return {}
|
276
|
+
end
|
277
|
+
|
278
|
+
def current_sprint_name sprint=current_sprint
|
279
|
+
sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
|
280
|
+
end
|
281
|
+
|
282
|
+
def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
|
283
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
|
284
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
285
|
+
curl.headers['Accept'] = 'application/json'
|
286
|
+
curl.headers['Content-Type'] = 'application/json'
|
287
|
+
curl.http_auth_types = :basic
|
288
|
+
curl.username = settings.config[:jira_user]
|
289
|
+
curl.password = settings.config[:jira_pass]
|
290
|
+
curl.verbose = true
|
291
|
+
end
|
292
|
+
|
293
|
+
raw_response = curl_request.body_str
|
294
|
+
begin
|
295
|
+
data = JSON::parse(raw_response)
|
296
|
+
keys.each { |k| data = data[k] }
|
297
|
+
return data
|
298
|
+
rescue
|
299
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
300
|
+
end
|
301
|
+
return nil
|
302
|
+
end
|
303
|
+
|
304
|
+
def today offset=0
|
305
|
+
todays_date(offset).strftime '%Y-%m-%d'
|
306
|
+
end
|
307
|
+
|
308
|
+
def tomorrow
|
309
|
+
today(one_day)
|
310
|
+
end
|
311
|
+
|
312
|
+
def name_for_today offset=0
|
313
|
+
todays_date(offset).strftime '%A %^b %-d'
|
314
|
+
end
|
315
|
+
|
316
|
+
def name_for_tomorrow
|
317
|
+
name_for_today(one_day)
|
318
|
+
end
|
319
|
+
|
320
|
+
def name_for_coming_week
|
321
|
+
todays_date.strftime 'Week of %^b %-d'
|
322
|
+
end
|
323
|
+
|
324
|
+
def jiras_for date
|
325
|
+
return [] unless logged_in?
|
326
|
+
unless ops?
|
327
|
+
return @jira_client.Issue.jql("due = #{date} AND project = #{settings.config[:project_name]} AND type != Change AND labels not in (OpsOnly)")
|
328
|
+
end
|
329
|
+
return @jira_client.Issue.jql("due = #{date} AND project = #{settings.config[:project_name]} AND type != Change")
|
330
|
+
end
|
331
|
+
|
332
|
+
def jira_count_for date
|
333
|
+
jiras_for(date).length
|
334
|
+
end
|
335
|
+
|
336
|
+
def jira_count_for_today ; jira_count_for(today) end
|
337
|
+
|
338
|
+
def jira_count_for_tomorrow ; jira_count_for(tomorrow) end
|
339
|
+
|
340
|
+
def raw_classes_for jira
|
341
|
+
classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
|
342
|
+
classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
|
343
|
+
end
|
344
|
+
|
345
|
+
def classes_for jira
|
346
|
+
raw_classes_for(jira).join(' ')
|
347
|
+
end
|
348
|
+
|
349
|
+
def sorting_key_for jira
|
350
|
+
rcs = raw_classes_for(jira)
|
351
|
+
idx = 1
|
352
|
+
idx = 2 if rcs.include? 'denimcores'
|
353
|
+
idx = 0 if rcs.include? 'closed'
|
354
|
+
return "#{idx}-#{jira.key}"
|
355
|
+
end
|
356
|
+
|
357
|
+
def issues_for date
|
358
|
+
jiras_for(date).sort_by do |jira|
|
359
|
+
sorting_key_for(jira)
|
360
|
+
end.reverse
|
361
|
+
end
|
362
|
+
|
363
|
+
def its_the_weekend?
|
364
|
+
now.saturday? || now.sunday?
|
365
|
+
end
|
366
|
+
|
367
|
+
def room_for_new_jiras_for? date
|
368
|
+
return true if ops?
|
369
|
+
jira_count_for(date) < settings.config[:queue_size]
|
370
|
+
end
|
371
|
+
|
372
|
+
def date_for_new_jiras
|
373
|
+
if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
|
374
|
+
return today if room_for_new_jiras_for? today
|
375
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
376
|
+
else
|
377
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
378
|
+
end
|
379
|
+
return nil
|
380
|
+
end
|
381
|
+
|
382
|
+
def room_for_new_jiras?
|
383
|
+
return true if ops?
|
384
|
+
!date_for_new_jiras.nil?
|
385
|
+
end
|
386
|
+
|
387
|
+
def validate_room_for_new_jiras
|
388
|
+
duedate = date_for_new_jiras
|
389
|
+
return duedate unless duedate.nil?
|
390
|
+
flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
|
391
|
+
redirect '/'
|
392
|
+
end
|
393
|
+
|
394
|
+
def validate_jira_params
|
395
|
+
flash[:error] = []
|
396
|
+
flash[:error] << 'Summary is required' if params['jira-summary'].empty?
|
397
|
+
redirect '/' unless flash[:error].empty?
|
398
|
+
return [
|
399
|
+
params['jira-component'],
|
400
|
+
params['jira-summary'],
|
401
|
+
params['jira-description'],
|
402
|
+
!!params['jira-assign_to_me'],
|
403
|
+
params['jira-epic'],
|
404
|
+
!!params['jira-ops_only']
|
405
|
+
]
|
406
|
+
end
|
407
|
+
|
408
|
+
def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
409
|
+
epic = 'INF-3091' if epic.nil? # OpsAsk default epic
|
410
|
+
assignee = assign_to_me ? @me : settings.config[:assignee]
|
411
|
+
components = []
|
412
|
+
components = [ { name: component } ] unless component
|
413
|
+
labels = [ 'OpsAsk', current_sprint_name ].compact
|
414
|
+
labels << 'OpsOnly' if ops_only
|
415
|
+
data = {
|
416
|
+
fields: {
|
417
|
+
project: { key: settings.config[:project_key] },
|
418
|
+
issuetype: { name: settings.config[:issue_type] },
|
419
|
+
versions: [ { name: settings.config[:version] } ],
|
420
|
+
duedate: duedate,
|
421
|
+
summary: summary,
|
422
|
+
description: description,
|
423
|
+
components: components,
|
424
|
+
assignee: { name: assignee },
|
425
|
+
reporter: { name: @me },
|
426
|
+
labels: labels,
|
427
|
+
customfield_10002: 1, # Story Points = 1
|
428
|
+
# customfield_10350: epic,
|
429
|
+
customfield_10040: { id: '-1' } # Release Priority = None
|
430
|
+
}
|
431
|
+
}
|
432
|
+
|
433
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
|
434
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
435
|
+
curl.headers['Accept'] = 'application/json'
|
436
|
+
curl.headers['Content-Type'] = 'application/json'
|
437
|
+
curl.http_auth_types = :basic
|
438
|
+
curl.username = settings.config[:jira_user]
|
439
|
+
curl.password = settings.config[:jira_pass]
|
440
|
+
curl.verbose = true
|
441
|
+
end
|
442
|
+
|
443
|
+
raw_response = curl_request.body_str
|
444
|
+
begin
|
445
|
+
response = JSON::parse raw_response
|
446
|
+
rescue
|
447
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
448
|
+
return nil
|
449
|
+
end
|
450
|
+
return response
|
451
|
+
end
|
452
|
+
|
453
|
+
def components
|
454
|
+
return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
|
455
|
+
end
|
456
|
+
|
457
|
+
def untracked_issues
|
458
|
+
return [] unless logged_in?
|
459
|
+
constraints = [
|
460
|
+
"project = #{settings.config[:project_name]}",
|
461
|
+
"due < #{today}",
|
462
|
+
"resolution = unresolved",
|
463
|
+
"assignee = denimcores"
|
464
|
+
].join(' AND ')
|
465
|
+
@jira_client.Issue.jql(constraints).sort_by do |jira|
|
466
|
+
sorting_key_for(jira)
|
467
|
+
end.reverse
|
468
|
+
end
|
469
|
+
|
470
|
+
def straggling_issues
|
471
|
+
return [] unless logged_in?
|
472
|
+
constraints = [
|
473
|
+
"project = #{settings.config[:project_name]}",
|
474
|
+
"due < #{today}",
|
475
|
+
"labels in (OpsAsk)",
|
476
|
+
"resolution = unresolved",
|
477
|
+
"assignee != denimcores"
|
478
|
+
].join(' AND ')
|
479
|
+
@jira_client.Issue.jql(constraints).sort_by do |jira|
|
480
|
+
sorting_key_for(jira)
|
481
|
+
end.reverse
|
482
|
+
end
|
483
|
+
|
484
|
+
def epics
|
485
|
+
data = {
|
486
|
+
jql: "type = Epic AND project = #{settings.config[:project_name]}",
|
487
|
+
startAt: 0,
|
488
|
+
maxResults: 1000
|
489
|
+
}
|
490
|
+
|
491
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/search"
|
492
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
493
|
+
curl.headers['Accept'] = 'application/json'
|
494
|
+
curl.headers['Content-Type'] = 'application/json'
|
495
|
+
curl.http_auth_types = :basic
|
496
|
+
curl.username = settings.config[:jira_user]
|
497
|
+
curl.password = settings.config[:jira_pass]
|
498
|
+
curl.verbose = true
|
499
|
+
end
|
500
|
+
|
501
|
+
raw_response = curl_request.body_str
|
502
|
+
begin
|
503
|
+
response = JSON::parse raw_response
|
504
|
+
rescue
|
505
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
506
|
+
return nil
|
507
|
+
end
|
508
|
+
return response['issues'].map do |epic|
|
509
|
+
{
|
510
|
+
'key' => epic['key'],
|
511
|
+
'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
|
512
|
+
}
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
def epic key
|
517
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
|
518
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
519
|
+
curl.headers['Accept'] = 'application/json'
|
520
|
+
curl.headers['Content-Type'] = 'application/json'
|
521
|
+
curl.http_auth_types = :basic
|
522
|
+
curl.username = settings.config[:jira_user]
|
523
|
+
curl.password = settings.config[:jira_pass]
|
524
|
+
curl.verbose = true
|
525
|
+
end
|
526
|
+
|
527
|
+
raw_response = curl_request.body_str
|
528
|
+
begin
|
529
|
+
response = JSON::parse raw_response
|
530
|
+
rescue
|
531
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
532
|
+
return nil
|
533
|
+
end
|
534
|
+
return {
|
535
|
+
'key' => response['key'],
|
536
|
+
'name' => response['fields']['customfield_10351'] || response['fields']['summary']
|
537
|
+
}
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
data/lib/opsask/main.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'sinatra/base'
|
3
|
+
|
4
|
+
require_relative 'app'
|
5
|
+
require_relative 'metadata'
|
6
|
+
|
7
|
+
|
8
|
+
module OpsAsk
|
9
|
+
class Main < Thor
|
10
|
+
desc 'art', 'Show the application art'
|
11
|
+
def art
|
12
|
+
max_line_len = OpsAsk::ART.lines.sort_by { |l| l.length }.last.length
|
13
|
+
description = "OpsAsk #{OpsAsk::VERSION} / #{OpsAsk::SUMMARY} / #{OpsAsk::AUTHOR} (#{OpsAsk::EMAIL})"
|
14
|
+
puts
|
15
|
+
puts OpsAsk::ART
|
16
|
+
puts description.center(max_line_len)
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'version', 'Show the application version'
|
21
|
+
def version
|
22
|
+
puts OpsAsk::VERSION
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'server', 'Start application web server'
|
26
|
+
option :port, default: 3000, aliases: :p
|
27
|
+
option :config, default: nil, aliases: :c
|
28
|
+
def server
|
29
|
+
config = {
|
30
|
+
ops_group: 'change-network-operations',
|
31
|
+
agile_board: '169', # Operations
|
32
|
+
assignee: 'denimcores',
|
33
|
+
jira_user: nil,
|
34
|
+
jira_pass: nil,
|
35
|
+
jira_url: 'http://jira.bluejeansnet.com',
|
36
|
+
queue_size: 10,
|
37
|
+
cutoff_hour: 18, # 6pm
|
38
|
+
project_key: 'INF',
|
39
|
+
project_name: 'Infrastructure',
|
40
|
+
issue_type: 'Task',
|
41
|
+
version: 'Un-targeted',
|
42
|
+
jira_private_key: 'opsask.pem',
|
43
|
+
jira_consumer_key: 'opsask-test',
|
44
|
+
app_version: OpsAsk::VERSION
|
45
|
+
}
|
46
|
+
|
47
|
+
if options[:config]
|
48
|
+
config.merge! JSON::parse(File.read(options[:config]), symbolize_names: true)
|
49
|
+
end
|
50
|
+
|
51
|
+
App.run! port: options[:port], config: config
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module OpsAsk
|
2
|
+
# General information about the project
|
3
|
+
SUMMARY = %q.Ask Ops for stuff.
|
4
|
+
AUTHOR = 'Sean Clemmer'
|
5
|
+
EMAIL = 'sczizzo@gmail.com'
|
6
|
+
LICENSE = 'ISC'
|
7
|
+
HOMEPAGE = 'http://git.ops.bluejeans.com/inf/opsask'
|
8
|
+
|
9
|
+
# Project root
|
10
|
+
ROOT = File.join File.dirname(__FILE__), '..', '..'
|
11
|
+
|
12
|
+
# Pull the project version out of the VERSION file
|
13
|
+
VERSION = File.read(File.join(ROOT, 'VERSION')).strip
|
14
|
+
|
15
|
+
ART = <<-'EOART'
|
16
|
+
_ _ _ _ _ _
|
17
|
+
/\ \ /\ \ / /\ / /\ / /\ /\_\
|
18
|
+
/ \ \ / \ \ / / \ / / \ / / \ / / / _
|
19
|
+
/ /\ \ \ / /\ \ \ / / /\ \__ / / /\ \ / / /\ \__ / / / /\_\
|
20
|
+
/ / /\ \ \ / / /\ \_\ / / /\ \___\ / / /\ \ \ / / /\ \___\ / / /__/ / /
|
21
|
+
/ / / \ \_\ / / /_/ / / \ \ \ \/___// / / \ \ \ \ \ \ \/___// /\_____/ /
|
22
|
+
/ / / / / // / /__\/ / \ \ \ / / /___/ /\ \ \ \ \ / /\_______/
|
23
|
+
/ / / / / // / /_____/_ \ \ \ / / /_____/ /\ \ _ \ \ \ / / /\ \ \
|
24
|
+
/ / /___/ / // / / /_/\__/ / / / /_________/\ \ \ /_/\__/ / / / / / \ \ \
|
25
|
+
/ / /____\/ // / / \ \/___/ / / / /_ __\ \_\\ \/___/ / / / / \ \ \
|
26
|
+
\/_________/ \/_/ \_____\/ \_\___\ /____/_/ \_____\/ \/_/ \_\_\
|
27
|
+
|
28
|
+
EOART
|
29
|
+
end
|
data/lib/opsask.rb
ADDED
data/opsask.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path(File.join('..', 'lib'), __FILE__)
|
3
|
+
require 'opsask/metadata'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'opsask'
|
7
|
+
s.version = OpsAsk::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.author = OpsAsk::AUTHOR
|
10
|
+
s.email = OpsAsk::EMAIL
|
11
|
+
s.summary = OpsAsk::SUMMARY
|
12
|
+
s.description = OpsAsk::SUMMARY + '.'
|
13
|
+
s.homepage = OpsAsk::HOMEPAGE
|
14
|
+
s.license = OpsAsk::LICENSE
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'thor', '~> 0'
|
17
|
+
s.add_runtime_dependency 'thin', '~> 1'
|
18
|
+
s.add_runtime_dependency 'curb', '~> 0.8'
|
19
|
+
s.add_runtime_dependency 'rack', '~> 1'
|
20
|
+
s.add_runtime_dependency 'sinatra', '~> 1.4'
|
21
|
+
s.add_runtime_dependency 'jira-ruby', '~> 0.1'
|
22
|
+
s.add_runtime_dependency 'rack-flash3', '~> 1'
|
23
|
+
s.add_runtime_dependency 'sinatra-partial', '~> 0.4'
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File::basename(f) }
|
28
|
+
s.require_paths = %w[ lib ]
|
29
|
+
end
|