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.
- 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