dri 0.5.1 → 0.6.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 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