dri 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb71a1d13a00cb33c436608e3c34e40a4202505bad341d31d5d85803c36495d4
4
- data.tar.gz: 73648bafa41eac28824435b62a41ead17494c144bbd99c6af52062e616379f2a
3
+ metadata.gz: 9f99fb005dc6cbc12b4fe9b8b131680c09474c125d7fe396fa3b0fadd8804dba
4
+ data.tar.gz: dfa14e8f527bfed0d17a428d81ac16955251cba3ef19907016a19bc43901e768
5
5
  SHA512:
6
- metadata.gz: 6c2d592d0906e3764ec1ab2fe298a42a1798be1d58dd216261ed2a88227cfe007a97a175ae199f5ee4f4db16f7efe2dd6f99521e8febc740e191b61c0f2ef8cc
7
- data.tar.gz: 1106b7fc11be59d38c72f471f8a5148eb4e724a81665b4382185da5c22a4e554fc7e1b1cc274a980b9ceeb6071f7fa5da0397e3657655132a6d21f2a31cf9712
6
+ metadata.gz: f7e619cb7241e804810b8cdd97759df84c98aada62c975e0d39fa9f9125219846f5e8bddb50624ce22cc1f836006b023d49375f0a17e298d0f0086d220390e87
7
+ data.tar.gz: '09e94ad8bc08ffb95839e01456703e81f8ed262c7c925beadeb21d71a816f6632b5772b91af5700901331c5e7560af56b1398ed41ac6dc04d884f61b6ab6043b'
data/.gitignore CHANGED
@@ -13,4 +13,5 @@
13
13
 
14
14
  .dri_profile.yml
15
15
  handover_reports/*
16
+ analyze_reports/*
16
17
  .idea/
data/.gitlab-ci.yml CHANGED
@@ -1,10 +1,10 @@
1
1
  .job_base:
2
- image: ruby:2.7
2
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3
3
3
  variables:
4
4
  BUNDLE_PATH: vendor/bundle
5
5
  BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true"
6
+ BUNDLE_FROZEN: "true"
6
7
  before_script:
7
- - gem install bundler -v 2.3.9 --no-document
8
8
  - bundle install
9
9
  cache:
10
10
  key:
@@ -13,26 +13,34 @@
13
13
  - Gemfile.lock
14
14
  paths:
15
15
  - vendor/bundle
16
+ policy: pull
16
17
  rules:
17
18
  - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
18
19
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
19
20
 
20
21
  include:
21
22
  - project: gitlab-org/quality/pipeline-common
22
- ref: 0.3.4
23
+ ref: 0.15.1
23
24
  file:
24
25
  - /ci/gem-release.yml
26
+ - /ci/ref-update.gitlab-ci.yml
27
+
28
+ variables:
29
+ RUBY_VERSION: "2.7"
25
30
 
26
31
  stages:
27
32
  - build
28
33
  - test
29
34
  - deploy
35
+ - update
30
36
 
31
37
  build_gem:
32
38
  stage: build
33
39
  extends: .job_base
34
40
  script:
35
41
  - gem build
42
+ cache:
43
+ policy: pull-push
36
44
 
37
45
  rubocop:
38
46
  stage: test
@@ -43,13 +51,8 @@ rubocop:
43
51
  rspec:
44
52
  stage: test
45
53
  extends: .job_base
54
+ parallel:
55
+ matrix:
56
+ - RUBY_VERSION: ['2.7', '3.0']
46
57
  script:
47
- - bundle exec rspec --color
48
-
49
- rspec 3.0:
50
- stage: test
51
- extends: .job_base
52
- image: ruby:3.0
53
- script:
54
- - bundle exec rspec --color
55
-
58
+ - bundle exec rspec --force-color
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.0.2
1
+ ruby 3.0.4
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dri (0.5.0)
4
+ dri (0.6.0)
5
+ amatch (~> 0.4.1)
5
6
  gitlab (~> 4.18)
6
7
  httparty (~> 0.20.0)
7
8
  json (~> 2.6.1)
@@ -27,14 +28,17 @@ GEM
27
28
  tzinfo (~> 2.0)
28
29
  addressable (2.8.0)
29
30
  public_suffix (>= 2.0.2, < 5.0)
31
+ amatch (0.4.1)
32
+ mize
33
+ tins (~> 1.0)
30
34
  ast (2.4.2)
31
35
  coderay (1.1.3)
32
36
  concurrent-ruby (1.1.10)
33
37
  crack (0.4.5)
34
38
  rexml
35
39
  diff-lcs (1.5.0)
36
- gitlab (4.18.0)
37
- httparty (~> 0.18)
40
+ gitlab (4.19.0)
41
+ httparty (~> 0.20)
38
42
  terminal-table (>= 1.5.1)
39
43
  gitlab-styles (7.0.0)
40
44
  rubocop (~> 0.91, >= 0.91.1)
@@ -56,12 +60,16 @@ GEM
56
60
  mime-types-data (~> 3.2015)
57
61
  mime-types-data (3.2022.0105)
58
62
  minitest (5.15.0)
63
+ mize (0.4.0)
64
+ protocol (~> 2.0)
59
65
  multi_xml (0.6.0)
60
66
  parallel (1.22.1)
61
67
  parser (3.1.1.0)
62
68
  ast (~> 2.4.1)
63
69
  pastel (0.8.0)
64
70
  tty-color (~> 0.5)
71
+ protocol (2.0.0)
72
+ ruby_parser (~> 3.0)
65
73
  pry (0.14.1)
66
74
  coderay (~> 1.1)
67
75
  method_source (~> 1.0)
@@ -110,15 +118,21 @@ GEM
110
118
  rubocop (~> 0.87)
111
119
  rubocop-ast (>= 0.7.1)
112
120
  ruby-progressbar (1.11.0)
121
+ ruby_parser (3.19.1)
122
+ sexp_processor (~> 4.16)
123
+ sexp_processor (4.16.1)
113
124
  strings (0.2.1)
114
125
  strings-ansi (~> 0.2)
115
126
  unicode-display_width (>= 1.5, < 3.0)
116
127
  unicode_utils (~> 1.4)
117
128
  strings-ansi (0.2.0)
129
+ sync (0.5.0)
118
130
  terminal-table (3.0.2)
119
131
  unicode-display_width (>= 1.1.1, < 3)
120
132
  thor (1.0.1)
121
133
  timecop (0.9.5)
134
+ tins (1.31.1)
135
+ sync
122
136
  tty-box (0.7.0)
123
137
  pastel (~> 0.8)
124
138
  strings (~> 0.2.0)
@@ -162,10 +176,10 @@ DEPENDENCIES
162
176
  dri!
163
177
  gitlab-styles (~> 7.0.0)
164
178
  pry (~> 0.14.1)
165
- rake
179
+ rake (~> 13.0)
166
180
  rspec (~> 3.10.0)
167
181
  timecop (~> 0.9.1)
168
182
  webmock (~> 3.5)
169
183
 
170
184
  BUNDLED WITH
171
- 2.3.9
185
+ 2.3.16
data/README.md CHANGED
@@ -66,7 +66,9 @@ $ dri profile
66
66
  - profile
67
67
  - reports
68
68
  - [6. incidents](#6-incidents)
69
- - [7. version](#7-version)
69
+ - [7. analyze](#7-analyze)
70
+ - stacktraces
71
+ - [8. version](#8-version)
70
72
 
71
73
  #### 1. init
72
74
 
@@ -123,12 +125,19 @@ $ dri fetch dequarantines
123
125
 
124
126
  Fetches open dequarantine Merge Requests to be reviewed
125
127
 
128
+ Results are organized by environment (production, staging, staging ref and preprod).
126
129
  ```shell
127
130
  $ dri fetch featureflags
128
131
  ```
129
132
 
130
133
  Fetches a list of today's feature flag changes, including the date and time in UTC of when the change occurred as well as a link to the corresponding issue from the feature-flag-log project.
131
- Results are organized by environment (production, staging, staging ref and preprod).
134
+
135
+ ```shell
136
+ $ dri fetch pipelines
137
+ ```
138
+
139
+ Fetches a table containing last executed pipeline and its test report link for all monitored pipelines.
140
+ The timestamps are in UTC
132
141
 
133
142
  #### 4. publish
134
143
 
@@ -191,7 +200,17 @@ $ dri incidents
191
200
 
192
201
  Have a quick look at currently active/mitigated incidents on GitLab services.
193
202
 
194
- #### 7. version
203
+ #### 7. analyze
204
+
205
+ ```shell
206
+ $ dri analyze stacktraces
207
+ ```
208
+ Searches through any open test failure issues and publishes a report that identifies
209
+ issues that have similar stack traces.
210
+ This may be useful to identify situations where a common test failure is presenting
211
+ itself across multiple individual test cases, over a period of time.
212
+
213
+ #### 8. version
195
214
 
196
215
  ```shell
197
216
  $ dri version
data/dri.gemspec CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
+ spec.add_dependency 'amatch', '~> 0.4.1'
25
26
  spec.add_dependency "gitlab", "~> 4.18"
26
27
  spec.add_dependency 'httparty', '~> 0.20.0'
27
28
  spec.add_dependency 'json', '~> 2.6.1'
@@ -39,7 +40,7 @@ Gem::Specification.new do |spec|
39
40
 
40
41
  spec.add_development_dependency 'gitlab-styles', '~> 7.0.0'
41
42
  spec.add_development_dependency "pry", "~> 0.14.1"
42
- spec.add_development_dependency 'rake'
43
+ spec.add_development_dependency 'rake', "~> 13.0"
43
44
  spec.add_development_dependency 'rspec', '~> 3.10.0'
44
45
  spec.add_development_dependency "timecop", "~> 0.9.1"
45
46
  spec.add_development_dependency 'webmock', '~> 3.5'
@@ -3,20 +3,28 @@
3
3
  require "httparty"
4
4
  require "json"
5
5
  require "tty-config"
6
- require 'cgi'
7
- require 'gitlab'
6
+ require "cgi"
7
+ require "gitlab"
8
8
 
9
9
  module Dri
10
- class ApiClient
11
- API_URL = 'https://gitlab.com/api/v4'
12
- TESTCASES_PROJECT_ID = '11229385'
13
- TRIAGE_PROJECT_ID = '15291320'
14
- GITLAB_PROJECT_ID = '278964'
15
- FEATURE_FLAG_LOG_PROJECT_ID = '15208716'
16
- INFRA_TEAM_PROD_PROJECT_ID = '7444821'
17
-
18
- def initialize(config)
10
+ TokenNotProvidedError = Class.new(StandardError)
11
+ class ApiClient # rubocop:disable Metrics/ClassLength
12
+ API_URL = "https://gitlab.com/api/v4"
13
+ OPS_API_URL = "https://ops.gitlab.net/api/v4"
14
+ TESTCASES_PROJECT_ID = "11229385"
15
+ TRIAGE_PROJECT_ID = "15291320"
16
+ GITLAB_PROJECT_ID = "278964"
17
+ FEATURE_FLAG_LOG_PROJECT_ID = "15208716"
18
+ INFRA_TEAM_PROD_PROJECT_ID = "7444821"
19
+ def initialize(config, ops = false)
19
20
  @token = config.read.dig("settings", "token")
21
+ @ops_token = config.read.dig("settings", "ops_token")
22
+ if @token.nil? || @ops_token.nil?
23
+ raise TokenNotProvidedError, "Gitlab API client cannot be initialized without both access tokens. " \
24
+ "Run dri init again or dri profile --edit"
25
+ end
26
+
27
+ @ops_instance = ops
20
28
  end
21
29
 
22
30
  # Fetch triaged failures
@@ -57,6 +65,18 @@ module Dri
57
65
  ).auto_paginate
58
66
  end
59
67
 
68
+ # Fetch issues related to failing test cases
69
+ #
70
+ # @return [Array<Gitlab::ObjectifiedHash>]
71
+ def fetch_test_failure_issues(labels: 'failure::new')
72
+ gitlab.issues(
73
+ GITLAB_PROJECT_ID,
74
+ labels: labels,
75
+ state: 'opened',
76
+ scope: "all"
77
+ ).auto_paginate
78
+ end
79
+
60
80
  # Fetch related issue mrs
61
81
  #
62
82
  # @param [Integer] issue_iid
@@ -149,18 +169,73 @@ module Dri
149
169
  gitlab.issues(INFRA_TEAM_PROD_PROJECT_ID, order_by: "updated_at", state: "opened", labels: "incident")
150
170
  end
151
171
 
172
+ # Fetch pipelines
173
+ #
174
+ # @param [Integer] project_id
175
+ # @return [Array<Gitlab::ObjectifiedHash>]
176
+ def pipelines(project_id:, options:, auto_paginate: false)
177
+ if auto_paginate
178
+ gitlab.pipelines(project_id, options).auto_paginate
179
+ else
180
+ gitlab.pipelines(project_id, options)
181
+ end
182
+ end
183
+
184
+ # Fetch single pipeline
185
+ #
186
+ # @param [Integer] project_id
187
+ # @param [Integer] pipeline_id
188
+ # @return [<Gitlab::ObjectifiedHash>]
189
+ def pipeline(project_id, pipeline_id)
190
+ gitlab.pipeline(project_id, pipeline_id)
191
+ end
192
+
193
+ # Fetch test report from a pipeline
194
+ #
195
+ # @param [Integer] project_id
196
+ # @param [Integer] pipeline_id
197
+ # @return [<Gitlab::ObjectifiedHash>]
198
+ def pipeline_test_report(project_id, pipeline_id)
199
+ gitlab.pipeline_test_report(project_id, pipeline_id)
200
+ end
201
+
202
+ # Fetch pipeline bridges/downstream pipelines
203
+ #
204
+ # @param [Integer] project_id
205
+ # @param [Integer] pipeline_id
206
+ # @return [Array<Gitlab::ObjectifiedHash>]
207
+ def pipeline_bridges(project_id, pipeline_id, options = {})
208
+ gitlab.pipeline_bridges(project_id, pipeline_id, options).auto_paginate
209
+ end
210
+
211
+ # Fetch jobs from a pipeline
212
+ #
213
+ # @param [Integer] project_id
214
+ # @param [Integer] pipeline_id
215
+ # @return [Array<Gitlab::ObjectifiedHash>]
216
+ def pipeline_jobs(project_id, pipeline_id, options = {})
217
+ gitlab.pipeline_jobs(project_id, pipeline_id, options).auto_paginate
218
+ end
219
+
152
220
  private
153
221
 
154
- attr_reader :token
222
+ attr_reader :token, :ops_token
155
223
 
156
224
  # Gitlab client
157
225
  #
158
226
  # @return [Gitlab::Client]
159
227
  def gitlab
160
- @gitlab ||= Gitlab.client(
161
- endpoint: API_URL,
162
- private_token: token
163
- )
228
+ if @ops_instance
229
+ @ops_client ||= Gitlab.client(
230
+ endpoint: OPS_API_URL,
231
+ private_token: ops_token
232
+ )
233
+ else
234
+ @gitlab_client ||= Gitlab.client(
235
+ endpoint: API_URL,
236
+ private_token: token
237
+ )
238
+ end
164
239
  end
165
240
  end
166
241
  end
data/lib/dri/cli.rb CHANGED
@@ -82,5 +82,8 @@ module Dri
82
82
 
83
83
  require_relative 'commands/publish'
84
84
  register Dri::Commands::Publish, 'publish', 'publish [SUBCOMMAND]', 'Publish report for handover'
85
+
86
+ require_relative 'commands/analyze'
87
+ register Dri::Commands::Analyze, 'analyze', 'analyze [SUBCOMMAND]', 'Analysis of test failures and issues'
85
88
  end
86
89
  end
data/lib/dri/command.rb CHANGED
@@ -32,8 +32,8 @@ module Dri
32
32
  end
33
33
  end
34
34
 
35
- def api_client
36
- ApiClient.new(config)
35
+ def api_client(ops: false)
36
+ ApiClient.new(config, ops)
37
37
  end
38
38
 
39
39
  def profile
@@ -52,6 +52,10 @@ module Dri
52
52
  @token ||= profile["settings"]["token"]
53
53
  end
54
54
 
55
+ def ops_token
56
+ @ops_token ||= profile["settings"]["ops_token"]
57
+ end
58
+
55
59
  def timezone
56
60
  @timezone ||= profile["settings"]["timezone"]
57
61
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../command'
4
+ require_relative '../../utils/table'
5
+ require 'amatch'
6
+ require 'fileutils'
7
+
8
+ module Dri
9
+ module Commands
10
+ class Analyze
11
+ class StackTraces < Dri::Command
12
+ include Amatch
13
+ include Dri::Utils::Table
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ @labels = options[:labels] || 'failure::new'
18
+ @similarity_score_threshold = options[:similarity_score_threshold] || 0.9
19
+ end
20
+
21
+ def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize
22
+ verify_config_exists
23
+ logger.info "#{Time.now.utc} Fetching issues"
24
+
25
+ data = []
26
+
27
+ spinner.run do
28
+ response = api_client.fetch_test_failure_issues(labels: @labels)
29
+ logger.info "#{Time.now.utc} API response completed"
30
+
31
+ if response.empty?
32
+ logger.info 'There are no failure::new issues identified!'
33
+ exit 0
34
+ end
35
+
36
+ data = identify_similar_issues(response)
37
+
38
+ logger.info "#{Time.now.utc} Processing Data Complete"
39
+ end
40
+
41
+ similar_stack_traces = data.each_with_object([]) do |item, similar_items|
42
+ next if item[:stack_trace].empty?
43
+ next if item[:related_errors].length < 2
44
+
45
+ similar_items << [item[:stack_trace], item[:related_errors]]
46
+ end
47
+
48
+ FileUtils.mkdir_p("#{Dir.pwd}/analyze_reports/stacktraces")
49
+ report_path = "analyze_reports/stacktraces/report-#{Time.now.utc.to_i}.md"
50
+ write_report(similar_stack_traces, report_path)
51
+ logger.success "Analyze StackTraces report is ready at: #{report_path}"
52
+
53
+ errors_count = similar_stack_traces.count
54
+ issues_count = similar_stack_traces.sum { |st| st[1].size }
55
+ output.puts "Found #{errors_count} common errors across a combination of #{issues_count} issues"
56
+ end
57
+
58
+ private
59
+
60
+ def write_report(similar_stack_traces, report_path)
61
+ File.open(report_path, 'a') do |out_file|
62
+ out_file.puts "## Stack Trace Analysis"
63
+ similar_stack_traces.each do |st|
64
+ out_file.puts "### StackTrace"
65
+ out_file.puts st[0]
66
+ out_file.puts "### Related issues"
67
+ st[1].each { |err| out_file.puts "* #{err}" }
68
+ out_file.puts "-" * 80
69
+ end
70
+ end
71
+ end
72
+
73
+ def identify_similar_issues(response)
74
+ data = []
75
+ response.each do |item|
76
+ stack_trace = extract_stack_trace(item['description'])
77
+
78
+ data.find(-> { add_stack_trace(data, stack_trace) }) do |row|
79
+ calc_similarity(row[:stack_trace], stack_trace) >= @similarity_score_threshold
80
+ end[:related_errors].append(item['web_url'])
81
+ end
82
+ data
83
+ end
84
+
85
+ def add_stack_trace(data, st)
86
+ obj = { stack_trace: st, related_errors: [] }
87
+ data.append(obj)
88
+ obj
89
+ end
90
+
91
+ def extract_stack_trace(description)
92
+ stack_trace = description[/```(.*)```/m]
93
+ # Remove common patterns that may impact matching
94
+ stack_trace&.gsub!(/^*Correlation Id: \S*$/, '')
95
+ stack_trace&.gsub!(/^*Sentry Url: \S*$/, '')
96
+ stack_trace&.gsub!(/^*Kibana Url: \S*$/, '')
97
+ stack_trace || ''
98
+ end
99
+
100
+ def calc_similarity(s1, s2)
101
+ JaroWinkler.new(s1).match(s2)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Dri
6
+ module Commands
7
+ class Analyze < Thor
8
+ namespace :analyze
9
+
10
+ desc 'stacktraces', 'Identify commonalities and patterns between stack traces'
11
+ method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
12
+ def stacktraces(*)
13
+ return invoke :help, %w[stacktraces] if options[:help]
14
+
15
+ require_relative 'analyze/stack_traces'
16
+ Dri::Commands::Analyze::StackTraces.new(options).execute
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative '../../command'
5
+ require_relative '../../utils/table'
6
+ require_relative '../../utils/constants'
7
+
8
+ module Dri
9
+ module Commands
10
+ class Fetch
11
+ class Pipelines < Dri::Command # rubocop:disable Metrics/ClassLength
12
+ include Dri::Utils::Table
13
+ using Refinements
14
+
15
+ NUM_OF_TESTS_LIVE_ENV = 1000
16
+ NOT_FOUND = "Not found"
17
+
18
+ def initialize(options)
19
+ @options = options
20
+ end
21
+
22
+ def execute(input: $stdin, output: $stdout)
23
+ verify_config_exists
24
+ logger.info "Fetching pipelines' status, this might take a while..."
25
+ pipelines = []
26
+ table_labels = define_table_labels
27
+
28
+ spinner.run do
29
+ Dri::Utils::Constants::PIPELINE_ENVIRONMENTS.each do |environment, details|
30
+ logger.info "Fetching last executed #{environment} pipeline"
31
+ pipelines << fetch_pipeline(pipeline_name: environment.to_s, details: details)
32
+ logger.info "Fetching complete for #{environment}"
33
+ end
34
+ end
35
+
36
+ print_table(table_labels, pipelines, alignments: [:left, :center, :center, :left])
37
+ pipelines # Returning the array mainly for spec
38
+ end
39
+
40
+ private
41
+
42
+ # Format a past date
43
+ # @param [Integer] hours_ago the amount of hours from now
44
+ # @return [String] formatted datetime
45
+ def past_timestamp(hours_ago)
46
+ timestamp = Time.now - (hours_ago * 60 * 60)
47
+ timestamp.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
48
+ end
49
+
50
+ # Get the first downstream pipeline of a project
51
+ # @param [Integer] project_id the id of the project
52
+ # @param [Integer] pipeline_id the pipeline id
53
+ # @return [Gitlab::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
54
+ def bridge_pipeline(project_id, pipeline_id)
55
+ bridges = api_client.pipeline_bridges(project_id, pipeline_id)
56
+ return if bridges.empty? # If downstream pipeline doesn't exist, which triggers the QA tests, return
57
+
58
+ bridges.first["downstream_pipeline"]
59
+ end
60
+
61
+ # Get jobs from a pipeline
62
+ # @param [Integer] project_id the id of the project
63
+ # @param [Integer] pipeline_id the pipeline id
64
+ # @param [Boolean] ops true if ops instance
65
+ # @return [Array::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
66
+ def jobs(project_id:, pipeline_id:, ops: false)
67
+ api_client(ops: ops).pipeline_jobs(project_id, pipeline_id)
68
+ end
69
+
70
+ # Checks if tests count exceeds threshold in a pipeline
71
+ # @param [Integer] project_id the id of the project
72
+ # @param [Integer] pipeline_id the pipeline id
73
+ # @param [Boolean] ops true if ops instance
74
+ # @return [Boolean] true if count exceeds threshold defined in constant NUM_OF_TESTS_LIVE_ENV
75
+ def tests_exceed_threshold?(project_id:, pipeline_id:, ops: true)
76
+ api_client(ops: ops).pipeline_test_report(project_id, pipeline_id).total_count > NUM_OF_TESTS_LIVE_ENV
77
+ end
78
+
79
+ # Checks if a job is present in an array of jobs
80
+ # @param [Array] jobs
81
+ # @param [String] job_name name of the job
82
+ # @return [Boolean] true if job is present
83
+ def contains_job?(jobs, job_name:)
84
+ jobs.any? { |job| job["name"].include?(job_name) }
85
+ end
86
+
87
+ # Checks if a stage is present from a list of jobs
88
+ # @param [Array] jobs
89
+ # @param [String] stage_name name of the stage
90
+ # @return [Boolean] true if stage is present
91
+ def contains_stage?(jobs, stage_name)
92
+ jobs.any? { |job| job["stage"].include?(stage_name) }
93
+ end
94
+
95
+ # Checks if pipeline ran only the QA smoke tests
96
+ # @param [Array] jobs
97
+ # @param [Integer] project_id
98
+ # @param [Integer] pipeline_id
99
+ # @param [Boolean] ops true if ops instance
100
+ def smoke_run?(jobs:, project_id:, pipeline_id:, ops:)
101
+ contains_stage?(jobs, "sanity") &&
102
+ !tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
103
+ end
104
+
105
+ # Checks if pipeline ran full suite of qa tests
106
+ # @param [Array] jobs
107
+ # @param [Integer] project_id
108
+ # @param [Integer] pipeline_id
109
+ # @param [Boolean] ops true if ops instance
110
+ def full_run?(jobs:, project_id:, pipeline_id:, ops:)
111
+ if ops
112
+ (contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")) &&
113
+ tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
114
+ else
115
+ contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")
116
+ # Nightly pipeline does not execute full E2E suite if sanity fails so can't check tests count
117
+ end
118
+ end
119
+
120
+ # Combined logic to check if a pipeline was a sanity run for all pipeline types - ie., live environment
121
+ # and gitlab-qa-mirror pipelines
122
+ # @param [Array] pipeline_jobs
123
+ # @param [Object] pipeline
124
+ # @param [Boolean] ops
125
+ def sanity?(pipeline_jobs:, pipeline:, ops:)
126
+ return true if ops && smoke_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
127
+ pipeline_id: pipeline.id, ops: ops)
128
+
129
+ false if full_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
130
+ pipeline_id: pipeline.id, ops: ops)
131
+ end
132
+
133
+ # Constructs allure report url for each pipeline
134
+ # @param [String] pipeline_name
135
+ # @param [Integer] pipeline_id
136
+ # @param [Boolean] sanity
137
+ def allure_report(pipeline_name:, pipeline_id:, sanity:)
138
+ "https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}"\
139
+ "/master/#{pipeline_id}/index.html"
140
+ end
141
+
142
+ # Returns the GCP bucket name for different pipeline types
143
+ # @param [String] pipeline_name
144
+ # @param [Boolean] sanity
145
+ def allure_bucket_name(pipeline_name, sanity)
146
+ case pipeline_name
147
+ when "master"
148
+ "package-and-qa"
149
+ when "nightly"
150
+ pipeline_name
151
+ when "pre_prod"
152
+ "preprod-#{run_type(sanity)}"
153
+ else
154
+ "#{pipeline_name.sub('_', '-')}-#{run_type(sanity)}"
155
+ end
156
+ end
157
+
158
+ def run_type(sanity)
159
+ sanity == true ? "sanity" : "full"
160
+ end
161
+
162
+ # Returns table headers
163
+ # @return [Array]
164
+ def define_table_labels
165
+ name = add_color("Pipeline", :magenta)
166
+ pipeline_last_executed = add_color("Last executed at", :magenta)
167
+ url = add_color("Pipeline Url", :magenta)
168
+ report = add_color("Last report", :magenta)
169
+ result = add_color("Result", :magenta)
170
+ [name, pipeline_last_executed, url, report, result]
171
+ end
172
+
173
+ # Checks if pipeline is running on ops.gitlab.net or gitlab.com
174
+ # @param [String] url
175
+ def ops_pipeline?(url)
176
+ url.include?("ops.gitlab.net")
177
+ end
178
+
179
+ def notify_slack_job_name(pipeline_name, ops)
180
+ return "notify-slack-qa-fail" if ops
181
+
182
+ pipeline_name.to_s.include?("master") ? "notify_slack" : "notify-slack-fail"
183
+ end
184
+
185
+ # Returns child pipeline if it is master pipeline
186
+ # @param [Gitlab::ObjectifiedHash] pipeline
187
+ def pipeline_with_qa_tests(pipeline)
188
+ if pipeline.web_url.to_s.include?("gitlab-qa-mirror")
189
+ bridge_pipeline(pipeline.project_id, pipeline.id)
190
+ else
191
+ pipeline
192
+ end
193
+ end
194
+
195
+ # Returns query options for pipelines api call
196
+ # @param [Hash] details
197
+ # @param [Boolean] ops
198
+ def options(details, ops)
199
+ options = { order_by: "updated_at", scope: "finished",
200
+ updated_after: past_timestamp(details[:search_hours_ago]) }
201
+ options.merge(username: "gitlab-bot") if ops
202
+ options
203
+ end
204
+
205
+ def emoji_for_success_failure(status)
206
+ return add_color("✓", :green) if status.include?("success")
207
+
208
+ add_color("x", :red)
209
+ end
210
+
211
+ # @param [String] pipeline_name
212
+ # @param [Hash] details Pipeline environment details
213
+ # @return [Array] Array of last executed pipeline details
214
+ # rubocop:disable Metrics/PerceivedComplexity
215
+ def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity
216
+ ops = ops_pipeline?(details[:url])
217
+ options = options(details, ops)
218
+ # instance is ops.gitlab.net or gitlab.com
219
+ response = api_client(ops: ops).pipelines(project_id: details[:project_id],
220
+ options: options, auto_paginate: true)
221
+ return [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] if response.empty?
222
+
223
+ # Return empty data to the table if no matching pipelines were found from the query
224
+ response.each do |pipeline|
225
+ pipeline_to_scan = pipeline_with_qa_tests(pipeline) # Fetch child pipeline if it is master
226
+ next if pipeline_to_scan.nil?
227
+
228
+ pipeline_jobs = jobs(project_id: pipeline.project_id, pipeline_id: pipeline_to_scan.id, ops: ops)
229
+ next unless contains_job?(pipeline_jobs, job_name: notify_slack_job_name(pipeline_name, ops))
230
+
231
+ # Need to know if it is a sanity or a full run to construct allure report url
232
+ sanity = sanity?(pipeline_jobs: pipeline_jobs, pipeline: pipeline_to_scan,
233
+ ops: ops)
234
+
235
+ next if sanity.nil? # To filter out some "clean up" pipelines present in live environments
236
+
237
+ next if sanity && @options[:full_runs_only] # Filter out sanity runs if --full-runs-only option is passed
238
+
239
+ name = ops ? "#{pipeline_name}_#{run_type(sanity)}" : pipeline_name
240
+ pipeline_last_executed = pipeline_to_scan.updated_at
241
+ url = pipeline_to_scan.web_url
242
+ report = allure_report(pipeline_name: pipeline_name, pipeline_id: pipeline_to_scan.id, sanity: sanity)
243
+ result = emoji_for_success_failure(pipeline_to_scan.status)
244
+ return [name, pipeline_last_executed, url, report, result]
245
+ end
246
+
247
+ [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] # Parsed through all of the response and
248
+ # no matching pipelines found
249
+ end
250
+ # rubocop:enable Metrics/PerceivedComplexity
251
+ end
252
+ end
253
+ end
254
+ end
@@ -82,6 +82,18 @@ module Dri
82
82
  require_relative 'fetch/quarantines'
83
83
  Dri::Commands::Fetch::Quarantines.new(options, search: '[DEQUARANTINE]').execute
84
84
  end
85
+
86
+ desc 'pipelines', 'Display status of pipelines'
87
+ method_option :help, aliases: '-h', type: :boolean,
88
+ desc: 'Display pipelines usage information'
89
+ method_option :full_runs_only, type: :boolean,
90
+ desc: 'Displays full pipeline runs only'
91
+ def pipelines(*)
92
+ return invoke :help, %w[pipelines] if options[:help]
93
+
94
+ require_relative 'fetch/pipelines'
95
+ Dri::Commands::Fetch::Pipelines.new(options).execute
96
+ end
85
97
  end
86
98
  end
87
99
  end
@@ -31,16 +31,18 @@ module Dri
31
31
 
32
32
  @username = prompt.ask("What is your GitLab username?")
33
33
  @token = prompt.mask("Please provide your GitLab personal access token:")
34
+ @ops_token = prompt.mask("Please provide your ops.gitlab.net personal access token:")
34
35
  @timezone = prompt.select("Choose your current timezone?", %w[EMEA AMER APAC])
35
36
  @emoji = prompt.ask("Have a triage emoji?")
36
37
 
37
- if (@emoji || @token || @username).nil?
38
- logger.error "Please provide a username, token, timezone and emoji used for triage."
38
+ if (@emoji || @token || @username || @ops_token).nil?
39
+ logger.error "Please provide a username, gitlab token, ops token, timezone and emoji used for triage."
39
40
  exit 1
40
41
  end
41
42
 
42
43
  config.set(:settings, :user, value: @username)
43
44
  config.set(:settings, :token, value: @token)
45
+ config.set(:settings, :ops_token, value: @ops_token)
44
46
  config.set(:settings, :timezone, value: @timezone)
45
47
  config.set(:settings, :emoji, value: @emoji)
46
48
  config.write(force: true)
@@ -38,6 +38,7 @@ module Dri
38
38
  def pretty_print_profile
39
39
  <<~PROFILE
40
40
  #{add_color('User:', :bright_cyan)} #{username}\n #{add_color('Token:', :bright_cyan)} #{token}
41
+ #{add_color('OpsToken:', :bright_cyan)} #{ops_token}
41
42
  #{add_color('Timezone:', :bright_cyan)} #{timezone}
42
43
  #{add_color('Emoji:', :bright_cyan)} #{emoji}
43
44
  PROFILE
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dri
4
+ module Utils
5
+ module Constants
6
+ PIPELINE_ENVIRONMENTS =
7
+ {
8
+ production: {
9
+ name: "production",
10
+ url: "https://ops.gitlab.net/gitlab-org/quality/production",
11
+ project_id: "275",
12
+ search_hours_ago: 12
13
+ },
14
+ staging: {
15
+ name: "staging",
16
+ url: "https://ops.gitlab.net/gitlab-org/quality/staging",
17
+ project_id: "263",
18
+ search_hours_ago: 12
19
+ },
20
+ canary: {
21
+ name: "canary.gitlab.com",
22
+ url: "https://ops.gitlab.net/gitlab-org/quality/canary",
23
+ project_id: "276",
24
+ search_hours_ago: 12
25
+ },
26
+ staging_canary: {
27
+ name: "canary.staging.gitlab.com",
28
+ url: "https://ops.gitlab.net/gitlab-org/quality/staging-canary",
29
+ project_id: "547",
30
+ search_hours_ago: 12
31
+ },
32
+ nightly: {
33
+ name: "nightly",
34
+ url: "https://gitlab.com/gitlab-org/quality/nightly",
35
+ project_id: "7523614",
36
+ search_hours_ago: 24
37
+ },
38
+ pre_prod: {
39
+ name: "pre.gitlab.com",
40
+ url: "https://ops.gitlab.net/gitlab-org/quality/preprod",
41
+ project_id: "294",
42
+ search_hours_ago: 12
43
+ },
44
+ staging_ref: {
45
+ name: "staging-ref",
46
+ url: "https://ops.gitlab.net/gitlab-org/quality/staging-ref",
47
+ project_id: "536",
48
+ search_hours_ago: 12
49
+ },
50
+ master: {
51
+ name: "master",
52
+ url: "https://gitlab.com/gitlab-org/gitlab-qa-mirror",
53
+ project_id: "14707715",
54
+ search_hours_ago: 6
55
+ }
56
+ }.freeze
57
+ end
58
+ end
59
+ end
@@ -5,7 +5,7 @@ require 'tty-table'
5
5
  module Dri
6
6
  module Utils
7
7
  module Table
8
- def print_table(headers, rows, alignments: [])
8
+ def print_table(headers, rows, alignments: [], **kwargs)
9
9
  if alignments.empty?
10
10
  (1..headers.size).each do
11
11
  alignments.push(:center)
@@ -13,7 +13,7 @@ module Dri
13
13
  end
14
14
 
15
15
  table = TTY::Table.new(headers, rows)
16
- puts table.render(:ascii, resize: true, multiline: true, alignments: alignments)
16
+ puts table.render(:ascii, resize: true, multiline: true, alignments: alignments, **kwargs)
17
17
  end
18
18
  end
19
19
  end
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.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-04 00:00:00.000000000 Z
11
+ date: 2022-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: amatch
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.1
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: gitlab
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -238,16 +252,16 @@ dependencies:
238
252
  name: rake
239
253
  requirement: !ruby/object:Gem::Requirement
240
254
  requirements:
241
- - - ">="
255
+ - - "~>"
242
256
  - !ruby/object:Gem::Version
243
- version: '0'
257
+ version: '13.0'
244
258
  type: :development
245
259
  prerelease: false
246
260
  version_requirements: !ruby/object:Gem::Requirement
247
261
  requirements:
248
- - - ">="
262
+ - - "~>"
249
263
  - !ruby/object:Gem::Version
250
- version: '0'
264
+ version: '13.0'
251
265
  - !ruby/object:Gem::Dependency
252
266
  name: rspec
253
267
  requirement: !ruby/object:Gem::Requirement
@@ -318,9 +332,12 @@ files:
318
332
  - lib/dri/api_client.rb
319
333
  - lib/dri/cli.rb
320
334
  - lib/dri/command.rb
335
+ - lib/dri/commands/analyze.rb
336
+ - lib/dri/commands/analyze/stack_traces.rb
321
337
  - lib/dri/commands/fetch.rb
322
338
  - lib/dri/commands/fetch/failures.rb
323
339
  - lib/dri/commands/fetch/featureflags.rb
340
+ - lib/dri/commands/fetch/pipelines.rb
324
341
  - lib/dri/commands/fetch/quarantines.rb
325
342
  - lib/dri/commands/fetch/testcases.rb
326
343
  - lib/dri/commands/fetch/triaged.rb
@@ -337,6 +354,7 @@ files:
337
354
  - lib/dri/gitlab/issues.rb
338
355
  - lib/dri/refinements/truncate.rb
339
356
  - lib/dri/report.rb
357
+ - lib/dri/utils/constants.rb
340
358
  - lib/dri/utils/feature_flag_consts.rb
341
359
  - lib/dri/utils/markdown_lists.rb
342
360
  - lib/dri/utils/table.rb