cl-magic 0.3.9 → 1.2.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.
@@ -0,0 +1,112 @@
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
+
17
+ #
18
+ # Features
19
+ #
20
+
21
+ def do_work(options)
22
+ break_at_one_page = false # when developing, set this to true
23
+ jira = Jira.new options[:base_uri], options[:username], options[:token], break_at_one_page
24
+
25
+ @logger.puts ""
26
+ @logger.wait "fetch epics"
27
+ epic_ids, epics = jira.get_epic_ids(options[:project], options[:epic_wildcard])
28
+
29
+ @logger.puts ""
30
+ @logger.wait "fetch issues"
31
+ issues = jira.get_issues_by_epic_ids(options[:project], epic_ids)
32
+
33
+ @logger.puts ""
34
+ @logger.wait "fetch change logs"
35
+ issues = jira.collect_status_changelogs(jira, issues)
36
+
37
+ @logger.puts ""
38
+ @logger.wait "fetch comments"
39
+ issues = jira.collect_comments(jira, issues)
40
+
41
+ @logger.puts ""
42
+ puts issues.to_json
43
+ end
44
+
45
+ #
46
+ # Options
47
+ #
48
+
49
+ options = {}
50
+ global_banner = <<DOC
51
+
52
+ Fetch jira issues, status changelogs and save them to a file
53
+
54
+ Usage: cl jira fetch-by-epics [options]
55
+
56
+ DOC
57
+
58
+ global = OptionParser.new do |g|
59
+ g.banner = global_banner
60
+ add_help_and_verbose(g)
61
+
62
+ g.on("--base-uri URI", "base uri for jira (ex. https://company.atlassian.net)") do |v|
63
+ options[:base_uri] = v
64
+ end
65
+
66
+ g.on("-u", "--username USERNAME", "jira username") do |v|
67
+ options[:username] = v
68
+ end
69
+
70
+ g.on("-t", "--token TOKEN", "jira token (you can create one, google it)") do |v|
71
+ options[:token] = v
72
+ end
73
+
74
+ g.on("-p", "--project KEY", "jira project to fetch from") do |v|
75
+ options[:project] = v
76
+ end
77
+
78
+ g.on("-w", "--epic-wildcard TEXT", "wildcard to filter the epics by") do |v|
79
+ options[:epic_wildcard] = v
80
+ end
81
+
82
+ end
83
+
84
+ #
85
+ # Run
86
+ #
87
+
88
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
89
+ global.parse(ARGV)
90
+
91
+ # error on token right away
92
+ if options[:token].nil?
93
+ @logger.error "missing --token"
94
+ exit
95
+ end
96
+
97
+ # prompt for missing options
98
+ ask_and_store_option(options, :base_uri, "base_uri: ")
99
+ ask_and_store_option(options, :username, "username: ")
100
+ ask_and_store_option(options, :project, "project: ")
101
+ ask_and_store_option(options, :epic_wildcard, "epic_wildcard: ")
102
+
103
+ # display full command
104
+ write_history("""cl jira fetch-by-epics \\
105
+ --base-uri=#{options[:base_uri]} \\
106
+ --username=#{options[:username]} \\
107
+ --project=#{options[:project]} \\
108
+ --epic-wildcard=#{options[:epic_wildcard]} \\
109
+ --token REDACTED
110
+ """)
111
+
112
+ do_work(options)
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+ # Jira fetch datafile to markdown
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/parse_and_pick.rb'
10
+ require 'cl/magic/common/common_options.rb'
11
+ require 'cl/magic/common/logging.rb'
12
+ require 'cl/magic/common/jira.rb'
13
+
14
+ require 'json'
15
+
16
+ @logger = get_logger()
17
+
18
+
19
+ def post(url, verb, data)
20
+ cmd = """
21
+ curl -X#{verb} \
22
+ #{url} \
23
+ -H 'Content-Type: application/json' \
24
+ -d '#{data.to_json}'
25
+ """
26
+ return `#{cmd}`
27
+ end
28
+
29
+ def create_index()
30
+ url = "#{@ELASTIC_URL}/jira"
31
+
32
+ return post(url, "PUT", {
33
+ "mappings": {
34
+ "properties": {
35
+ "text": {
36
+ "type": "text"
37
+ }
38
+ }
39
+ }
40
+ })
41
+ end
42
+
43
+ # def do_work(options, data)
44
+ # #puts create_index()
45
+
46
+ # url = "#{@ELASTIC_URL}/jira/_doc/1"
47
+ # puts post(url, "POST", {
48
+ # "text": "This is a new issue created in Jira"
49
+ # })
50
+ # end
51
+
52
+
53
+ #
54
+ # Features
55
+ #
56
+
57
+ def do_work(options)
58
+ filepath = File.join(@working_dir, options[:data_filepath])
59
+ issues = JSON.parse(File.read(filepath))
60
+ issues.each do |issue|
61
+
62
+ md = []
63
+ md << "# #{issue['key']}"
64
+ md << "project: #{issue['fields']['project']['key']}"
65
+ md << "created: #{issue['fields']['created']}"
66
+ md << "updated: #{issue['fields']['updated']}"
67
+ md << "status: #{issue['fields']['status']['statusCategory']['name']}" unless issue['fields']["status"].nil?
68
+ md << "priority: #{issue['fields']['priority']['name']}"
69
+ md << "labels: #{issue['fields']['labels'].join(',')}"
70
+ md << "issue_type: #{issue['fields']['issuetype']['name']}" unless issue['fields']["issuetype"].nil?
71
+ md << "assignee: #{issue['fields']['assignee']['displayName']}" unless issue['fields']["assignee"].nil?
72
+ md << ""
73
+ md << "## Summary:"
74
+ md << "#{issue['fields']['summary']}"
75
+ # push to elastic
76
+
77
+ md << "## Comments"
78
+ md << ""
79
+ issue["comments"].each_with_index do |comment, i|
80
+ md << "### Comment by #{comment["author"]["displayName"]} "
81
+ md << ""
82
+ md << "created: #{comment["created"]}"
83
+ md << "#{comment["body"].gsub('{noformat}', "\n```\n")}"
84
+ md << ""
85
+ end
86
+ puts md
87
+ end
88
+ end
89
+
90
+ #
91
+ # Options
92
+ #
93
+
94
+ options = {}
95
+ global_banner = <<DOC
96
+
97
+ Jira fetch datafile to markdown
98
+
99
+ Usage: cl jira to-markdown [options]
100
+
101
+ DOC
102
+
103
+ global = OptionParser.new do |g|
104
+ g.banner = global_banner
105
+ add_help_and_verbose(g)
106
+
107
+ g.on("-f", "--data-filepath FILEPATH", "relative path jira datafile") do |v|
108
+ options[:data_filepath] = v
109
+ end
110
+ end
111
+
112
+ #
113
+ # Run
114
+ #
115
+
116
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
117
+ global.parse(ARGV)
118
+
119
+ ask_and_store_option(options, :data_filepath, "data_filepath: ")
120
+
121
+ # display full command
122
+ write_history("""cl jira to-markdown \\
123
+ --data-filepath=#{options[:data_filepath]}
124
+ """)
125
+
126
+ do_work(options)
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # Jira fetch datafile to markdown
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/parse_and_pick.rb'
10
+ require 'cl/magic/common/common_options.rb'
11
+ require 'cl/magic/common/logging.rb'
12
+ require 'cl/magic/common/jira.rb'
13
+
14
+ require 'json'
15
+
16
+ @logger = get_logger()
17
+
18
+ #
19
+ # Features
20
+ #
21
+
22
+ def do_work(options)
23
+ filepath = File.join(@working_dir, options[:data_filepath])
24
+ issues = JSON.parse(File.read(filepath))
25
+ issues.each do |i|
26
+ issue_md, comments = Jira.jira_to_markdown(i)
27
+ puts issue_md
28
+ puts comments.map{ |o| o[1] }.join("\n")
29
+ end
30
+ end
31
+
32
+ #
33
+ # Options
34
+ #
35
+
36
+ options = {}
37
+ global_banner = <<DOC
38
+
39
+ Jira fetch datafile to markdown
40
+
41
+ Usage: cl jira to-markdown [options]
42
+
43
+ DOC
44
+
45
+ global = OptionParser.new do |g|
46
+ g.banner = global_banner
47
+ add_help_and_verbose(g)
48
+
49
+ g.on("-f", "--data-filepath FILEPATH", "relative path jira datafile") do |v|
50
+ options[:data_filepath] = v
51
+ end
52
+ end
53
+
54
+ #
55
+ # Run
56
+ #
57
+
58
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
59
+ global.parse(ARGV)
60
+
61
+ ask_and_store_option(options, :data_filepath, "data_filepath: ")
62
+
63
+ # display full command
64
+ write_history("""cl jira to-markdown \\
65
+ --data-filepath=#{options[:data_filepath]}
66
+ """)
67
+
68
+ do_work(options)
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env ruby
2
+ # Jira fetch datafile to 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
+
18
+ #
19
+ # Features
20
+ #
21
+
22
+ def get_issues_from_datafile(options)
23
+ final_issues = []
24
+ filepath = File.join(@working_dir, options[:data_filepath])
25
+ issues = JSON.parse(File.read(filepath))
26
+
27
+ @logger.info "stats for: #{options[:data_filepath]}"
28
+ issues.each do |issue|
29
+ issuetype = issue["fields"]["issuetype"]["name"]
30
+ labels = issue["fields"]["labels"]
31
+
32
+ has_excluded_labels = (labels & options[:exclude_labels]).any?
33
+ is_excluded_issuetype = options[:exclude_issuetypes].include?(issuetype.downcase)
34
+ final_issues << issue unless has_excluded_labels or is_excluded_issuetype
35
+ end
36
+ return final_issues
37
+ end
38
+
39
+ def in_range_issue_stats(issues, start_date, end_date, options)
40
+
41
+ def in_range_logs(start_date, end_date, issue)
42
+ return issue["status_changelogs"].select do |log|
43
+ (start_date..end_date).cover?(Date.parse(log["created"]))
44
+ end
45
+ end
46
+
47
+ # in-range
48
+ in_range_issues = issues.select do |issue|
49
+
50
+ # issue created date?
51
+ issue_in_range = Date.parse(issue["fields"]["created"]) < end_date
52
+
53
+ # logs created date?
54
+ logs_in_range = in_range_logs(start_date, end_date, issue).any?
55
+
56
+ # select if
57
+ (logs_in_range or issue_in_range)
58
+ end
59
+
60
+ # stat hashes
61
+ return in_range_issues.collect do |issue|
62
+
63
+ # most recent in-range log?
64
+ changelog = in_range_logs(start_date, end_date, issue)
65
+ .sort_by { |l| Date.parse(l["created"]) }.last
66
+
67
+ # yield stat hash
68
+ status = changelog ? changelog["toString"] : issue["fields"]["status"]["name"]
69
+ {
70
+ key: issue["key"],
71
+ issuetype: issue["fields"]["issuetype"]["name"],
72
+ status: status
73
+ }
74
+ end.compact
75
+ end
76
+
77
+ def print_stats(stat_hashes, start_date, end_date)
78
+ counts = {
79
+ start_date: start_date.strftime("%m-%d-%Y"),
80
+ end_date: end_date.strftime("%m-%d-%Y"),
81
+ total: 0,
82
+ total_todo: 0,
83
+ total_done: 0,
84
+ by_type: {}
85
+ }
86
+
87
+ stat_hashes.each do |stat|
88
+ issuetype = stat[:issuetype]
89
+ status = stat[:status]
90
+
91
+ # count by status
92
+ case status
93
+ when "To Do","Ready","Rework","In Progress","In QA","Ready For Code Review","In Code Review"
94
+ increment_type(counts, issuetype, :to_do)
95
+ increment(counts, :total_todo)
96
+ when "Ready to Deploy","Closed"
97
+ increment_type(counts, issuetype, :done)
98
+ increment(counts, :total_done)
99
+ else
100
+ # if status != "Won't Do"
101
+ # debugger
102
+ # end
103
+ end
104
+
105
+ # count totals
106
+ unless status=="Won't Do"
107
+ increment(counts, :total)
108
+ increment_type(counts, issuetype, :total)
109
+ end
110
+
111
+ end
112
+ return counts
113
+ end
114
+
115
+ def oldest_issue_date(issues)
116
+ return issues.collect {|i| Date.parse(i["fields"]["created"])}.sort.first.beginning_of_day
117
+ end
118
+
119
+ def increment_type(hash, type, status_category)
120
+ type = type.downcase.underscore
121
+ hash[:by_type][type] = {} unless hash[:by_type].key?(type)
122
+ hash[:by_type][type][status_category] ||= 0
123
+ hash[:by_type][type][status_category] += 1
124
+ end
125
+
126
+ def increment(hash, key)
127
+ hash[key] ||= 0
128
+ hash[key] += 1
129
+ end
130
+
131
+ def iter_date_range(start_date)
132
+ while start_date < Date.today.end_of_week
133
+ yield start_date.beginning_of_week.beginning_of_day, start_date.end_of_week.end_of_day
134
+ start_date += 1.weeks # increment
135
+ end
136
+ end
137
+
138
+ def do_work(options)
139
+ issues = get_issues_from_datafile(options)
140
+ oldest_date = oldest_issue_date(issues).beginning_of_week
141
+ @logger.info "starting at #{oldest_date}"
142
+
143
+ iter_date_range(oldest_date) do |start_date, end_date|
144
+ stat_hashes = in_range_issue_stats(issues, start_date, end_date, options)
145
+ counts = print_stats(stat_hashes, start_date, end_date)
146
+ puts counts # print each time range
147
+ end
148
+ end
149
+
150
+ #
151
+ # Options
152
+ #
153
+
154
+ options = {
155
+ exclude_issuetypes: []
156
+ }
157
+ global_banner = <<DOC
158
+
159
+ Jira fetch datafile to stats
160
+
161
+ Usage: cl jira to-stats [options]
162
+
163
+ DOC
164
+
165
+ global = OptionParser.new do |g|
166
+ g.banner = global_banner
167
+ add_help_and_verbose(g)
168
+
169
+ g.on("-f", "--data-filepath FILEPATH", "relative path to file produced by 'cl jira fetch' command") do |v|
170
+ options[:data_filepath] = v
171
+ end
172
+
173
+ g.on("-e", "--exclude-issuetypes CSV", "comma separated list of issuetypes you want to exclude") do |v|
174
+ options[:exclude_issuetypes] = v.split(',')
175
+ options[:exclude_issuetypes] << "Won't Do"
176
+ end
177
+
178
+ g.on("-l", "--exclude-labels CSV", "comma separated list of labels that will cause a ticket to be excluded") do |v|
179
+ options[:exclude_labels] = v.split(',')
180
+ end
181
+ end
182
+
183
+ #
184
+ # Run
185
+ #
186
+
187
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
188
+ global.parse(ARGV)
189
+
190
+ # prompt for missing options
191
+ ask_and_store_option(options, :data_filepath, "data_filepath: ")
192
+ options[:exclude_issuetypes] = [] if options[:exclude_issuetypes].nil?
193
+ options[:exclude_labels] = [] if options[:exclude_labels].nil?
194
+
195
+ # display full command
196
+ write_history("""cl jira to-stats \\
197
+ --data-filepath=#{options[:data_filepath]} \\
198
+ --exclude-issuetypes=#{options[:exclude_issuetypes].join(',')} \\
199
+ --exclude-labels=#{options[:exclude_labels].join(',')}
200
+ """)
201
+
202
+ do_work(options)
@@ -17,8 +17,10 @@ require 'cl/magic/common/kubectl.rb'
17
17
  # Features
18
18
  #
19
19
 
20
+
20
21
  def do_work(options, pods, containers)
21
- cmd = "kubectl stern '#{pods.collect(&:first).join('|')}' --context #{options[:kube_context]} --namespace #{options[:namespace]} --container '#{containers.collect(&:first).join('|')}' --since #{options[:since]} --container-state 'running,waiting,terminated'"
22
+ container_name_regex = "^(#{containers.collect(&:first).join('|')})$"
23
+ cmd = "kubectl stern '#{pods.collect(&:first).join('|')}' --context #{options[:kube_context]} --namespace #{options[:namespace]} --container '#{container_name_regex}' --since #{options[:since]} --container-state 'running,waiting,terminated'"
22
24
  cmd += " | grep #{options[:grep]}" if options[:grep]
23
25
 
24
26
  @logger.puts
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)