cl-magic 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +12 -1
- data/README.md +15 -1
- data/cl-magic.gemspec +2 -0
- data/lib/cl/magic/cl-auth +19 -1
- data/lib/cl/magic/cl-dk +76 -64
- data/lib/cl/magic/cl-jira-fetch +127 -0
- data/lib/cl/magic/cl-jira-stats +199 -0
- data/lib/cl/magic/cl-poll +20 -5
- data/lib/cl/magic/common/common_options.rb +8 -1
- data/lib/cl/magic/common/jira.rb +163 -0
- data/lib/cl/magic/version.rb +1 -1
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08b4655abcd92a0d5e3a779fdc7e0e09231c7cb15d073e06878c085227ef3f9d'
|
4
|
+
data.tar.gz: 63999d535d6ad18d22b8cff1e259b9b054bbf7faf75939e54c9819885ca24feb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
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
|
-
|
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
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
90
|
-
|
94
|
+
# first line
|
95
|
+
@logger.puts(key, help_parts.shift)
|
91
96
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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 ""
|
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
|
-
|
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(
|
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
|
-
|
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
|
data/lib/cl/magic/version.rb
CHANGED
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.
|
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-
|
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
|