dri 0.10.2 → 0.11.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,38 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "influxdb-client"
4
+
3
5
  require_relative '../../command'
4
6
  require_relative '../../utils/table'
5
7
  require_relative '../../utils/constants'
8
+ require_relative '../../support/influxdb_tools'
6
9
 
7
10
  module Dri
8
11
  module Commands
9
12
  class Fetch
10
- class Failures < Dri::Command
13
+ class Failures < Dri::Command # rubocop:disable Metrics/ClassLength
11
14
  include Dri::Utils::Table
12
15
  include Dri::Utils::Constants::Triage::Labels
16
+ include Dri::Support::InfluxdbTools
13
17
  using Refinements
14
18
 
15
19
  def initialize(options)
16
20
  @options = options
17
- @start_date = @options[:start_date] ? Date.parse(@options[:start_date]) : Date.today
21
+ @start_date = @options[:start_date] ? Date.parse(@options[:start_date]) : Date.today - 1
18
22
  @end_date = @options[:end_date] ? Date.parse(@options[:end_date]) : Date.today
19
23
  @cutoff_time = @options[:cutoff] ? Time.parse(options[:cutoff]).utc : nil
20
24
  end
21
25
 
22
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
26
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
23
27
  verify_config_exists
24
28
 
25
- urgent_environments = %w[canary canary.staging]
26
-
27
29
  title = add_color('Title', :bright_yellow)
28
30
  triaged = add_color('Triaged?', :bright_yellow)
29
31
  environment = add_color('Environment', :bright_yellow)
30
- author = add_color('Author', :bright_yellow)
31
32
  url = add_color('URL', :bright_yellow)
32
33
  updated_at = add_color('Updated at', :bright_yellow)
33
34
 
34
35
  sorted_failures = []
35
- labels = { title: title, triaged: triaged, environment: environment, author: author, url: url,
36
+ labels = { title: title, triaged: triaged, environment: environment, url: url,
36
37
  updated_at: updated_at }
37
38
  triaged_counter = 0
38
39
 
@@ -57,10 +58,30 @@ module Dri
57
58
  exit 0
58
59
  end
59
60
 
61
+ blocker_set = Set.new
62
+
63
+ blocker_set = Set.new
64
+
65
+ if query_api
66
+ begin
67
+ blockers = query_api.query(query: query_reliables_smokes)
68
+
69
+ blockers.each do |table|
70
+ table.records.each do |record|
71
+ blocker_set.add(record.values['name'])
72
+ end
73
+ end
74
+ rescue StandardError => e
75
+ logger.error "An error occurred querying for reliable and smoke failures: #{e.message}"
76
+ end
77
+ end
78
+
60
79
  failures.each do |failure|
61
80
  project_id = failure.project_id
62
81
  title = failure.title.truncate(60)
63
- author = failure.to_h.dig('author', 'username').truncate(25)
82
+
83
+ title = bold("*#{failure.title.truncate(60)}*") if blocker?(failure.title, blocker_set)
84
+
64
85
  url = failure.web_url
65
86
  updated_at = failure.updated_at
66
87
  triaged = add_color('x', :red)
@@ -69,9 +90,6 @@ module Dri
69
90
 
70
91
  env == 'gitlab.com' ? 'production' : env
71
92
  end
72
- urgent = urgent_environments.all? { |env| envs.include?(env) }
73
-
74
- next if @options[:urgent] && !urgent
75
93
 
76
94
  emoji_awards = api_client.fetch_awarded_emojis(failure.iid, project_id: project_id).find do |e|
77
95
  e.name == emoji && e.to_h.dig('user', 'username') == username
@@ -82,33 +100,74 @@ module Dri
82
100
  triaged_counter += 1
83
101
  end
84
102
 
85
- sorted_failures << { title: title, triaged: triaged, environment: envs.first || 'none', author: author,
103
+ sorted_failures << { title: title, triaged: triaged, environment: envs.first || 'none',
86
104
  url: url, updated_at: updated_at }
87
105
  end
88
106
 
89
107
  sorted_failures.sort_by! { |failure| failure[@options[:sort_by]&.to_sym || :environment] }
90
108
  end
91
109
 
92
- msg = if @options[:urgent]
93
- <<~MSG
94
- Found: #{sorted_failures.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
95
- MSG
96
- else
97
- <<~MSG
98
- Found: #{sorted_failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
99
- MSG
100
- end
110
+ msg = <<~MSG
111
+ Found: #{sorted_failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
112
+ Smoke and Reliable failures are marked with *Failure..* in bold.
113
+ MSG
101
114
 
102
115
  terminal_width = TTY::Screen.width
103
116
 
104
- if terminal_width <= 210 # adjust the desired minimum width
117
+ if terminal_width <= 210
105
118
  labels.delete(:updated_at)
106
119
  sorted_failures.map { |failure| failure.delete(:updated_at) }
107
120
  end
108
121
 
109
- print_table(labels.values, sorted_failures.map(&:values), alignments: [:left, :center, :center, :left, :left])
122
+ print_table(
123
+ labels.values,
124
+ sorted_failures.map(&:values),
125
+ alignments: [:left, :center, :center, :left]
126
+ )
110
127
  output.puts(msg)
111
128
  end
129
+
130
+ private
131
+
132
+ def blocker?(title, set)
133
+ title_part = title.split('|').last
134
+ return true if title_part&.strip && set.include?(title_part.strip)
135
+
136
+ false
137
+ end
138
+
139
+ def query_reliables_smokes
140
+ <<~QUERY
141
+ from(bucket: "#{Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET}")
142
+ |> range(start: -1d)
143
+ |> filter(fn: (r) => r._measurement == "test-stats")
144
+ |> filter(fn: (r) => r.run_type == "staging-full" or
145
+ r.run_type == "staging-sanity" or
146
+ r.run_type == "production-full" or
147
+ r.run_type == "production-sanity" or
148
+ r.run_type == "package-and-qa" or
149
+ r.run_type == "nightly" or
150
+ r.run_type == "e2e-test-on-gdk"
151
+ )
152
+ |> filter(fn: (r) => r.job_name != "airgapped" and
153
+ r.job_name != "instance-image-slow-network" and
154
+ r.job_name != "nplus1-instance-image"
155
+ )
156
+ |> filter(fn: (r) => r.status != "pending" and
157
+ r.merge_request == "false" and
158
+ r.quarantined == "false" and
159
+ r.smoke == "true" or
160
+ r.reliable == "true"
161
+ )
162
+ |> filter(fn: (r) => r["_field"] == "job_url" or
163
+ r["_field"] == "failure_exception" or
164
+ r["_field"] == "id"
165
+ )
166
+ |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
167
+ |> group(columns: ["name"])
168
+ |> distinct(column: "name")
169
+ QUERY
170
+ end
112
171
  end
113
172
  end
114
173
  end
@@ -17,7 +17,7 @@ module Dri
17
17
  @today_iso_format = Time.now.strftime('%Y-%m-%dT00:00:00Z')
18
18
  end
19
19
 
20
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
20
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
21
21
  verify_config_exists
22
22
 
23
23
  summary = add_color('Summary', :bright_yellow)
@@ -20,7 +20,7 @@ module Dri
20
20
  @options = options
21
21
  end
22
22
 
23
- def execute(input: $stdin, output: $stdout)
23
+ def execute(*)
24
24
  verify_config_exists
25
25
  logger.info "Fetching pipelines' status, this might take a while..."
26
26
  logger.warn "This command needs a large window to correctly print the table"
@@ -149,8 +149,8 @@ module Dri
149
149
  # @param [Integer] pipeline_id
150
150
  # @param [Boolean] sanity
151
151
  def allure_report(pipeline_name:, pipeline_id:, sanity:)
152
- "https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}"\
153
- "/master/#{pipeline_id}/index.html"
152
+ "https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}" \
153
+ "/master/#{pipeline_id}/index.html"
154
154
  end
155
155
 
156
156
  # Returns the GCP bucket name for different pipeline types
@@ -14,7 +14,7 @@ module Dri
14
14
  @options = options
15
15
  end
16
16
 
17
- def execute(folder: nil, input: $stdin, output: $stdout)
17
+ def execute(folder: nil, _input: $stdin, output: $stdout)
18
18
  return fetch_summary(output: output) unless folder
19
19
 
20
20
  fetch_runbook("#{folder}/index.md", output: output)
@@ -21,7 +21,7 @@ module Dri
21
21
  ]
22
22
  end
23
23
 
24
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
24
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
25
25
  verify_config_exists
26
26
 
27
27
  if @options[:filter_pipelines]
@@ -16,7 +16,7 @@ module Dri
16
16
  @options = options
17
17
  end
18
18
 
19
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
19
+ def execute(*) # rubocop:disable Metrics/AbcSize
20
20
  verify_config_exists
21
21
 
22
22
  title = add_color('Title', :magenta)
@@ -48,8 +48,6 @@ module Dri
48
48
  desc 'failures', 'Display failures opened on a given period.'
49
49
  method_option :help, aliases: '-h', type: :boolean,
50
50
  desc: 'Display usage information'
51
- method_option :urgent, type: :boolean,
52
- desc: 'Shows failures that quickly escalated'
53
51
  method_option :sort_by, type: :string,
54
52
  desc: 'Shows failures in specified order'
55
53
  method_option :start_date, type: :string,
@@ -68,28 +66,6 @@ module Dri
68
66
  end
69
67
  end
70
68
 
71
- desc 'quarantines', 'Display open quarantine MRs'
72
- method_option :help, aliases: '-h', type: :boolean,
73
- desc: 'Display usage information'
74
- def quarantines(*)
75
- if options[:help]
76
- invoke :help, ['quarantines']
77
- else
78
- require_relative 'fetch/quarantines'
79
- Dri::Commands::Fetch::Quarantines.new(options, search: '[QUARANTINE]').execute
80
- end
81
- end
82
-
83
- desc 'dequarantines', 'Display open dequarantine MRs'
84
- method_option :help, aliases: '-h', type: :boolean,
85
- desc: 'Display usage information'
86
- def dequarantines(*)
87
- return invoke :help, %w[quarantines] if options[:help]
88
-
89
- require_relative 'fetch/quarantines'
90
- Dri::Commands::Fetch::Quarantines.new(options, search: '[DEQUARANTINE]').execute
91
- end
92
-
93
69
  desc 'pipelines', 'Display status of pipelines'
94
70
  method_option :help, aliases: '-h', type: :boolean,
95
71
  desc: 'Display pipelines usage information'
@@ -15,7 +15,7 @@ module Dri
15
15
  @options = options
16
16
  end
17
17
 
18
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
18
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
19
19
  verify_config_exists
20
20
 
21
21
  incident = add_color('Incident', :bright_yellow)
@@ -14,7 +14,7 @@ module Dri
14
14
  puts pastel.yellow(font.write("DRI"))
15
15
  end
16
16
 
17
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
17
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
18
18
  output.puts "🤖 Welcome to DRI 🤖\n"
19
19
 
20
20
  logger.info "🔎 Scanning for existing configurations...\n"
@@ -11,7 +11,7 @@ module Dri
11
11
  @options = options
12
12
  end
13
13
 
14
- def execute(input: $stdin, output: $stdout)
14
+ def execute(_input: $stdin, output: $stdout)
15
15
  if config.exist? && @options["edit"]
16
16
  editor.open(config.source_file)
17
17
  return
@@ -39,13 +39,13 @@ module Dri
39
39
 
40
40
  def pretty_print_profile
41
41
  <<~PROFILE
42
- #{add_color('User:', :bright_cyan)} #{username}
42
+ #{add_color('User:', :bright_cyan)} #{username}#{' '}
43
43
  #{add_color('Token:', :bright_cyan)} #{token}
44
44
  #{add_color('OpsToken:', :bright_cyan)} #{ops_token}
45
45
  #{add_color('Timezone:', :bright_cyan)} #{timezone}
46
46
  #{add_color('Emoji:', :bright_cyan)} #{emoji}
47
47
  #{add_color('Report Path:', :bright_cyan)} #{handover_report_path}
48
-
48
+
49
49
  PROFILE
50
50
  end
51
51
  end
@@ -24,7 +24,7 @@ module Dri
24
24
  @today_iso_format = Time.now.strftime('%Y-%m-%dT00:00:00Z')
25
25
  end
26
26
 
27
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
27
+ def execute(_input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
28
28
  verify_config_exists
29
29
  report = Dri::Report.new(config)
30
30
 
@@ -195,9 +195,7 @@ module Dri
195
195
  FileUtils.mkdir_p("#{Dir.pwd}/handover_reports/.tmp")
196
196
  report_path = "handover_reports/.tmp/report-#{@date}.json"
197
197
 
198
- File.open(report_path, 'w') do |out_file|
199
- out_file.write(posted_note.to_h.to_json)
200
- end
198
+ File.write(report_path, posted_note.to_h.to_json)
201
199
  end
202
200
 
203
201
  output.puts "Done! ✅\n"
@@ -10,7 +10,7 @@ module Dri
10
10
  @options = options
11
11
  end
12
12
 
13
- def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
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?"
@@ -11,7 +11,7 @@ module Dri
11
11
  @options = options
12
12
  end
13
13
 
14
- def execute(input: $stdin, output: $stdout)
14
+ def execute(*)
15
15
  verify_config_exists
16
16
 
17
17
  remove = prompt.yes? "Are you sure you want to remove existing profile?"
@@ -11,7 +11,7 @@ module Dri
11
11
  @options = options
12
12
  end
13
13
 
14
- def execute(input: $stdin, output: $stdout)
14
+ def execute(*)
15
15
  FileUtils.rm_rf(handover_report_path) if prompt.yes?("Remove everything in #{handover_report_path}?")
16
16
  end
17
17
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../command'
4
+
5
+ module Dri
6
+ module Commands
7
+ class View
8
+ class FastQuarantine < Dri::Command
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def execute(*)
14
+ verify_config_exists
15
+
16
+ logger.info "Fetching fast quarantined tests..."
17
+
18
+ file_content = api_client.get_fast_quarantine_tests
19
+
20
+ file_content.each_line do |line|
21
+ puts "• #{line}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Dri
6
+ module Commands
7
+ class View < Thor
8
+ namespace :view
9
+
10
+ desc 'fastquarantine', 'View fast-quarantine tests'
11
+ method_option :help, aliases: '-h', type: :boolean,
12
+ desc: 'Display usage information'
13
+ def fastquarantine(*)
14
+ if options[:help]
15
+ invoke :help, ['fastquarantine']
16
+ else
17
+ require_relative 'view/fast_quarantine'
18
+ Dri::Commands::View::FastQuarantine.new(options).execute
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -26,7 +26,7 @@ module Dri
26
26
  feature_flag_data = [summary, changed_on, url]
27
27
 
28
28
  labels = feature_flag.labels
29
- host_label = labels.select { |label| /^host::/.match(label) }.join('')
29
+ host_label = labels.select { |label| /^host::/.match(label) }.join
30
30
 
31
31
  case host_label
32
32
  when PRODUCTION
data/lib/dri/report.rb CHANGED
@@ -63,66 +63,17 @@ module Dri
63
63
 
64
64
  failure_type = filter_failure_type_labels(labels)
65
65
  assigned_status = assigned?(failure["assignees"])
66
- pipelines = filter_pipeline_labels(labels)
67
-
68
- linked_pipelines = link_pipelines(project_id, iid, pipelines, description)
66
+ pipelines = format_pipelines(labels)
69
67
 
70
68
  actions_status = actions_status_template(failure_type, assigned_status, actions_opts)
71
69
  actions_fixes = actions_fixes_template(related_mrs)
72
70
 
73
- @failures << [title, emojified_link, linked_pipelines, stack_trace, "#{actions_status}#{actions_fixes}"]
71
+ @failures << [title, emojified_link, pipelines, stack_trace, "#{actions_status}#{actions_fixes}"]
74
72
  end
75
73
 
76
74
  private
77
75
 
78
- def link_pipelines(project_id, iid, pipelines, description) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
79
- linked = []
80
- label_pipeline_map = {
81
- 'gitlab.com' => '/quality/production',
82
- 'canary.gitlab.com' => '/quality/canary',
83
- 'canary.staging.gitlab.com' => '/quality/staging-canary',
84
- 'main' => '/gitlab-org/gitlab-qa-mirror',
85
- 'master' => '/gitlab-org/gitlab-qa-mirror',
86
- 'pre.gitlab.com' => '/quality/preprod',
87
- 'staging-ref' => '/quality/staging-ref',
88
- 'staging.gitlab.com' => '/quality/staging',
89
- 'release' => '/quality/release'
90
- }
91
-
92
- failure_notes = @api_client.fetch_failure_notes(project_id, iid)
93
-
94
- return if pipelines.empty?
95
-
96
- pipelines.each do |pipeline|
97
- next unless label_pipeline_map.has_key?(pipeline)
98
-
99
- pipeline_in_notes_found = false
100
- pipeline_link = ''
101
- pipeline_markdown = pipeline.gsub(/.gitlab.com/, '')
102
-
103
- failure_notes.each do |note|
104
- next unless note.body.include?(label_pipeline_map.fetch(pipeline))
105
-
106
- pipeline_in_notes_found = true
107
- pipeline_link = URI.extract(note.body, %w[https])
108
- end
109
-
110
- unless pipeline_in_notes_found
111
- links_description = URI.extract(description, %w[https])
112
- pipeline_link = links_description.select { |link| link.include? label_pipeline_map.fetch(pipeline) }
113
- end
114
-
115
- unless pipeline_link.empty?
116
- pipeline_link_sanitized = pipeline_link.join.strip
117
- pipeline_markdown = "[#{pipeline_markdown}](#{pipeline_link_sanitized})"
118
- end
119
-
120
- linked << pipeline_markdown
121
- end
122
- linked.join(', ')
123
- end
124
-
125
- def actions_status_template(failure_type, assigned_status, actions_opts) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity/MethodLength
76
+ def actions_status_template(failure_type, assigned_status, actions_opts) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity/
126
77
  quarantined = ''
127
78
  reproduced = ''
128
79
  transient = ''
@@ -184,6 +135,15 @@ module Dri
184
135
  pipelines
185
136
  end
186
137
 
138
+ def format_pipelines(labels)
139
+ labels.filter_map do |label|
140
+ next unless label.include?(FOUND)
141
+
142
+ sanitized_label = label.gsub(FOUND, ' ').strip
143
+ sanitized_label.gsub(/.gitlab.com/, '')
144
+ end.join(', ')
145
+ end
146
+
187
147
  def filter_failure_type_labels(labels)
188
148
  labels.each do |label|
189
149
  @type = label.gsub!(FAILURE, ' ').to_s if label.include? FAILURE
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dri
4
+ module Support
5
+ # Common tools for use with influxdb metrics setup
6
+ #
7
+ module InfluxdbTools
8
+ # @return [String] bucket for storing metrics from main runs
9
+ INFLUX_MAIN_TEST_METRICS_BUCKET = "e2e-test-stats-main"
10
+
11
+ private
12
+
13
+ # Query client
14
+ #
15
+ # @return [QueryApi]
16
+ def query_api
17
+ return nil unless influx_client
18
+
19
+ @query_api ||= influx_client.create_query_api
20
+ end
21
+
22
+ # InfluxDb client
23
+ #
24
+ # @return [InfluxDB2::Client]
25
+ def influx_client
26
+ return nil unless ENV["QA_INFLUXDB_URL"] && ENV["QA_INFLUXDB_TOKEN"]
27
+
28
+ @influx_client ||= InfluxDB2::Client.new(
29
+ ENV.fetch("QA_INFLUXDB_URL", nil),
30
+ ENV.fetch("QA_INFLUXDB_TOKEN", nil),
31
+ bucket: INFLUX_MAIN_TEST_METRICS_BUCKET,
32
+ org: "gitlab-qa"
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -11,13 +11,14 @@ module Dri
11
11
  FEATURE_FLAG_LOG_PROJECT_ID = 15208716
12
12
  INFRA_TEAM_PROD_PROJECT_ID = 7444821
13
13
  RUNBOOKS_PROJECT_ID = 41045213
14
+ FAST_QUARANTINE_PROJECT_ID = 45427186
14
15
  end
15
16
 
16
17
  module Triage
17
18
  module Labels
18
19
  FOUND = 'found:'
19
20
  FAILURE = 'failure::'
20
- FAILURE_NEW = "#{FAILURE}new"
21
+ FAILURE_NEW = "#{FAILURE}new".freeze
21
22
  QA = "QA"
22
23
  INCIDENT = 'Incident::'
23
24
  SERVICE = 'Service::'
data/lib/dri/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dri
4
- VERSION = "0.10.2"
4
+ VERSION = "0.11.0"
5
5
  end