dri 0.1.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.gitlab-ci.yml +32 -13
- data/.rubocop.yml +53 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/Gemfile +2 -3
- data/Gemfile.lock +69 -17
- data/README.md +33 -2
- data/Rakefile +3 -1
- data/bin/console +1 -0
- data/dri.gemspec +25 -22
- data/exe/dri +3 -3
- data/lib/dri/api_client.rb +51 -10
- data/lib/dri/cli.rb +14 -2
- data/lib/dri/command.rb +8 -6
- data/lib/dri/commands/fetch/failures.rb +38 -29
- data/lib/dri/commands/fetch/featureflags.rb +95 -0
- data/lib/dri/commands/fetch/quarantines.rb +53 -0
- data/lib/dri/commands/fetch/testcases.rb +16 -10
- data/lib/dri/commands/fetch/triaged.rb +11 -15
- data/lib/dri/commands/fetch.rb +38 -2
- data/lib/dri/commands/incidents.rb +59 -0
- data/lib/dri/commands/init.rb +7 -5
- data/lib/dri/commands/profile.rb +14 -5
- data/lib/dri/commands/publish/report.rb +36 -10
- data/lib/dri/commands/publish.rb +4 -5
- data/lib/dri/commands/rm/emoji.rb +5 -5
- data/lib/dri/commands/rm/profile.rb +2 -2
- data/lib/dri/commands/rm.rb +0 -1
- data/lib/dri/refinements/truncate.rb +15 -0
- data/lib/dri/report.rb +72 -43
- data/lib/dri/templates/incidents/.gitkeep +1 -0
- data/lib/dri/utils/markdown_lists.rb +7 -9
- data/lib/dri/utils/table.rb +20 -0
- data/lib/dri/version.rb +3 -1
- data/lib/dri.rb +2 -0
- metadata +69 -46
@@ -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 = [
|
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(
|
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
|
-
|
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
|
-
|
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
|
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
|
data/lib/dri/commands/publish.rb
CHANGED
@@ -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
|
-
|
14
|
-
method_option :format, aliases: '-f', type: :string, :
|
15
|
-
|
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
|
-
|
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[
|
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
|
-
|
41
|
-
url = "#{award_emoji_url}/#{emoji_found[
|
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 ✅"
|
data/lib/dri/commands/rm.rb
CHANGED
@@ -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?
|
35
|
-
|
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
|
-
|
44
|
-
|
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,
|
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
|
-
|
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
|
77
|
+
next unless label_pipeline_map.has_key?(pipeline)
|
73
78
|
|
74
79
|
pipeline_in_notes_found = false
|
75
80
|
pipeline_link = ''
|
76
|
-
|
77
|
-
pipeline_markdown = ''
|
81
|
+
pipeline_markdown = pipeline.gsub(/.gitlab.com/, '')
|
78
82
|
|
79
83
|
failure_notes.each do |note|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
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
|
-
|
95
|
+
unless pipeline_link.empty?
|
93
96
|
pipeline_link_sanitized = pipeline_link.join.strip.chop
|
94
|
-
pipeline_markdown = "[#{
|
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
|
-
|
125
|
-
|
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
|
-
|
129
|
-
|
145
|
+
|
146
|
+
"<ul>#{mrs.join}</ul>"
|
130
147
|
end
|
131
148
|
|
132
149
|
def assigned?(assignees)
|
133
|
-
assignees.empty? ?
|
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
|
-
|
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 in `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(' ', ' ') 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
|
-
|
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
|
-
|
9
|
+
arr << "<li><b>#{label}</b>:<br> #{element}</li>"
|
12
10
|
end
|
13
|
-
|
11
|
+
arr << "<hr>"
|
14
12
|
end
|
15
|
-
|
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