dri 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../../command'
2
4
  require_relative '../../utils/markdown_lists'
3
5
  require_relative "../../report"
@@ -17,14 +19,14 @@ module Dri
17
19
  @time = Time.now.to_i
18
20
  end
19
21
 
20
- def execute(input: $stdin, output: $stdout)
22
+ def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
21
23
  verify_config_exists
22
24
  report = Dri::Report.new(config)
23
25
 
24
26
  logger.info "Fetching triaged failures with award emoji #{emoji}..."
25
27
 
26
28
  spinner.start
27
- issues = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
29
+ issues = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
28
30
  spinner.stop
29
31
 
30
32
  if issues.empty?
@@ -33,25 +35,46 @@ module Dri
33
35
  end
34
36
 
35
37
  logger.info "Assembling the report... "
36
- # sets each failure on the table
37
- action_options = ["pinged SET", "reproduced", "transient", "quarantined"]
38
+ # sets each failure on the table
39
+ action_options = [
40
+ "pinged SET",
41
+ "reproduced",
42
+ "transient",
43
+ "quarantined",
44
+ "active investigation",
45
+ "blocking pipelines",
46
+ "awaiting for a fix to merge",
47
+ "notified the team",
48
+ "due to feature flag"
49
+ ]
38
50
 
39
51
  spinner.start
40
52
  issues.each do |issue|
41
53
  actions = []
42
54
 
43
55
  if @options[:actions]
44
- actions = prompt.multi_select("Please mark the actions on #{add_color(issue['title'], :yellow)}: ", action_options)
56
+ actions = prompt.multi_select(
57
+ "Please mark the actions on #{add_color(issue['title'], :yellow)}: ",
58
+ action_options,
59
+ per_page: 9
60
+ )
45
61
  end
62
+
46
63
  report.add_failure(issue, actions)
47
64
  end
48
65
 
49
66
  if @options[:format] == 'list'
50
67
  # generates markdown list with failures
51
68
  format_style = Utils::MarkdownLists.make_list(report.labels, report.failures) unless report.failures.empty?
52
- else
69
+ else
53
70
  # generates markdown table with rows as failures
54
- format_style = MarkdownTables.make_table(report.labels, report.failures, is_rows: true, align: %w[c l c l l]) unless report.failures.empty?
71
+ unless report.failures.empty?
72
+ format_style = MarkdownTables.make_table(
73
+ report.labels,
74
+ report.failures,
75
+ is_rows: true, align: %w[l l l l l]
76
+ )
77
+ end
55
78
  end
56
79
 
57
80
  report.set_header(timezone, username)
@@ -71,7 +94,7 @@ module Dri
71
94
  File.open(report_path, 'a') do |out_file|
72
95
  out_file.puts note
73
96
  end
74
-
97
+
75
98
  spinner.stop
76
99
 
77
100
  output.puts "Done! ✅\n"
@@ -82,10 +105,13 @@ module Dri
82
105
  # sends note to the weekly triage report
83
106
  issues = api_client.fetch_current_triage_issue
84
107
  current_issue_iid = issues[0]["iid"]
85
- response = api_client.post_triage_report_note(iid: current_issue_iid, body: note)
108
+
109
+ api_client.post_triage_report_note(iid: current_issue_iid, body: note)
86
110
 
87
111
  output.puts "Done! ✅\n"
88
- logger.success "Thanks @#{username}, your report was posted at https://gitlab.com/gitlab-org/quality/pipeline-triage/-/issues/#{current_issue_iid} 🎉"
112
+ logger.success(<<~MSG)
113
+ Thanks @#{username}, your report was posted at https://gitlab.com/gitlab-org/quality/pipeline-triage/-/issues/#{current_issue_iid} 🎉
114
+ MSG
89
115
  end
90
116
  end
91
117
  end
@@ -5,16 +5,15 @@ require 'thor'
5
5
  module Dri
6
6
  module Commands
7
7
  class Publish < Thor
8
-
9
8
  namespace :publish
10
9
 
11
10
  desc 'report', 'Generate a report'
12
11
  method_option :dry_run, type: :boolean,
13
- desc: 'Generates a report locally'
14
- method_option :format, aliases: '-f', type: :string, :default => "table",
15
- desc: 'Formats the report'
12
+ desc: 'Generates a report locally'
13
+ method_option :format, aliases: '-f', type: :string, default: "table",
14
+ desc: 'Formats the report'
16
15
  method_option :actions, type: :boolean,
17
- desc: 'Updates actions on failures'
16
+ desc: 'Updates actions on failures'
18
17
  def report(*)
19
18
  if options[:help]
20
19
  invoke :help, ['report']
@@ -10,9 +10,9 @@ module Dri
10
10
  @options = options
11
11
  end
12
12
 
13
- def execute(input: $stdin, output: $stdout)
13
+ def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
14
14
  verify_config_exists
15
-
15
+
16
16
  remove = prompt.yes? "Are you sure you want to remove all #{emoji} award emojis from issues?"
17
17
 
18
18
  unless remove
@@ -29,7 +29,7 @@ module Dri
29
29
  spinner.stop
30
30
 
31
31
  issues_with_award_emoji.each do |issue|
32
- logger.info "Removing #{emoji} from #{issue["web_url"]}..."
32
+ logger.info "Removing #{emoji} from #{issue['web_url']}..."
33
33
 
34
34
  award_emoji_url = issue["_links"]["award_emoji"]
35
35
 
@@ -37,8 +37,8 @@ module Dri
37
37
 
38
38
  emoji_found = response.find { |e| e['name'] == emoji && e['user']['username'] == username }
39
39
 
40
- if !emoji_found.nil?
41
- url = "#{award_emoji_url}/#{emoji_found["id"]}"
40
+ unless emoji_found.nil?
41
+ url = "#{award_emoji_url}/#{emoji_found['id']}"
42
42
  api_client.delete_award_emoji(url)
43
43
  end
44
44
  end
@@ -13,7 +13,7 @@ module Dri
13
13
 
14
14
  def execute(input: $stdin, output: $stdout)
15
15
  verify_config_exists
16
-
16
+
17
17
  remove = prompt.yes? "Are you sure you want to remove existing profile?"
18
18
 
19
19
  unless remove
@@ -22,7 +22,7 @@ module Dri
22
22
  end
23
23
 
24
24
  logger.info "Removing profile..."
25
-
25
+
26
26
  FileUtils.rm("#{Dir.pwd}/.dri_profile.yml")
27
27
 
28
28
  logger.success "Done ✅"
@@ -5,7 +5,6 @@ require 'thor'
5
5
  module Dri
6
6
  module Commands
7
7
  class Rm < Thor
8
-
9
8
  namespace :rm
10
9
 
11
10
  desc 'profile', 'Command description...'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Refinements
4
+ refine String do
5
+ # Truncate string to limit of _n_ characters
6
+ # @param [Integer] limit the limit of characters
7
+ # @return [String] the resulting string after truncation
8
+ def truncate(limit = 50)
9
+ return freeze if size <= limit
10
+
11
+ # limit - 3 for the ellipses
12
+ self[0...(limit - 3)] << '...'
13
+ end
14
+ end
15
+ end
data/lib/dri/report.rb CHANGED
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dri
2
- class Report
4
+ class Report # rubocop:disable Metrics/ClassLength
3
5
  attr_reader :header, :failures, :labels
4
6
 
5
7
  def initialize(config)
6
8
  @labels = ['Title', 'Issue', 'Pipelines', 'Stack Trace', 'Actions']
7
9
  @failures = []
8
10
  @date = Date.today
9
- @today = Date.today.strftime("%Y-%m-%d")
11
+ @today = Date.today.strftime("%Y-%m-%d")
10
12
  @weekday = Date.today.strftime("%A")
11
13
  @header = nil
12
14
 
@@ -19,20 +21,24 @@ module Dri
19
21
 
20
22
  def add_failure(failure, actions_opts = [])
21
23
  iid = failure["iid"]
22
- title = failure["title"]
24
+ title = format_title(failure["title"])
23
25
  link = failure["web_url"]
24
26
  labels = failure["labels"]
25
27
  created_at = failure["created_at"]
26
28
  assignees = failure["assignees"]
27
- award_emoji_url = failure["_links"]["award_emoji"]
28
29
  description = failure["description"]
29
30
 
30
31
  related_mrs = @api_client.fetch_related_mrs(issue_iid: iid)
31
32
  emoji = classify_failure_emoji(created_at)
32
33
  emojified_link = "#{emoji} #{link}"
33
-
34
- stack_blob = description.empty? ? "No stack trace found" : description.split("### Stack trace").last.gsub(/\n|`|!|\[|\]/, '').squeeze(" ")[0...250]
35
- stack_trace = ":link:[`#{stack_blob}...`](#{link + '#stack-trace'})"
34
+
35
+ stack_blob = if description.empty?
36
+ "No stack trace found"
37
+ else
38
+ description.split("### Stack trace").last.gsub(/\n|`|!|\[|\]/, '').squeeze(" ")[0...250]
39
+ end
40
+
41
+ stack_trace = ":link:[`#{stack_blob}...`](#{link}#stack-trace)"
36
42
 
37
43
  failure_type = filter_failure_type_labels(labels)
38
44
  assigned_status = assigned?(assignees)
@@ -40,21 +46,20 @@ module Dri
40
46
 
41
47
  linked_pipelines = link_pipelines(iid, pipelines, description)
42
48
 
43
- actions = ""
44
- actions.concat actions_status_template(failure_type, assigned_status, actions_opts)
45
- actions.concat actions_fixes_template(related_mrs)
49
+ actions_status = actions_status_template(failure_type, assigned_status, actions_opts)
50
+ actions_fixes = actions_fixes_template(related_mrs)
46
51
 
47
- @failures << [title, emojified_link, linked_pipelines, stack_trace, actions]
52
+ @failures << [title, emojified_link, linked_pipelines, stack_trace, "#{actions_status}#{actions_fixes}"]
48
53
  end
49
54
 
50
55
  private
51
56
 
52
- def link_pipelines(iid, pipelines, description)
57
+ def link_pipelines(iid, pipelines, description) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
53
58
  linked = []
54
59
  label_pipeline_map = {
55
60
  'gitlab.com' => '/quality/production',
56
61
  'canary.gitlab.com' => '/quality/canary',
57
- # 'canary.staging.gitlab.com' => '',
62
+ 'canary.staging.gitlab.com' => '/quality/staging-canary',
58
63
  'main' => '/gitlab-org/gitlab-qa-mirror',
59
64
  'master' => '/gitlab-org/gitlab-qa-mirror',
60
65
  'nightly' => '/quality/nightly',
@@ -66,79 +71,91 @@ module Dri
66
71
 
67
72
  failure_notes = @api_client.fetch_failure_notes(issue_iid: iid)
68
73
 
69
- return if pipelines.empty?
70
-
74
+ return if pipelines.empty?
75
+
71
76
  pipelines.each do |pipeline|
72
- next if !label_pipeline_map.has_key? pipeline
77
+ next unless label_pipeline_map.has_key?(pipeline)
73
78
 
74
79
  pipeline_in_notes_found = false
75
80
  pipeline_link = ''
76
- pipeline_link_sanitized = ''
77
- pipeline_markdown = ''
81
+ pipeline_markdown = pipeline.gsub(/.gitlab.com/, '')
78
82
 
79
83
  failure_notes.each do |note|
80
- if note["body"].include? label_pipeline_map.fetch(pipeline)
81
- pipeline_in_notes_found = true
82
- pipeline_link = URI.extract(note["body"], %w(https))
83
- break
84
- end
84
+ next unless note["body"].include?(label_pipeline_map.fetch(pipeline))
85
+
86
+ pipeline_in_notes_found = true
87
+ pipeline_link = URI.extract(note["body"], %w[https])
85
88
  end
86
-
89
+
87
90
  unless pipeline_in_notes_found
88
- links_description = URI.extract(description, %w(https))
89
- pipeline_link = links_description.select { |link| link.include? label_pipeline_map.fetch(pipeline) }
91
+ links_description = URI.extract(description, %w[https])
92
+ pipeline_link = links_description.select { |link| link.include? label_pipeline_map.fetch(pipeline) }
90
93
  end
91
94
 
92
- if !pipeline_link.empty?
95
+ unless pipeline_link.empty?
93
96
  pipeline_link_sanitized = pipeline_link.join.strip.chop
94
- pipeline_markdown = "[#{pipeline.gsub(/.gitlab.com/, '')}](#{pipeline_link_sanitized})"
95
- linked << pipeline_markdown
97
+ pipeline_markdown = "[#{pipeline_markdown}](#{pipeline_link_sanitized})"
96
98
  end
99
+
100
+ linked << pipeline_markdown
97
101
  end
98
102
  linked.join(', ')
99
103
  end
100
104
 
101
- def actions_status_template(failure_type, assigned_status, actions_opts)
102
- notified_set = ''
105
+ def actions_status_template(failure_type, assigned_status, actions_opts) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity/MethodLength
103
106
  quarantined = ''
104
107
  reproduced = ''
105
108
  transient = ''
109
+ active_investigation = ''
110
+ blocking_pipelines = ''
111
+ wait_for_fix = ''
112
+ notified_team = ''
113
+ feature_flag = ''
106
114
 
107
115
  notified_set = '<li>[x] notified SET</li>' if actions_opts.include? 'pinged SET'
108
116
  quarantined = '<li>[x] quarantined</li>' if actions_opts.include? 'quarantined'
109
117
  reproduced = '<li>[x] reproduced</li>' if actions_opts.include? 'reproduced'
110
118
  transient = '<li>[x] transient</li>' if actions_opts.include? 'transient'
119
+ active_investigation = '<li>[x] active investigation</li>' if actions_opts.include? 'active investigation'
120
+ blocking_pipelines = '<li>[x] is blocking pipelines</li>' if actions_opts.include? 'blocking pipelines'
121
+ wait_for_fix = '<li>[x] waiting for fix to merge</li>' if actions_opts.include? 'awaiting for a fix to merge'
122
+ notified_team = '<li>[x] notified the team</li>' if actions_opts.include? 'notified the team'
123
+ feature_flag = '<li>[x] due to feature flag</li>' if actions_opts.include? 'due to feature flag'
111
124
 
112
- action_status = "<i>Status:</i><ul>"
125
+ action_status = ["<i>Status:</i><ul>"]
113
126
  action_status << "<li>#{failure_type}</li>"
114
127
  action_status << "</li><li>#{assigned_status}</li>"
115
128
  action_status << notified_set
116
129
  action_status << quarantined
117
130
  action_status << reproduced
118
131
  action_status << transient
132
+ action_status << active_investigation
133
+ action_status << blocking_pipelines
134
+ action_status << wait_for_fix
135
+ action_status << notified_team
136
+ action_status << feature_flag
119
137
 
120
- action_status
138
+ action_status.join
121
139
  end
122
140
 
123
141
  def actions_fixes_template(related_mrs)
124
- actions_fixes_template = '<ul><i>Potential fixes:</i><br>'
125
- related_mrs.each do |mr|
126
- actions_fixes_template.concat "<li>[#{mr["title"]}](#{mr["web_url"]})</li>"
142
+ mrs = related_mrs.each_with_object(['<i>Potential fixes:</i><br>']) do |mr, fixes|
143
+ fixes << "<li>[#{mr['title'].truncate(40)}](#{mr['web_url']})</li>"
127
144
  end
128
- actions_fixes_template.concat '</ul>'
129
- actions_fixes_template
145
+
146
+ "<ul>#{mrs.join}</ul>"
130
147
  end
131
148
 
132
149
  def assigned?(assignees)
133
- assignees.empty? ? 'Assigned :x:' : 'Assigned :white_check_mark:'
150
+ assignees.empty? ? 'Assigned :x:' : 'Assigned :white_check_mark:'
134
151
  end
135
152
 
136
153
  def filter_pipeline_labels(labels)
137
154
  pipelines = []
138
155
 
139
156
  labels.each do |label|
140
- matchers = { 'found:' => ' '}
141
-
157
+ matchers = { 'found:' => ' ' }
158
+
142
159
  if label.include? "found:"
143
160
  pipeline = label.gsub(/found:/) { |match| matchers[match] }
144
161
  pipelines << pipeline.strip
@@ -161,8 +178,20 @@ module Dri
161
178
  if created_at.include? @today
162
179
  new_failure_emoji
163
180
  else
164
- known_failure_emoji
181
+ known_failure_emoji
165
182
  end
166
183
  end
184
+
185
+ # Turns something like:
186
+ # Failure in browser_ui/3_create/merge_request/create_merge_request_spec.rb | Create a merge request
187
+ # into
188
+ # Failure&nbsp;in&nbsp;`browser_ui/3_create/merge_request/create_merge_request_spec.rb` | Create a merge request
189
+ def format_title(title)
190
+ path, desc = title.split('|')
191
+ path = "Failure in `#{path.strip}`" if path.delete_prefix!('Failure in ')
192
+ path = path.strip.gsub(' ', '&nbsp;') if desc
193
+
194
+ "#{path} | #{desc}"
195
+ end
167
196
  end
168
- end
197
+ end
@@ -0,0 +1 @@
1
+ #
@@ -1,20 +1,18 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module Dri
3
4
  module Utils
4
5
  class MarkdownLists
5
6
  def self.make_list(labels, items)
6
- @list = ""
7
-
8
- @list.concat "<ul>"
9
- items.each do |item|
7
+ list = items.each_with_object([]) do |item, arr|
10
8
  item.zip(labels).each do |element, label|
11
- @list.concat "<li><b>#{label}</b>:<br> #{element}</li>"
9
+ arr << "<li><b>#{label}</b>:<br> #{element}</li>"
12
10
  end
13
- @list.concat "<hr>"
11
+ arr << "<hr>"
14
12
  end
15
- @list.concat "</ul>"
13
+
14
+ "<ul>#{list.join}</ul>"
16
15
  end
17
- @list
18
16
  end
19
17
  end
20
- end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-table'
4
+
5
+ module Dri
6
+ module Utils
7
+ module Table
8
+ def print_table(headers, rows, alignments: [])
9
+ if alignments.empty?
10
+ (1..headers.size).each do
11
+ alignments.push(:center)
12
+ end
13
+ end
14
+
15
+ table = TTY::Table.new(headers, rows)
16
+ puts table.render(:ascii, resize: true, multiline: true, alignments: alignments)
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/dri/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dri
2
- VERSION = "0.1.2"
4
+ VERSION = "0.3.0"
3
5
  end
data/lib/dri.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dri/version"
2
4
 
3
5
  module Dri