cl-magic 0.3.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aee6512ae8cbee738242f87ec89baa10a56c04c2600fdf766c93f7844d6d6a38
4
- data.tar.gz: d43a49eca90391948305353fa7d0242bf5bf14504492531bf9cda43bd7a4d0cf
3
+ metadata.gz: '08b4655abcd92a0d5e3a779fdc7e0e09231c7cb15d073e06878c085227ef3f9d'
4
+ data.tar.gz: 63999d535d6ad18d22b8cff1e259b9b054bbf7faf75939e54c9819885ca24feb
5
5
  SHA512:
6
- metadata.gz: 3b040f7fe5dfccd9c62395d433a1c445f9c246e27b98636c0af318beca7e62ac7a4a082bdfdf1f68e8e602b2652ff3a9c961f611a317de5d2555262652405544
7
- data.tar.gz: 4a0ced863718cfe30127ff57f18698ef641ae520245094166d511310b7995f55c08e3644e8028044e27389681250e8c63c7850bf44c43e933546aa8bc2bbb557
6
+ metadata.gz: 4157d207e824e2d0a309790187873d3b6b02c41bd892fc3436e359cc1f4639044576129d1a8d36ac9a0faee8afa6dc2366db506f93401bf4165e93766435f3d7
7
+ data.tar.gz: 0bce3b21105e81dd1bc1d5656e941ceec30814bb01e19c16a7a5ee57eba98403093d7359eb9e113bd60b0850a87764b1241a08febe1881dcde74c303fb9d35be
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cl-magic (0.3.8)
4
+ cl-magic (0.4.0)
5
+ activesupport
5
6
  optparse-subcommand
6
7
  pastel
7
8
  tty-command
@@ -11,7 +12,15 @@ PATH
11
12
  GEM
12
13
  remote: https://rubygems.org/
13
14
  specs:
15
+ activesupport (7.0.6)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
14
20
  byebug (11.1.3)
21
+ concurrent-ruby (1.2.2)
22
+ i18n (1.14.1)
23
+ concurrent-ruby (~> 1.0)
15
24
  minitest (5.18.0)
16
25
  optparse-subcommand (1.0.0)
17
26
  pastel (0.8.0)
@@ -31,6 +40,8 @@ GEM
31
40
  tty-screen (~> 0.8)
32
41
  wisper (~> 2.0)
33
42
  tty-screen (0.8.1)
43
+ tzinfo (2.0.6)
44
+ concurrent-ruby (~> 1.0)
34
45
  wisper (2.0.1)
35
46
 
36
47
  PLATFORMS
data/README.md CHANGED
@@ -20,4 +20,18 @@ cd $MAGIC_DIR bundle install --path=vendor
20
20
 
21
21
  # symlink
22
22
  ln -s $MAGIC_DIR/bin/cl /usr/local/bin
23
- ```
23
+ ```
24
+
25
+ ## Development
26
+
27
+ If you installed the gem, you need to remove the simlink
28
+
29
+ ```
30
+ rm /usr/local/bin/cl
31
+ ```
32
+
33
+ Then sim-link to your cl-magic source code's bin script
34
+
35
+ ```
36
+ ln -s $(pwd)/bin/cl /usr/local/bin
37
+ ```
data/cl-magic.gemspec CHANGED
@@ -34,5 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "tty-command"
35
35
  spec.add_dependency "tty-prompt"
36
36
  spec.add_dependency "pastel"
37
+ spec.add_dependency "activesupport"
37
38
  spec.add_development_dependency "byebug"
39
+
38
40
  end
data/lib/cl/magic/cl-auth CHANGED
@@ -41,6 +41,12 @@ end
41
41
  def aws_okta(options)
42
42
  is_tty = $stdout.isatty
43
43
 
44
+ if options[:aws_profile].nil?
45
+ puts
46
+ @logger.info "Note: have you run 'aws-okta add' already?"
47
+ puts
48
+ end
49
+
44
50
  # pick profile
45
51
  profiles = get_aws_profiles()
46
52
  profile = pick_single_result(profiles, "Pick aws profile", options[:aws_profile])
@@ -55,8 +61,20 @@ def aws_okta(options)
55
61
  # cmd = "aws-okta add" # old
56
62
  # do work
57
63
  if is_tty
64
+
65
+ # ensure we are logged in
66
+ begin
67
+ TTY::Command.new(:printer => :null).run("aws-okta exec #{options[:aws_profile]} -- env")
68
+ rescue TTY::Command::ExitError => e
69
+ if e.message.include? "Failed to authenticate"
70
+ @logger.info "authenticate first with: aws-okta add"
71
+ else
72
+ raise
73
+ end
74
+ end
75
+
58
76
  puts
59
- @logger.info "export into your environment with"
77
+ @logger.info "now export into your environment with"
60
78
  puts
61
79
  puts "export $(#{history_command})"
62
80
  else
data/lib/cl/magic/cl-dk CHANGED
@@ -41,7 +41,11 @@ end
41
41
 
42
42
  def get_repo_basename()
43
43
  command = "cd #{@working_dir} && basename $(git remote get-url origin 2> /dev/null) .git"
44
- return TTY::Command.new(:printer => :null).run(command).out.gsub('.git', '').strip.chomp
44
+ repo_basename = TTY::Command.new(:printer => :null).run(command).out.gsub('.git', '').strip.chomp
45
+ if repo_basename==".git" or repo_basename==""
46
+ return File.basename(@working_dir)
47
+ end
48
+ return repo_basename
45
49
  end
46
50
 
47
51
  def get_world_settings_filepath()
@@ -80,21 +84,23 @@ end
80
84
  #
81
85
 
82
86
  def print_dk_help_line(key, help)
83
- if help.nil?
84
- @logger.puts("#{key.ljust(15, ' ')} ???no help???")
85
- else
86
- key = key.ljust(15, ' ')
87
- help_parts = help.split(";")
87
+ if $stdout.isatty
88
+ if help.nil?
89
+ @logger.puts("#{key.ljust(15, ' ')} ???no help???")
90
+ else
91
+ key = key.ljust(15, ' ')
92
+ help_parts = help.split(";")
88
93
 
89
- # first line
90
- @logger.puts(key, help_parts.shift)
94
+ # first line
95
+ @logger.puts(key, help_parts.shift)
91
96
 
92
- # following lines
93
- padding = "".ljust(15, ' ')
94
- help_parts.each do |p|
95
- @logger.puts(padding, p)
97
+ # following lines
98
+ padding = "".ljust(15, ' ')
99
+ help_parts.each do |p|
100
+ @logger.puts(padding, p)
101
+ end
102
+ @logger.puts("") if help.end_with?(";")
96
103
  end
97
- puts("") if help.end_with?(";")
98
104
  end
99
105
  end
100
106
 
@@ -104,56 +110,60 @@ def print_dk_help(dk_parts_hash, dk_make_hash, args)
104
110
  has_dk_commands = dk_parts_hash.keys.any?
105
111
 
106
112
  if no_args or asked_for_help
107
- if has_dk_commands
108
- puts ""
109
- puts "Usage: dk [DK_PARTS] [COMPOSE_OPTIONS] COMPOSE_COMMAND"
110
- puts ""
111
- puts "Run docker compose while munging yamls in sophisticated ways."
112
- puts ""
113
- if get_repo_basename
114
- puts "PROJ INFO"
115
- puts " - Repo basename: #{get_repo_basename}"
116
- puts " - World filepath: #{get_world_project_path()}"
117
- puts ""
118
- end
119
- puts "PROJ PARTS"
120
- dk_parts_hash.keys.each do |key|
121
- print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
122
- end
123
- puts ""
124
- puts "YML TOKENS"
125
- puts " - <dk-remove> removes section of yaml"
126
- puts " - <dk-replace> replaces values in a yaml array"
127
- puts " - <dk-world-path> absolute filepath to world directory"
128
- puts " - <dk-project-path> absolute filepath to world/project directory"
129
- puts " - <dk-working-path> absolute filepath to location dk command was run from"
130
- puts ""
131
- puts "ADDITIONAL TURNKEY COMMANDS"
132
- puts " - dk set-world sets the location of the world directory"
133
- puts " - dk make turnkey commands for a project"
134
- puts " - dk save-parts save parts so they are automatically applied to commands"
113
+ puts ""
114
+ puts "Usage: dk [DK_PARTS] [COMPOSE_OPTIONS] COMPOSE_COMMAND"
115
+ puts ""
116
+ puts "Run docker compose while munging yamls in sophisticated ways."
117
+ puts ""
118
+ if get_repo_basename
119
+ puts "PROJ INFO"
120
+ puts " - Repo basename: #{get_repo_basename}"
121
+ puts " - World filepath: #{get_world_project_path()}"
135
122
  puts ""
136
- puts "-------------------------"
137
123
  end
124
+ puts "PROJ PARTS"
125
+ dk_parts_hash.keys.each do |key|
126
+ print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
127
+ end
128
+ puts ""
129
+ puts "YML TOKENS"
130
+ puts " - <dk-remove> removes section of yaml"
131
+ puts " - <dk-replace> replaces values in a yaml array"
132
+ puts " - <dk-world-path> absolute filepath to world directory"
133
+ puts " - <dk-project-path> absolute filepath to world/project directory"
134
+ puts " - <dk-working-path> absolute filepath to location dk command was run from"
135
+ puts ""
136
+ puts "ADDITIONAL TURNKEY COMMANDS"
137
+ puts " - dk set-world sets the location of the world directory"
138
+ puts " - dk make turnkey commands for a project"
139
+ puts " - dk set-parts save parts so they are automatically applied to commands"
140
+ puts ""
141
+ puts "-------------------------"
138
142
  end
139
143
  end
140
144
 
141
145
  def print_make_help(dk_parts_hash, dk_make_hash)
142
- puts ""
143
- puts "Usage: dk [DK_PARTS] make COMMAND"
144
- puts ""
145
- puts "Make commands designed to make your developer experience more turnkey"
146
- puts ""
147
- puts "Parts:"
148
- dk_parts_hash.keys.each do |key|
149
- print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
150
- end
151
- puts ""
152
- puts "Commands:"
153
- dk_make_hash.keys.each do |key|
154
- print_dk_help_line(key, dk_make_hash[key].fetch('help'))
146
+ if $stdout.isatty
147
+ puts ""
148
+ puts "Usage: dk [DK_PARTS] make COMMAND"
149
+ puts ""
150
+ puts "Make commands designed to make your developer experience more turnkey"
151
+ puts ""
152
+ puts "Parts:"
153
+ dk_parts_hash.keys.each do |key|
154
+ print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
155
+ end
156
+ puts ""
157
+ puts "Commands:"
158
+ dk_make_hash.keys.each do |key|
159
+ print_dk_help_line(key, dk_make_hash[key].fetch('help'))
160
+ end
161
+ puts ""
162
+ else
163
+ dk_make_hash.keys.each do |key|
164
+ puts key
165
+ end
155
166
  end
156
- puts ""
157
167
  end
158
168
 
159
169
  def print_set_world_help()
@@ -227,10 +237,10 @@ end
227
237
  def merge_world_files(compose_hash, show_help=false)
228
238
  dk_proj_path = get_world_project_path()
229
239
  if dk_proj_path
230
- print_dk_help_line("dk-project-path", "#{dk_proj_path}") if show_help
240
+ print_dk_help_line("dk-project-path", "#{dk_proj_path}") if show_help and $stdout.isatty
231
241
 
232
242
  Dir.glob("#{dk_proj_path}/*.yml").sort.each do |filepath|
233
- print_dk_help_line("dk-world", "#{filepath}") if show_help
243
+ print_dk_help_line("dk-world", "#{filepath}") if show_help and $stdout.isatty
234
244
 
235
245
  # read file and replace
236
246
  contents = File.read(filepath)
@@ -274,10 +284,12 @@ def merge_parts_save_and_prep_args(compose_hash, dk_parts_hash, dk_make_hash, sa
274
284
  print_dk_help(dk_parts_hash, dk_make_hash, args)
275
285
 
276
286
  def print_and_merge_part(part_key, dk_part, compose_hash)
277
- # print
278
- help_str = dk_part.fetch('help')
279
- print_dk_help_line("#{part_key}", "#{help_str ? '- ' + help_str : ''}") if dk_part.keys.any?
280
287
 
288
+ # print
289
+ if $stdout.isatty
290
+ help_str = dk_part.fetch('help')
291
+ print_dk_help_line("#{part_key}", "#{help_str ? '- ' + help_str : ''}") if dk_part.keys.any?
292
+ end
281
293
  # merge
282
294
  return dk_merge_and_remove(compose_hash, dk_part)
283
295
  end
@@ -305,7 +317,7 @@ def merge_parts_save_and_prep_args(compose_hash, dk_parts_hash, dk_make_hash, sa
305
317
  break
306
318
  end
307
319
  end
308
- puts "" # tailing line break
320
+ @logger.puts "" if $stdout.isatty # tailing line break
309
321
  tempfile.write(compose_hash.to_yaml) # write it to the tempfile
310
322
 
311
323
  # clone args
@@ -423,7 +435,7 @@ def run_dk_make(compose_args, dk_make_hash, dk_parts_hash, selected_part_keys)
423
435
  make_commands.each_with_index do |key, i|
424
436
 
425
437
  if not dk_make_hash.has_key?(key)
426
- puts "#{key} - command not found"
438
+ @logger.error "#{key} - command not found"
427
439
  exit 1
428
440
  else
429
441
 
@@ -463,7 +475,7 @@ def prep_make_command(c, selected_part_keys)
463
475
  c = interpolate_parts_into_command(c, selected_part_keys)
464
476
 
465
477
  # logging
466
- @logger.puts ""
478
+ @logger.puts "" if $stdout.isatty
467
479
  @logger.wait(c)
468
480
  cmd = "cd #{@working_dir} && #{c}"
469
481
  end
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # Fetch jira issues, status changelogs and save them to a file
3
+ require 'optparse'
4
+ require 'optparse/subcommand'
5
+ require 'tty-command'
6
+ require 'tty-prompt'
7
+
8
+ require 'cl/magic/common/common_options.rb'
9
+ require 'cl/magic/common/logging.rb'
10
+ require 'cl/magic/common/jira.rb'
11
+
12
+ require 'net/http'
13
+ require 'json'
14
+
15
+ @logger = get_logger()
16
+ @cl_cmd_name = File.basename(__FILE__).split('-').join(' ')
17
+
18
+ #
19
+ # Features
20
+ #
21
+
22
+ def do_work(options)
23
+ break_at_one_page = false # when developing, set this to true
24
+ jira = Jira.new options[:base_uri], options[:username], options[:token], break_at_one_page
25
+
26
+ puts ""
27
+ @logger.wait "fetch epics"
28
+ epic_ids, epics = jira.get_epic_ids(options[:project], options[:epic_wildcard])
29
+ @logger.success "#{epic_ids.count} epics"
30
+
31
+ puts ""
32
+ @logger.wait "fetch issues"
33
+ issues = jira.get_issues(options[:project], epic_ids)
34
+ @logger.success "#{issues.count} issues"
35
+
36
+ puts ""
37
+ @logger.wait "fetch & merge change logs"
38
+ issues = collect_status_changelogs(jira, issues, options)
39
+ @logger.success ""
40
+
41
+ puts ""
42
+ @logger.wait "saving file"
43
+ output_filepath = File.join(@working_dir, options[:output_filename])
44
+ File.open(output_filepath, 'w') do |file|
45
+ issues.each do |o|
46
+ file.puts(o.to_json)
47
+ end
48
+ end
49
+
50
+ @logger.success " #{output_filepath}"
51
+ end
52
+
53
+ #
54
+ # Options
55
+ #
56
+
57
+ options = {
58
+ output_filename: "jira-fetch-#{Time.now.strftime("%Y%m%d%H%M%S")}.datafile"
59
+ }
60
+ global_banner = <<DOC
61
+
62
+ Fetch jira issues, status changelogs and save them to a file
63
+
64
+ Usage: #{@cl_cmd_name} [options]
65
+
66
+ DOC
67
+
68
+ global = OptionParser.new do |g|
69
+ g.banner = global_banner
70
+ add_help_and_verbose(g)
71
+
72
+ g.on("--base-uri URI", "base uri for jira (ex. https://company.atlassian.net)") do |v|
73
+ options[:base_uri] = v
74
+ end
75
+
76
+ g.on("-u", "--username USERNAME", "jira username") do |v|
77
+ options[:username] = v
78
+ end
79
+
80
+ g.on("-t", "--token TOKEN", "jira token (you can create one, google it)") do |v|
81
+ options[:token] = v
82
+ end
83
+
84
+ g.on("-p", "--project KEY", "jira project to fetch from") do |v|
85
+ options[:project] = v
86
+ end
87
+
88
+ g.on("-w", "--epic-wildcard TEXT", "wildcard to filter the epics by") do |v|
89
+ options[:epic_wildcard] = v
90
+ end
91
+
92
+ g.on("-f", "--output-filename NAME", "the name of the output file (defaults to jira-fetch-timestamp.datafile)") do |v|
93
+ options[:output_filename] = v
94
+ end
95
+
96
+ end
97
+
98
+ #
99
+ # Run
100
+ #
101
+
102
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
103
+ global.parse(ARGV)
104
+
105
+ # error on token right away
106
+ if options[:token].nil?
107
+ @logger.error "missing --token"
108
+ exit
109
+ end
110
+
111
+ # prompt for missing options
112
+ ask_and_store_option(options, :base_uri, "base_uri: ")
113
+ ask_and_store_option(options, :username, "username: ")
114
+ ask_and_store_option(options, :project, "project: ")
115
+ ask_and_store_option(options, :epic_wildcard, "epic_wildcard: ")
116
+ ask_and_store_option(options, :output_filename, "output_filename: ")
117
+
118
+ # display full command
119
+ write_history("""#{@cl_cmd_name} \\
120
+ --base-uri=#{options[:base_uri]} \\
121
+ --username=#{options[:username]} \\
122
+ --project=#{options[:project]} \\
123
+ --epic-wildcard=#{options[:epic_wildcard]} \\
124
+ --token
125
+ """)
126
+
127
+ do_work(options)
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env ruby
2
+ # Fetch jira issues print stats
3
+ require 'optparse'
4
+ require 'optparse/subcommand'
5
+ require 'tty-command'
6
+ require 'tty-prompt'
7
+ require 'active_support/all'
8
+
9
+ require 'cl/magic/common/common_options.rb'
10
+ require 'cl/magic/common/logging.rb'
11
+ require 'cl/magic/common/jira.rb'
12
+
13
+ require 'net/http'
14
+ require 'json'
15
+
16
+ @logger = get_logger()
17
+ @cl_cmd_name = File.basename(__FILE__).split('-').join(' ')
18
+
19
+ #
20
+ # Features
21
+ #
22
+
23
+ def get_issues_from_datafile(options)
24
+ issues = []
25
+ filepath = File.join(@working_dir, options[:data_filepath])
26
+ File.foreach(filepath) do |line|
27
+ issue = JSON.parse(line)
28
+ issuetype = issue["fields"]["issuetype"]["name"]
29
+ labels = issue["fields"]["labels"]
30
+
31
+ has_excluded_labels = (labels & options[:exclude_labels]).any?
32
+ is_excluded_issuetype = options[:exclude_issuetypes].include?(issuetype.downcase)
33
+ issues << issue unless has_excluded_labels or is_excluded_issuetype
34
+ end
35
+ return issues
36
+ end
37
+
38
+ def in_range_issue_stats(issues, start_date, end_date, options)
39
+
40
+ def in_range_logs(start_date, end_date, issue)
41
+ return issue["status_changelogs"].select do |log|
42
+ (start_date..end_date).cover?(Date.parse(log["created"]))
43
+ end
44
+ end
45
+
46
+ # in-range
47
+ in_range_issues = issues.select do |issue|
48
+
49
+ # issue created date?
50
+ issue_in_range = Date.parse(issue["fields"]["created"]) < end_date
51
+
52
+ # logs created date?
53
+ logs_in_range = in_range_logs(start_date, end_date, issue).any?
54
+
55
+ # select if
56
+ (logs_in_range or issue_in_range)
57
+ end
58
+
59
+ # stat hashes
60
+ return in_range_issues.collect do |issue|
61
+
62
+ # most recent in-range log?
63
+ changelog = in_range_logs(start_date, end_date, issue)
64
+ .sort_by { |l| Date.parse(l["created"]) }.last
65
+
66
+ # yield stat hash
67
+ status = changelog ? changelog["toString"] : issue["fields"]["status"]["name"]
68
+ {
69
+ key: issue["key"],
70
+ issuetype: issue["fields"]["issuetype"]["name"],
71
+ status: status
72
+ }
73
+ end.compact
74
+ end
75
+
76
+ def print_stats(stat_hashes, start_date, end_date)
77
+ counts = {
78
+ start_date: start_date.strftime("%m-%d-%Y"),
79
+ end_date: end_date.strftime("%m-%d-%Y"),
80
+ total: 0,
81
+ total_todo: 0,
82
+ total_done: 0,
83
+ by_type: {}
84
+ }
85
+
86
+ stat_hashes.each do |stat|
87
+ issuetype = stat[:issuetype]
88
+ status = stat[:status]
89
+
90
+ # count by status
91
+ case status
92
+ when "To Do","Ready","Rework","In Progress","In QA","Ready For Code Review","In Code Review"
93
+ increment_type(counts, issuetype, :to_do)
94
+ increment(counts, :total_todo)
95
+ when "Ready to Deploy","Closed"
96
+ increment_type(counts, issuetype, :done)
97
+ increment(counts, :total_done)
98
+ else
99
+ # if status != "Won't Do"
100
+ # debugger
101
+ # end
102
+ end
103
+
104
+ # count totals
105
+ unless status=="Won't Do"
106
+ increment(counts, :total)
107
+ increment_type(counts, issuetype, :total)
108
+ end
109
+
110
+ end
111
+ return counts
112
+ end
113
+
114
+ def oldest_issue_date(issues)
115
+ return issues.collect {|i| Date.parse(i["fields"]["created"])}.sort.first.beginning_of_day
116
+ end
117
+
118
+ def increment_type(hash, type, status_category)
119
+ type = type.downcase.underscore
120
+ hash[:by_type][type] = {} unless hash[:by_type].key?(type)
121
+ hash[:by_type][type][status_category] ||= 0
122
+ hash[:by_type][type][status_category] += 1
123
+ end
124
+
125
+ def increment(hash, key)
126
+ hash[key] ||= 0
127
+ hash[key] += 1
128
+ end
129
+
130
+ def iter_date_range(start_date)
131
+ while start_date < Date.today.end_of_week
132
+ yield start_date.beginning_of_week.beginning_of_day, start_date.end_of_week.end_of_day
133
+ start_date += 1.weeks # increment
134
+ end
135
+ end
136
+
137
+ def do_work(options)
138
+ issues = get_issues_from_datafile(options)
139
+ oldest_date = oldest_issue_date(issues).beginning_of_week
140
+ iter_date_range(oldest_date) do |start_date, end_date|
141
+ stat_hashes = in_range_issue_stats(issues, start_date, end_date, options)
142
+ counts = print_stats(stat_hashes, start_date, end_date)
143
+ puts counts # print each time range
144
+ end
145
+ end
146
+
147
+ #
148
+ # Options
149
+ #
150
+
151
+ options = {
152
+ exclude_issuetypes: []
153
+ }
154
+ global_banner = <<DOC
155
+
156
+ Process jira fetch file an return stats
157
+
158
+ Usage: #{@cl_cmd_name} [options]
159
+
160
+ DOC
161
+
162
+ global = OptionParser.new do |g|
163
+ g.banner = global_banner
164
+ add_help_and_verbose(g)
165
+
166
+ g.on("-f", "--data-filepath FILEPATH", "relative path to file produced by 'cl jira fetch' command") do |v|
167
+ options[:data_filepath] = v
168
+ end
169
+
170
+ g.on("-e", "--exclude-issuetypes CSV", "comma separated list of issuetypes you want to exclude") do |v|
171
+ options[:exclude_issuetypes] = v.split(',')
172
+ options[:exclude_issuetypes] << "Won't Do"
173
+ end
174
+
175
+ g.on("-l", "--exclude-labels CSV", "comma separated list of labels that will cause a ticket to be excluded") do |v|
176
+ options[:exclude_labels] = v.split(',')
177
+ end
178
+ end
179
+
180
+ #
181
+ # Run
182
+ #
183
+
184
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
185
+ global.parse(ARGV)
186
+
187
+ # prompt for missing options
188
+ ask_and_store_option(options, :data_filepath, "data_filepath: ")
189
+ options[:exclude_issuetypes] = [] if options[:exclude_issuetypes].nil?
190
+ options[:exclude_labels] = [] if options[:exclude_labels].nil?
191
+
192
+ # display full command
193
+ write_history("""#{@cl_cmd_name} \\
194
+ --data-filepath=#{options[:data_filepath]} \\
195
+ --exclude-issuetypes=#{options[:exclude_issuetypes].join(',')} \\
196
+ --exclude-labels=#{options[:exclude_labels].join(',')}
197
+ """)
198
+
199
+ do_work(options)
data/lib/cl/magic/cl-poll CHANGED
@@ -17,10 +17,10 @@ require 'cl/magic/common/kubectl.rb'
17
17
  # Features
18
18
  #
19
19
 
20
- def do_work(options)
20
+ def do_work(options, remaining_options)
21
21
  while true
22
- tty_command = TTY::Command.new(printer: :null)
23
- out, err = tty_command.run(ARGV.join(' '))
22
+ tty_command = TTY::Command.new(printer: :null, pty: options[:pty])
23
+ out, err = tty_command.run("cd #{@working_dir} && #{remaining_options.join(' ')}")
24
24
  puts out
25
25
  puts
26
26
  sleep(1)
@@ -31,7 +31,9 @@ end
31
31
  # Options
32
32
  #
33
33
 
34
- options = {}
34
+ options = {
35
+ pty: true
36
+ }
35
37
  global_banner = <<DOC
36
38
 
37
39
  A sandbox to try things
@@ -43,10 +45,23 @@ DOC
43
45
  global = OptionParser.new do |g|
44
46
  g.banner = global_banner
45
47
  add_help_and_verbose(g)
48
+
49
+ g.on("--no-pty", "disable pseudo terminal") do |v|
50
+ options[:pty] = v
51
+ end
46
52
  end
47
53
 
48
54
  #
49
55
  # Run
50
56
  #
51
57
 
52
- do_work(options)
58
+ remaining_options = []
59
+ global.order! do |option|
60
+ remaining_options << option
61
+ end
62
+
63
+ global.parse(ARGV)
64
+
65
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
66
+
67
+ do_work(options, remaining_options)
@@ -1,3 +1,4 @@
1
+ require 'tty-prompt'
1
2
 
2
3
  def add_help_and_verbose(opts)
3
4
  add_verbose(opts)
@@ -13,4 +14,10 @@ def add_help(opts)
13
14
  puts opts
14
15
  exit
15
16
  end
16
- end
17
+ end
18
+
19
+ def ask_and_store_option(options, key, question)
20
+ if options[key].nil?
21
+ options[key] = TTY::Prompt.new.ask(question)
22
+ end
23
+ end
@@ -0,0 +1,163 @@
1
+
2
+ class Jira
3
+
4
+ def initialize(base_uri, username, token, break_at_one_page=false)
5
+ @base_uri = base_uri.chomp("/")
6
+ @username = username
7
+ @token = token
8
+ @break_at_one_page = break_at_one_page
9
+ end
10
+
11
+ #
12
+ # Fetch: Issues & Change Logs
13
+ #
14
+
15
+ def get_epic_ids(project, epic_wildcard)
16
+ jql_query = "project = \"#{project}\" AND issuetype = Epic AND text ~ \"#{epic_wildcard}\""
17
+ results = run_jql_query(jql_query)
18
+ epics = results.select{|h| h['fields']['summary'].start_with? epic_wildcard}
19
+ epic_ids = epics.map {|h| h['id']}
20
+ return epic_ids, epics
21
+ end
22
+
23
+ def get_issues(project, epic_ids)
24
+ jql_query = "project = \"#{project}\" AND parentEpic IN (#{epic_ids.join(',')})"
25
+ return run_jql_query(jql_query)
26
+ end
27
+
28
+ def get_issue_status_changelog(issue_key)
29
+ uri = URI.parse("#{@base_uri}/rest/api/2/issue/#{issue_key}/changelog")
30
+ jira_get(uri) do |response|
31
+ result = JSON.parse(response.body)
32
+ return result["values"]
33
+ end
34
+ end
35
+
36
+ #
37
+ # Helpers: GET & POST
38
+ #
39
+
40
+ def jira_get(uri)
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = true
43
+
44
+ # get request
45
+ request = Net::HTTP::Get.new(uri.path)
46
+ request.basic_auth(@username, @token)
47
+
48
+ # fetch
49
+ response = http.request(request)
50
+ if response.code == '200'
51
+ yield response
52
+ else
53
+ raise "Jira query failed with HTTP status code #{response.code}"
54
+ end
55
+ end
56
+
57
+ def jira_post(uri, body)
58
+ http = Net::HTTP.new(uri.host, uri.port)
59
+ http.use_ssl = true
60
+
61
+ # post request
62
+ request = Net::HTTP::Post.new(uri.path)
63
+ request.basic_auth(@username, @token)
64
+ request.content_type = 'application/json'
65
+ request.body = body.to_json
66
+
67
+ # fetch
68
+ response = http.request(request)
69
+ if response.code == '200'
70
+ yield response
71
+ else
72
+ raise "Jira query failed with HTTP status code #{response.code}"
73
+ end
74
+ end
75
+
76
+ #
77
+ # Fetch: JQL Query
78
+ #
79
+
80
+ def run_jql_query(jql)
81
+ start_at = 0
82
+ max_results = 50
83
+ total_results = nil
84
+ all_results = []
85
+
86
+ page_loop = true
87
+ while page_loop
88
+
89
+ uri = URI("#{@base_uri}/rest/api/2/search")
90
+ body = { jql: jql, startAt: start_at, maxResults: max_results }
91
+
92
+ # post
93
+ jira_post(uri, body) do |response|
94
+ result = JSON.parse(response.body)
95
+
96
+ # get issues
97
+ issues = result['issues']
98
+ all_results += issues
99
+
100
+ # debug: one page only
101
+ if @break_at_one_page
102
+ page_loop = false
103
+ break
104
+ end
105
+
106
+ # paginate
107
+ total_results ||= result['total']
108
+ if all_results.count == total_results
109
+ page_loop = false # we got them all, stop paging
110
+ else
111
+ start_at += max_results # else next page
112
+ end
113
+ end
114
+
115
+ print '.' # loop
116
+ end
117
+ all_results.map {|h| h}
118
+ end
119
+ end
120
+
121
+ #
122
+ # Collect status changelogs
123
+ #
124
+ # Given a array of jira issue hashes
125
+ # * fetch the change log
126
+ # * filter down to status changes
127
+ # * add it to the issue hash as ["status_changelogs"]
128
+ #
129
+
130
+ def collect_status_changelogs(jira, issues, options)
131
+ final_issue_hashes = []
132
+
133
+ issues.each do |issue|
134
+ issue_key = issue["key"]
135
+ issue["status_changelogs"] = []
136
+
137
+ # fetch change log
138
+ print '.'
139
+ changelogs = jira.get_issue_status_changelog(issue_key)
140
+
141
+ changelogs.each do |change_log|
142
+
143
+ # all items that are status changes
144
+ status_logs = change_log["items"].select {|i| i["field"]=="status"}
145
+ status_logs = status_logs.collect do |status_log|
146
+ {
147
+ "key": issue_key,
148
+ "created": change_log["created"],
149
+ "toString": status_log["toString"],
150
+ "fromString": status_log["fromString"]
151
+ }
152
+ end
153
+
154
+ # append them to issue
155
+ status_logs.each do |status_log|
156
+ issue["status_changelogs"] << status_log
157
+ end
158
+ end
159
+
160
+ final_issue_hashes << issue # save
161
+ end
162
+ return final_issue_hashes
163
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cl
4
4
  module Magic
5
- VERSION = "0.3.8"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cl-magic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Don Najd
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-21 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activesupport
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: byebug
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +146,8 @@ files:
132
146
  - lib/cl/magic/cl-gc-tags
133
147
  - lib/cl/magic/cl-glab-commit
134
148
  - lib/cl/magic/cl-history
149
+ - lib/cl/magic/cl-jira-fetch
150
+ - lib/cl/magic/cl-jira-stats
135
151
  - lib/cl/magic/cl-kube-cp
136
152
  - lib/cl/magic/cl-kube-deployment
137
153
  - lib/cl/magic/cl-kube-ktx
@@ -145,6 +161,7 @@ files:
145
161
  - lib/cl/magic/cl-vault
146
162
  - lib/cl/magic/common/common_options.rb
147
163
  - lib/cl/magic/common/gcloud.rb
164
+ - lib/cl/magic/common/jira.rb
148
165
  - lib/cl/magic/common/kubectl.rb
149
166
  - lib/cl/magic/common/logging.rb
150
167
  - lib/cl/magic/common/parse_and_pick.rb