dri 0.1.2 → 0.3.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.
@@ -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