dri 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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