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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.yardoc/checksums +4 -0
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/.yardoc/proxy_types +0 -0
  7. data/Gemfile +15 -0
  8. data/Gemfile.lock +75 -0
  9. data/LICENSE +13 -0
  10. data/Rakefile +30 -0
  11. data/Readme.md +3 -0
  12. data/VERSION +1 -0
  13. data/bin/opsask +4 -0
  14. data/lib/opsask/app.rb +540 -0
  15. data/lib/opsask/main.rb +54 -0
  16. data/lib/opsask/metadata.rb +29 -0
  17. data/lib/opsask.rb +3 -0
  18. data/opsask.gemspec +29 -0
  19. data/public/css/foundation.css +5068 -0
  20. data/public/css/foundation.min.css +1 -0
  21. data/public/css/normalize.css +410 -0
  22. data/public/css/style.css +152 -0
  23. data/public/favicon.ico +0 -0
  24. data/public/fonts/DINMittelschriftStd.woff +0 -0
  25. data/public/img/.gitkeep +1 -0
  26. data/public/js/foundation/foundation.abide.js +256 -0
  27. data/public/js/foundation/foundation.accordion.js +49 -0
  28. data/public/js/foundation/foundation.alert.js +37 -0
  29. data/public/js/foundation/foundation.clearing.js +485 -0
  30. data/public/js/foundation/foundation.dropdown.js +208 -0
  31. data/public/js/foundation/foundation.equalizer.js +64 -0
  32. data/public/js/foundation/foundation.interchange.js +326 -0
  33. data/public/js/foundation/foundation.joyride.js +848 -0
  34. data/public/js/foundation/foundation.js +587 -0
  35. data/public/js/foundation/foundation.magellan.js +171 -0
  36. data/public/js/foundation/foundation.offcanvas.js +39 -0
  37. data/public/js/foundation/foundation.orbit.js +464 -0
  38. data/public/js/foundation/foundation.reveal.js +399 -0
  39. data/public/js/foundation/foundation.tab.js +58 -0
  40. data/public/js/foundation/foundation.tooltip.js +215 -0
  41. data/public/js/foundation/foundation.topbar.js +387 -0
  42. data/public/js/foundation/jquery.cookie.js +8 -0
  43. data/public/js/foundation.min.js +10 -0
  44. data/public/js/opsask.js +19 -0
  45. data/public/js/vendor/fastclick.js +9 -0
  46. data/public/js/vendor/jquery.cookie.js +8 -0
  47. data/public/js/vendor/jquery.js +26 -0
  48. data/public/js/vendor/modernizr.js +8 -0
  49. data/public/js/vendor/placeholder.js +2 -0
  50. data/tasks/generate-email-summary.rb +73 -0
  51. data/tasks/send-email-summary.sh +13 -0
  52. data/views/_components.erb +6 -0
  53. data/views/_count.erb +11 -0
  54. data/views/_flash.erb +3 -0
  55. data/views/_form.erb +37 -0
  56. data/views/_queue.erb +16 -0
  57. data/views/agile.erb +6 -0
  58. data/views/glance.erb +19 -0
  59. data/views/index.erb +27 -0
  60. data/views/layout.erb +43 -0
  61. data/views/stragglers.erb +6 -0
  62. data/views/untracked.erb +6 -0
  63. 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
@@ -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
@@ -0,0 +1,3 @@
1
+ require_relative 'opsask/app'
2
+ require_relative 'opsask/main'
3
+ require_relative 'opsask/metadata'
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