opsask 2.0.11 → 2.0.12
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 +4 -4
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/lib/opsask/app.rb +3 -424
- data/lib/opsask/helpers.rb +426 -0
- data/public/css/style.css +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64a1996b5ca927b9165bf0b852c8914558154b1c
|
4
|
+
data.tar.gz: 9137f959e9afcd4137cb5db3434f78df3fab3b9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9bed853571ccac80880043300a3261d60d404474ce677e0698fb93a50dd886c19466bb098c07c939d4f4637500e9958bfa0f67670fa11ce73eec869a9c9e26d
|
7
|
+
data.tar.gz: c00de6d25a2ff7cf79915601471733b94a5f44cb60bd2fa35de15b902951bf529a4bfaa88fc4ce8c2b7bec1a74a14802b8c1c38a87e502d17e59c4c678433f6c
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0.
|
1
|
+
2.0.12
|
data/lib/opsask/app.rb
CHANGED
@@ -5,11 +5,14 @@ require 'rack-flash'
|
|
5
5
|
require 'json'
|
6
6
|
require 'jira'
|
7
7
|
|
8
|
+
require_relative 'helpers'
|
8
9
|
require_relative 'metadata'
|
9
10
|
|
10
11
|
|
11
12
|
module OpsAsk
|
12
13
|
class App < Sinatra::Base
|
14
|
+
include OpsAsk::Helpers
|
15
|
+
|
13
16
|
set :root, OpsAsk::ROOT
|
14
17
|
|
15
18
|
# Add flash support
|
@@ -225,429 +228,5 @@ module OpsAsk
|
|
225
228
|
end
|
226
229
|
end
|
227
230
|
|
228
|
-
|
229
|
-
private
|
230
|
-
def logged_in?
|
231
|
-
!!session[:jira_auth]
|
232
|
-
end
|
233
|
-
|
234
|
-
def ops?
|
235
|
-
return false unless logged_in?
|
236
|
-
@myself['groups']['items'].each do |i|
|
237
|
-
return true if i['name'] == settings.config[:ops_group]
|
238
|
-
end
|
239
|
-
return false
|
240
|
-
end
|
241
|
-
|
242
|
-
def one_day
|
243
|
-
1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
|
244
|
-
end
|
245
|
-
|
246
|
-
def now
|
247
|
-
Time.now # + 3 * one_day # DEBUG
|
248
|
-
end
|
249
|
-
|
250
|
-
def todays_date offset=0
|
251
|
-
date = now + offset
|
252
|
-
date += one_day if date.saturday?
|
253
|
-
date += one_day if date.sunday?
|
254
|
-
return date
|
255
|
-
end
|
256
|
-
|
257
|
-
def stats_for issues, resolved_link, unresolved_link
|
258
|
-
return {} unless logged_in?
|
259
|
-
return {} unless issues
|
260
|
-
|
261
|
-
resolved_issues, unresolved_issues = [], []
|
262
|
-
|
263
|
-
issues.map! do |i|
|
264
|
-
key = i['key']
|
265
|
-
status = i['fields']['status']['name']
|
266
|
-
resolution = i['fields']['resolution']['name'] rescue nil
|
267
|
-
points = i['fields']['customfield_10002'].to_i
|
268
|
-
|
269
|
-
issue = {
|
270
|
-
key: key,
|
271
|
-
status: status,
|
272
|
-
resolution: resolution,
|
273
|
-
points: points
|
274
|
-
}
|
275
|
-
|
276
|
-
if resolution.nil?
|
277
|
-
unresolved_issues << issue
|
278
|
-
else
|
279
|
-
resolved_issues << issue
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
{
|
284
|
-
resolved: {
|
285
|
-
number: resolved_issues.size,
|
286
|
-
points: resolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
287
|
-
link: resolved_link
|
288
|
-
},
|
289
|
-
unresolved: {
|
290
|
-
number: unresolved_issues.size,
|
291
|
-
points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
292
|
-
link: unresolved_link
|
293
|
-
}
|
294
|
-
}
|
295
|
-
end
|
296
|
-
|
297
|
-
def items_in_current_sprint
|
298
|
-
items_in_sprint current_sprint_num
|
299
|
-
end
|
300
|
-
|
301
|
-
def items_in_sprint num
|
302
|
-
return [] unless logged_in?
|
303
|
-
issues = []
|
304
|
-
id = get_sprint(num)['id']
|
305
|
-
query = normalized_jql("sprint = #{id}", nil)
|
306
|
-
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
307
|
-
issues << i.attrs
|
308
|
-
end
|
309
|
-
return issues
|
310
|
-
end
|
311
|
-
|
312
|
-
def asks_in_current_sprint
|
313
|
-
asks_in_sprint current_sprint_num
|
314
|
-
end
|
315
|
-
|
316
|
-
def asks_in_sprint num
|
317
|
-
return [] unless logged_in?
|
318
|
-
issues = []
|
319
|
-
query = normalized_jql("labels in (Sprint#{num})", nil)
|
320
|
-
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
321
|
-
issues << i.attrs
|
322
|
-
end
|
323
|
-
return issues
|
324
|
-
end
|
325
|
-
|
326
|
-
def sprints
|
327
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
|
328
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
329
|
-
curl.headers['Accept'] = 'application/json'
|
330
|
-
curl.headers['Content-Type'] = 'application/json'
|
331
|
-
curl.http_auth_types = :basic
|
332
|
-
curl.username = settings.config[:jira_user]
|
333
|
-
curl.password = settings.config[:jira_pass]
|
334
|
-
curl.verbose = true
|
335
|
-
end
|
336
|
-
|
337
|
-
raw_response = curl_request.body_str
|
338
|
-
begin
|
339
|
-
data = JSON::parse(raw_response)
|
340
|
-
return data['sprints']
|
341
|
-
rescue
|
342
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
343
|
-
end
|
344
|
-
return nil
|
345
|
-
end
|
346
|
-
|
347
|
-
def get_sprint num
|
348
|
-
sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
|
349
|
-
sprint_id = sprint.first['id']
|
350
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
|
351
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
352
|
-
curl.headers['Accept'] = 'application/json'
|
353
|
-
curl.headers['Content-Type'] = 'application/json'
|
354
|
-
curl.http_auth_types = :basic
|
355
|
-
curl.username = settings.config[:jira_user]
|
356
|
-
curl.password = settings.config[:jira_pass]
|
357
|
-
curl.verbose = true
|
358
|
-
end
|
359
|
-
|
360
|
-
raw_response = curl_request.body_str
|
361
|
-
begin
|
362
|
-
data = JSON::parse(raw_response)
|
363
|
-
contents = data.delete('contents')
|
364
|
-
data = data.delete('sprint')
|
365
|
-
return data.merge(contents)
|
366
|
-
rescue
|
367
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
368
|
-
end
|
369
|
-
return {}
|
370
|
-
end
|
371
|
-
|
372
|
-
def current_sprint_name sprint=current_sprint
|
373
|
-
sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
|
374
|
-
end
|
375
|
-
|
376
|
-
def current_sprint_num sprint=current_sprint
|
377
|
-
sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '')
|
378
|
-
end
|
379
|
-
|
380
|
-
def current_sprint_id sprint=current_sprint
|
381
|
-
sprint.nil? ? nil : sprint['id']
|
382
|
-
end
|
383
|
-
|
384
|
-
def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
|
385
|
-
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
|
386
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
387
|
-
curl.headers['Accept'] = 'application/json'
|
388
|
-
curl.headers['Content-Type'] = 'application/json'
|
389
|
-
curl.http_auth_types = :basic
|
390
|
-
curl.username = settings.config[:jira_user]
|
391
|
-
curl.password = settings.config[:jira_pass]
|
392
|
-
curl.verbose = true
|
393
|
-
end
|
394
|
-
|
395
|
-
raw_response = curl_request.body_str
|
396
|
-
begin
|
397
|
-
data = JSON::parse(raw_response)
|
398
|
-
keys.each { |k| data = data[k] }
|
399
|
-
return data unless data.nil?
|
400
|
-
rescue
|
401
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
402
|
-
end
|
403
|
-
|
404
|
-
return sprints.last
|
405
|
-
end
|
406
|
-
|
407
|
-
def today offset=0
|
408
|
-
todays_date(offset).strftime '%Y-%m-%d'
|
409
|
-
end
|
410
|
-
|
411
|
-
def tomorrow
|
412
|
-
today(one_day)
|
413
|
-
end
|
414
|
-
|
415
|
-
def name_for_today offset=0
|
416
|
-
todays_date(offset).strftime '%A %-d %b'
|
417
|
-
end
|
418
|
-
|
419
|
-
def name_for_tomorrow
|
420
|
-
name_for_today(one_day)
|
421
|
-
end
|
422
|
-
|
423
|
-
def name_for_coming_week
|
424
|
-
todays_date.strftime 'Week of %-d %b'
|
425
|
-
end
|
426
|
-
|
427
|
-
def jiras_for date
|
428
|
-
return [] unless logged_in?
|
429
|
-
unless ops?
|
430
|
-
return @jira_client.Issue.jql normalized_jql("due = #{date} and type != Change and labels in (OpsAsk) and labels not in (OpsOnly)"), max_results: 100
|
431
|
-
end
|
432
|
-
return @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk) and type != Change"), max_results: 100
|
433
|
-
end
|
434
|
-
|
435
|
-
def jira_count_for date
|
436
|
-
jiras_for(date).length
|
437
|
-
end
|
438
|
-
|
439
|
-
def jira_count_for_today ; jira_count_for(today) end
|
440
|
-
|
441
|
-
def jira_count_for_tomorrow ; jira_count_for(tomorrow) end
|
442
|
-
|
443
|
-
def raw_classes_for jira
|
444
|
-
classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
|
445
|
-
classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
|
446
|
-
end
|
447
|
-
|
448
|
-
def classes_for jira
|
449
|
-
raw_classes_for(jira).join(' ')
|
450
|
-
end
|
451
|
-
|
452
|
-
def sorting_key_for jira
|
453
|
-
rcs = raw_classes_for(jira)
|
454
|
-
idx = 1
|
455
|
-
idx = 2 if rcs.include? 'denimcores'
|
456
|
-
idx = 0 if rcs.include? 'closed'
|
457
|
-
return "#{idx}-#{jira.key}"
|
458
|
-
end
|
459
|
-
|
460
|
-
def issues_for date
|
461
|
-
jiras_for(date).sort_by do |jira|
|
462
|
-
sorting_key_for(jira)
|
463
|
-
end.reverse
|
464
|
-
end
|
465
|
-
|
466
|
-
def its_the_weekend?
|
467
|
-
now.saturday? || now.sunday?
|
468
|
-
end
|
469
|
-
|
470
|
-
def room_for_new_jiras_for? date
|
471
|
-
return true if ops?
|
472
|
-
jira_count_for(date) < settings.config[:queue_size]
|
473
|
-
end
|
474
|
-
|
475
|
-
def date_for_new_jiras
|
476
|
-
if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
|
477
|
-
return today if room_for_new_jiras_for? today
|
478
|
-
return tomorrow if room_for_new_jiras_for? tomorrow
|
479
|
-
else
|
480
|
-
return tomorrow if room_for_new_jiras_for? tomorrow
|
481
|
-
end
|
482
|
-
return nil
|
483
|
-
end
|
484
|
-
|
485
|
-
def room_for_new_jiras?
|
486
|
-
return true if ops?
|
487
|
-
!date_for_new_jiras.nil?
|
488
|
-
end
|
489
|
-
|
490
|
-
def validate_room_for_new_jiras
|
491
|
-
duedate = date_for_new_jiras
|
492
|
-
return duedate unless duedate.nil?
|
493
|
-
flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
|
494
|
-
redirect '/'
|
495
|
-
end
|
496
|
-
|
497
|
-
def validate_jira_params
|
498
|
-
flash[:error] = []
|
499
|
-
flash[:error] << 'Summary is required' if params['jira-summary'].empty?
|
500
|
-
redirect '/' unless flash[:error].empty?
|
501
|
-
return [
|
502
|
-
params['jira-component'],
|
503
|
-
params['jira-summary'],
|
504
|
-
params['jira-description'],
|
505
|
-
!!params['jira-assign_to_me'],
|
506
|
-
params['jira-epic'],
|
507
|
-
!!params['jira-ops_only']
|
508
|
-
]
|
509
|
-
end
|
510
|
-
|
511
|
-
def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
512
|
-
epic = 'INF-3091' if epic.nil? # OpsAsk default epic
|
513
|
-
assignee = assign_to_me ? @me : settings.config[:assignee]
|
514
|
-
components = []
|
515
|
-
components = [ { name: component } ] unless component
|
516
|
-
labels = [ 'OpsAsk', current_sprint_name ].compact
|
517
|
-
labels << 'OpsOnly' if ops_only
|
518
|
-
labels << settings.config[:require_label] if settings.config[:require_label]
|
519
|
-
data = {
|
520
|
-
fields: {
|
521
|
-
project: { key: settings.config[:project_key] },
|
522
|
-
issuetype: { name: settings.config[:issue_type] },
|
523
|
-
versions: [ { name: settings.config[:version] } ],
|
524
|
-
duedate: duedate,
|
525
|
-
summary: summary,
|
526
|
-
description: description,
|
527
|
-
components: components,
|
528
|
-
assignee: { name: assignee },
|
529
|
-
reporter: { name: @me },
|
530
|
-
labels: labels,
|
531
|
-
customfield_10002: 1, # Story Points = 1
|
532
|
-
# customfield_10350: epic,
|
533
|
-
customfield_10040: { id: '-1' } # Release Priority = None
|
534
|
-
}
|
535
|
-
}
|
536
|
-
|
537
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
|
538
|
-
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
539
|
-
curl.headers['Accept'] = 'application/json'
|
540
|
-
curl.headers['Content-Type'] = 'application/json'
|
541
|
-
curl.http_auth_types = :basic
|
542
|
-
curl.username = settings.config[:jira_user]
|
543
|
-
curl.password = settings.config[:jira_pass]
|
544
|
-
curl.verbose = true
|
545
|
-
end
|
546
|
-
|
547
|
-
raw_response = curl_request.body_str
|
548
|
-
begin
|
549
|
-
response = JSON::parse raw_response
|
550
|
-
rescue
|
551
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
552
|
-
return nil
|
553
|
-
end
|
554
|
-
return response
|
555
|
-
end
|
556
|
-
|
557
|
-
def components
|
558
|
-
return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
|
559
|
-
end
|
560
|
-
|
561
|
-
def untracked_issues
|
562
|
-
return [] unless logged_in?
|
563
|
-
constraints = [
|
564
|
-
"due < #{today}",
|
565
|
-
"resolution = unresolved",
|
566
|
-
"assignee = denimcores"
|
567
|
-
].join(' and ')
|
568
|
-
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
569
|
-
sorting_key_for(jira)
|
570
|
-
end.reverse
|
571
|
-
end
|
572
|
-
|
573
|
-
def straggling_issues
|
574
|
-
return [] unless logged_in?
|
575
|
-
constraints = [
|
576
|
-
"due < #{today}",
|
577
|
-
"labels in (OpsAsk)",
|
578
|
-
"resolution = unresolved",
|
579
|
-
"assignee != denimcores"
|
580
|
-
].join(' and ')
|
581
|
-
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
582
|
-
sorting_key_for(jira)
|
583
|
-
end.reverse
|
584
|
-
end
|
585
|
-
|
586
|
-
def epics
|
587
|
-
data = {
|
588
|
-
jql: normalized_jql("type = Epic"),
|
589
|
-
startAt: 0,
|
590
|
-
maxResults: 1000
|
591
|
-
}
|
592
|
-
|
593
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/search"
|
594
|
-
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
595
|
-
curl.headers['Accept'] = 'application/json'
|
596
|
-
curl.headers['Content-Type'] = 'application/json'
|
597
|
-
curl.http_auth_types = :basic
|
598
|
-
curl.username = settings.config[:jira_user]
|
599
|
-
curl.password = settings.config[:jira_pass]
|
600
|
-
curl.verbose = true
|
601
|
-
end
|
602
|
-
|
603
|
-
raw_response = curl_request.body_str
|
604
|
-
begin
|
605
|
-
response = JSON::parse raw_response
|
606
|
-
rescue
|
607
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
608
|
-
return nil
|
609
|
-
end
|
610
|
-
return response['issues'].map do |epic|
|
611
|
-
{
|
612
|
-
'key' => epic['key'],
|
613
|
-
'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
|
614
|
-
}
|
615
|
-
end
|
616
|
-
end
|
617
|
-
|
618
|
-
def epic key
|
619
|
-
url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
|
620
|
-
curl_request = Curl::Easy.http_get(url) do |curl|
|
621
|
-
curl.headers['Accept'] = 'application/json'
|
622
|
-
curl.headers['Content-Type'] = 'application/json'
|
623
|
-
curl.http_auth_types = :basic
|
624
|
-
curl.username = settings.config[:jira_user]
|
625
|
-
curl.password = settings.config[:jira_pass]
|
626
|
-
curl.verbose = true
|
627
|
-
end
|
628
|
-
|
629
|
-
raw_response = curl_request.body_str
|
630
|
-
begin
|
631
|
-
response = JSON::parse raw_response
|
632
|
-
rescue
|
633
|
-
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
634
|
-
return nil
|
635
|
-
end
|
636
|
-
return {
|
637
|
-
'key' => response['key'],
|
638
|
-
'name' => response['fields']['customfield_10351'] || response['fields']['summary']
|
639
|
-
}
|
640
|
-
end
|
641
|
-
|
642
|
-
def normalized_jql query, \
|
643
|
-
project=settings.config[:project_name], \
|
644
|
-
require_label=settings.config[:require_label],
|
645
|
-
ignore_label=settings.config[:ignore_label]
|
646
|
-
# ...
|
647
|
-
query += %Q| and project = #{project}| if project
|
648
|
-
query += %Q| and labels = #{require_label}| if require_label
|
649
|
-
query += %Q| and (labels != #{ignore_label} OR labels is empty)| if ignore_label
|
650
|
-
return query
|
651
|
-
end
|
652
231
|
end
|
653
232
|
end
|
@@ -0,0 +1,426 @@
|
|
1
|
+
module OpsAsk
|
2
|
+
module Helpers
|
3
|
+
def logged_in?
|
4
|
+
!!session[:jira_auth]
|
5
|
+
end
|
6
|
+
|
7
|
+
def ops?
|
8
|
+
return false unless logged_in?
|
9
|
+
@myself['groups']['items'].each do |i|
|
10
|
+
return true if i['name'] == settings.config[:ops_group]
|
11
|
+
end
|
12
|
+
return false
|
13
|
+
end
|
14
|
+
|
15
|
+
def one_day
|
16
|
+
1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
|
17
|
+
end
|
18
|
+
|
19
|
+
def now
|
20
|
+
Time.now # + 3 * one_day # DEBUG
|
21
|
+
end
|
22
|
+
|
23
|
+
def todays_date offset=0
|
24
|
+
date = now + offset
|
25
|
+
date += one_day if date.saturday?
|
26
|
+
date += one_day if date.sunday?
|
27
|
+
return date
|
28
|
+
end
|
29
|
+
|
30
|
+
def stats_for issues, resolved_link, unresolved_link
|
31
|
+
return {} unless logged_in?
|
32
|
+
return {} unless issues
|
33
|
+
|
34
|
+
resolved_issues, unresolved_issues = [], []
|
35
|
+
|
36
|
+
issues.map! do |i|
|
37
|
+
key = i['key']
|
38
|
+
status = i['fields']['status']['name']
|
39
|
+
resolution = i['fields']['resolution']['name'] rescue nil
|
40
|
+
points = i['fields']['customfield_10002'].to_i
|
41
|
+
|
42
|
+
issue = {
|
43
|
+
key: key,
|
44
|
+
status: status,
|
45
|
+
resolution: resolution,
|
46
|
+
points: points
|
47
|
+
}
|
48
|
+
|
49
|
+
if resolution.nil?
|
50
|
+
unresolved_issues << issue
|
51
|
+
else
|
52
|
+
resolved_issues << issue
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
resolved: {
|
58
|
+
number: resolved_issues.size,
|
59
|
+
points: resolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
60
|
+
link: resolved_link
|
61
|
+
},
|
62
|
+
unresolved: {
|
63
|
+
number: unresolved_issues.size,
|
64
|
+
points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+),
|
65
|
+
link: unresolved_link
|
66
|
+
}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def items_in_current_sprint
|
71
|
+
items_in_sprint current_sprint_num
|
72
|
+
end
|
73
|
+
|
74
|
+
def items_in_sprint num
|
75
|
+
return [] unless logged_in?
|
76
|
+
issues = []
|
77
|
+
id = get_sprint(num)['id']
|
78
|
+
query = normalized_jql("sprint = #{id}", nil)
|
79
|
+
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
80
|
+
issues << i.attrs
|
81
|
+
end
|
82
|
+
return issues
|
83
|
+
end
|
84
|
+
|
85
|
+
def asks_in_current_sprint
|
86
|
+
asks_in_sprint current_sprint_num
|
87
|
+
end
|
88
|
+
|
89
|
+
def asks_in_sprint num
|
90
|
+
return [] unless logged_in?
|
91
|
+
issues = []
|
92
|
+
query = normalized_jql("labels in (Sprint#{num})", nil)
|
93
|
+
@jira_client.Issue.jql(query, max_results: 500).each do |i|
|
94
|
+
issues << i.attrs
|
95
|
+
end
|
96
|
+
return issues
|
97
|
+
end
|
98
|
+
|
99
|
+
def sprints
|
100
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
|
101
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
102
|
+
curl.headers['Accept'] = 'application/json'
|
103
|
+
curl.headers['Content-Type'] = 'application/json'
|
104
|
+
curl.http_auth_types = :basic
|
105
|
+
curl.username = settings.config[:jira_user]
|
106
|
+
curl.password = settings.config[:jira_pass]
|
107
|
+
curl.verbose = true
|
108
|
+
end
|
109
|
+
|
110
|
+
raw_response = curl_request.body_str
|
111
|
+
begin
|
112
|
+
data = JSON::parse(raw_response)
|
113
|
+
return data['sprints']
|
114
|
+
rescue
|
115
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
116
|
+
end
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def get_sprint num
|
121
|
+
sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
|
122
|
+
sprint_id = sprint.first['id']
|
123
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
|
124
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
125
|
+
curl.headers['Accept'] = 'application/json'
|
126
|
+
curl.headers['Content-Type'] = 'application/json'
|
127
|
+
curl.http_auth_types = :basic
|
128
|
+
curl.username = settings.config[:jira_user]
|
129
|
+
curl.password = settings.config[:jira_pass]
|
130
|
+
curl.verbose = true
|
131
|
+
end
|
132
|
+
|
133
|
+
raw_response = curl_request.body_str
|
134
|
+
begin
|
135
|
+
data = JSON::parse(raw_response)
|
136
|
+
contents = data.delete('contents')
|
137
|
+
data = data.delete('sprint')
|
138
|
+
return data.merge(contents)
|
139
|
+
rescue
|
140
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
141
|
+
end
|
142
|
+
return {}
|
143
|
+
end
|
144
|
+
|
145
|
+
def current_sprint_name sprint=current_sprint
|
146
|
+
sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
|
147
|
+
end
|
148
|
+
|
149
|
+
def current_sprint_num sprint=current_sprint
|
150
|
+
sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '')
|
151
|
+
end
|
152
|
+
|
153
|
+
def current_sprint_id sprint=current_sprint
|
154
|
+
sprint.nil? ? nil : sprint['id']
|
155
|
+
end
|
156
|
+
|
157
|
+
def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
|
158
|
+
url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
|
159
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
160
|
+
curl.headers['Accept'] = 'application/json'
|
161
|
+
curl.headers['Content-Type'] = 'application/json'
|
162
|
+
curl.http_auth_types = :basic
|
163
|
+
curl.username = settings.config[:jira_user]
|
164
|
+
curl.password = settings.config[:jira_pass]
|
165
|
+
curl.verbose = true
|
166
|
+
end
|
167
|
+
|
168
|
+
raw_response = curl_request.body_str
|
169
|
+
begin
|
170
|
+
data = JSON::parse(raw_response)
|
171
|
+
keys.each { |k| data = data[k] }
|
172
|
+
return data unless data.nil?
|
173
|
+
rescue
|
174
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
175
|
+
end
|
176
|
+
|
177
|
+
return sprints.last
|
178
|
+
end
|
179
|
+
|
180
|
+
def today offset=0
|
181
|
+
todays_date(offset).strftime '%Y-%m-%d'
|
182
|
+
end
|
183
|
+
|
184
|
+
def tomorrow
|
185
|
+
today(one_day)
|
186
|
+
end
|
187
|
+
|
188
|
+
def name_for_today offset=0
|
189
|
+
todays_date(offset).strftime '%A %-d %b'
|
190
|
+
end
|
191
|
+
|
192
|
+
def name_for_tomorrow
|
193
|
+
name_for_today(one_day)
|
194
|
+
end
|
195
|
+
|
196
|
+
def name_for_coming_week
|
197
|
+
todays_date.strftime 'Week of %-d %b'
|
198
|
+
end
|
199
|
+
|
200
|
+
def jiras_for date
|
201
|
+
return [] unless logged_in?
|
202
|
+
unless ops?
|
203
|
+
return @jira_client.Issue.jql normalized_jql("due = #{date} and type != Change and labels in (OpsAsk) and labels not in (OpsOnly)"), max_results: 100
|
204
|
+
end
|
205
|
+
return @jira_client.Issue.jql normalized_jql("due = #{date} and labels in (OpsAsk) and type != Change"), max_results: 100
|
206
|
+
end
|
207
|
+
|
208
|
+
def jira_count_for date
|
209
|
+
jiras_for(date).length
|
210
|
+
end
|
211
|
+
|
212
|
+
def jira_count_for_today ; jira_count_for(today) end
|
213
|
+
|
214
|
+
def jira_count_for_tomorrow ; jira_count_for(tomorrow) end
|
215
|
+
|
216
|
+
def raw_classes_for jira
|
217
|
+
classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
|
218
|
+
classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
|
219
|
+
end
|
220
|
+
|
221
|
+
def classes_for jira
|
222
|
+
raw_classes_for(jira).join(' ')
|
223
|
+
end
|
224
|
+
|
225
|
+
def sorting_key_for jira
|
226
|
+
rcs = raw_classes_for(jira)
|
227
|
+
idx = 1
|
228
|
+
idx = 2 if rcs.include? 'denimcores'
|
229
|
+
idx = 0 if rcs.include? 'closed'
|
230
|
+
return "#{idx}-#{jira.key}"
|
231
|
+
end
|
232
|
+
|
233
|
+
def issues_for date
|
234
|
+
jiras_for(date).sort_by do |jira|
|
235
|
+
sorting_key_for(jira)
|
236
|
+
end.reverse
|
237
|
+
end
|
238
|
+
|
239
|
+
def its_the_weekend?
|
240
|
+
now.saturday? || now.sunday?
|
241
|
+
end
|
242
|
+
|
243
|
+
def room_for_new_jiras_for? date
|
244
|
+
return true if ops?
|
245
|
+
jira_count_for(date) < settings.config[:queue_size]
|
246
|
+
end
|
247
|
+
|
248
|
+
def date_for_new_jiras
|
249
|
+
if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
|
250
|
+
return today if room_for_new_jiras_for? today
|
251
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
252
|
+
else
|
253
|
+
return tomorrow if room_for_new_jiras_for? tomorrow
|
254
|
+
end
|
255
|
+
return nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def room_for_new_jiras?
|
259
|
+
return true if ops?
|
260
|
+
!date_for_new_jiras.nil?
|
261
|
+
end
|
262
|
+
|
263
|
+
def validate_room_for_new_jiras
|
264
|
+
duedate = date_for_new_jiras
|
265
|
+
return duedate unless duedate.nil?
|
266
|
+
flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
|
267
|
+
redirect '/'
|
268
|
+
end
|
269
|
+
|
270
|
+
def validate_jira_params
|
271
|
+
flash[:error] = []
|
272
|
+
flash[:error] << 'Summary is required' if params['jira-summary'].empty?
|
273
|
+
redirect '/' unless flash[:error].empty?
|
274
|
+
return [
|
275
|
+
params['jira-component'],
|
276
|
+
params['jira-summary'],
|
277
|
+
params['jira-description'],
|
278
|
+
!!params['jira-assign_to_me'],
|
279
|
+
params['jira-epic'],
|
280
|
+
!!params['jira-ops_only']
|
281
|
+
]
|
282
|
+
end
|
283
|
+
|
284
|
+
def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
|
285
|
+
epic = 'INF-3091' if epic.nil? # OpsAsk default epic
|
286
|
+
assignee = assign_to_me ? @me : settings.config[:assignee]
|
287
|
+
components = []
|
288
|
+
components = [ { name: component } ] unless component
|
289
|
+
labels = [ 'OpsAsk', current_sprint_name ].compact
|
290
|
+
labels << 'OpsOnly' if ops_only
|
291
|
+
labels << settings.config[:require_label] if settings.config[:require_label]
|
292
|
+
data = {
|
293
|
+
fields: {
|
294
|
+
project: { key: settings.config[:project_key] },
|
295
|
+
issuetype: { name: settings.config[:issue_type] },
|
296
|
+
versions: [ { name: settings.config[:version] } ],
|
297
|
+
duedate: duedate,
|
298
|
+
summary: summary,
|
299
|
+
description: description,
|
300
|
+
components: components,
|
301
|
+
assignee: { name: assignee },
|
302
|
+
reporter: { name: @me },
|
303
|
+
labels: labels,
|
304
|
+
customfield_10002: 1, # Story Points = 1
|
305
|
+
# customfield_10350: epic,
|
306
|
+
customfield_10040: { id: '-1' } # Release Priority = None
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
|
311
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
312
|
+
curl.headers['Accept'] = 'application/json'
|
313
|
+
curl.headers['Content-Type'] = 'application/json'
|
314
|
+
curl.http_auth_types = :basic
|
315
|
+
curl.username = settings.config[:jira_user]
|
316
|
+
curl.password = settings.config[:jira_pass]
|
317
|
+
curl.verbose = true
|
318
|
+
end
|
319
|
+
|
320
|
+
raw_response = curl_request.body_str
|
321
|
+
begin
|
322
|
+
response = JSON::parse raw_response
|
323
|
+
rescue
|
324
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
325
|
+
return nil
|
326
|
+
end
|
327
|
+
return response
|
328
|
+
end
|
329
|
+
|
330
|
+
def components
|
331
|
+
return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
|
332
|
+
end
|
333
|
+
|
334
|
+
def untracked_issues
|
335
|
+
return [] unless logged_in?
|
336
|
+
constraints = [
|
337
|
+
"due < #{today}",
|
338
|
+
"resolution = unresolved",
|
339
|
+
"assignee = denimcores"
|
340
|
+
].join(' and ')
|
341
|
+
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
342
|
+
sorting_key_for(jira)
|
343
|
+
end.reverse
|
344
|
+
end
|
345
|
+
|
346
|
+
def straggling_issues
|
347
|
+
return [] unless logged_in?
|
348
|
+
constraints = [
|
349
|
+
"due < #{today}",
|
350
|
+
"labels in (OpsAsk)",
|
351
|
+
"resolution = unresolved",
|
352
|
+
"assignee != denimcores"
|
353
|
+
].join(' and ')
|
354
|
+
@jira_client.Issue.jql(normalized_jql(constraints), max_results: 100).sort_by do |jira|
|
355
|
+
sorting_key_for(jira)
|
356
|
+
end.reverse
|
357
|
+
end
|
358
|
+
|
359
|
+
def epics
|
360
|
+
data = {
|
361
|
+
jql: normalized_jql("type = Epic"),
|
362
|
+
startAt: 0,
|
363
|
+
maxResults: 1000
|
364
|
+
}
|
365
|
+
|
366
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/search"
|
367
|
+
curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
|
368
|
+
curl.headers['Accept'] = 'application/json'
|
369
|
+
curl.headers['Content-Type'] = 'application/json'
|
370
|
+
curl.http_auth_types = :basic
|
371
|
+
curl.username = settings.config[:jira_user]
|
372
|
+
curl.password = settings.config[:jira_pass]
|
373
|
+
curl.verbose = true
|
374
|
+
end
|
375
|
+
|
376
|
+
raw_response = curl_request.body_str
|
377
|
+
begin
|
378
|
+
response = JSON::parse raw_response
|
379
|
+
rescue
|
380
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
381
|
+
return nil
|
382
|
+
end
|
383
|
+
return response['issues'].map do |epic|
|
384
|
+
{
|
385
|
+
'key' => epic['key'],
|
386
|
+
'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def epic key
|
392
|
+
url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
|
393
|
+
curl_request = Curl::Easy.http_get(url) do |curl|
|
394
|
+
curl.headers['Accept'] = 'application/json'
|
395
|
+
curl.headers['Content-Type'] = 'application/json'
|
396
|
+
curl.http_auth_types = :basic
|
397
|
+
curl.username = settings.config[:jira_user]
|
398
|
+
curl.password = settings.config[:jira_pass]
|
399
|
+
curl.verbose = true
|
400
|
+
end
|
401
|
+
|
402
|
+
raw_response = curl_request.body_str
|
403
|
+
begin
|
404
|
+
response = JSON::parse raw_response
|
405
|
+
rescue
|
406
|
+
$stderr.puts "Failed to parse response from JIRA: #{raw_response}"
|
407
|
+
return nil
|
408
|
+
end
|
409
|
+
return {
|
410
|
+
'key' => response['key'],
|
411
|
+
'name' => response['fields']['customfield_10351'] || response['fields']['summary']
|
412
|
+
}
|
413
|
+
end
|
414
|
+
|
415
|
+
def normalized_jql query, \
|
416
|
+
project=settings.config[:project_name], \
|
417
|
+
require_label=settings.config[:require_label],
|
418
|
+
ignore_label=settings.config[:ignore_label]
|
419
|
+
# ...
|
420
|
+
query += %Q| and project = #{project}| if project
|
421
|
+
query += %Q| and labels = #{require_label}| if require_label
|
422
|
+
query += %Q| and (labels != #{ignore_label} OR labels is empty)| if ignore_label
|
423
|
+
return query
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
data/public/css/style.css
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opsask
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sean Clemmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- bin/opsask
|
140
140
|
- lib/opsask.rb
|
141
141
|
- lib/opsask/app.rb
|
142
|
+
- lib/opsask/helpers.rb
|
142
143
|
- lib/opsask/main.rb
|
143
144
|
- lib/opsask/metadata.rb
|
144
145
|
- opsask.gemspec
|