opsask 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|