dri 0.5.0 → 0.6.1

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: 879c4292254a2a17a0ebcd8e2c0a3f8bbc6e654a69c153ad66423c71eb750fc4
4
- data.tar.gz: 51f8e1acec1894ce4a6ab264ef690cf063259f46512d743609bf2f5a6529132f
3
+ metadata.gz: 990d9ed4c79b5b7c7ff2e5535dba71599a3189d4c742b918f98d3063a6e15c9f
4
+ data.tar.gz: 15e48f44496a55d5d13185409a91d9ddd525d127fd8dbe73265dc176c6d14634
5
5
  SHA512:
6
- metadata.gz: ad020b17eb8d05322800332e59b5d286dcb8712df055dab50be7f234df2864a5ba719f900d61b0ba25308a1ea130fa2b586b7f01215beec90eafb614cc8c6da1
7
- data.tar.gz: c0ef20575dcc1f9fa3312df5970b79a8b6fa11b8401a1a80d30561c8a764c7bcd140d4e649c619fbce1be6b9f71c943fb51aa5a48c20b18c5f480af4a821bc07
6
+ metadata.gz: 22a9838a3821c90cb24346f6f98b6158e2a71ec9bd0c4e7b5d9e447a75b676202be1d365e1603d7d12a8441f6b5d448f26337c33163b60f346819933da24fea3
7
+ data.tar.gz: 6b117a44436b2c4c064c3b19b22deb454a42c8d6d460492af7813e72299600d50ad2cef612e5e1f2a84566588f560d31c82da3c5c92b35f1d8749ea445e9664e
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.4.0)
4
+ dri (0.6.1)
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` to add an ops_token entry."
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
@@ -13,8 +13,9 @@ module Dri
13
13
  SORT_BY_OPTIONS = {
14
14
  title: 0,
15
15
  triaged: 1,
16
- author: 2,
17
- url: 3
16
+ environment: 2,
17
+ author: 3,
18
+ url: 4
18
19
  }.freeze
19
20
 
20
21
  def initialize(options)
@@ -25,14 +26,16 @@ module Dri
25
26
  def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
26
27
  verify_config_exists
27
28
 
29
+ urgent_environments = %w[canary canary.staging]
30
+
28
31
  title = add_color('Title', :bright_yellow)
29
32
  triaged = add_color('Triaged?', :bright_yellow)
33
+ environment = add_color('Environment', :bright_yellow)
30
34
  author = add_color('Author', :bright_yellow)
31
35
  url = add_color('URL', :bright_yellow)
32
36
 
33
37
  failures = []
34
- urgent = []
35
- labels = [title, triaged, author, url]
38
+ labels = [title, triaged, environment, author, url]
36
39
  triaged_counter = 0
37
40
 
38
41
  logger.info "Fetching today's failures..."
@@ -50,6 +53,12 @@ module Dri
50
53
  author = failure.to_h.dig('author', 'username')
51
54
  url = failure.web_url
52
55
  triaged = add_color('x', :red)
56
+ envs = failure.labels.select { |l| l.include?('found:') }.map do |l|
57
+ env = l.split(':').last.gsub('.gitlab.com', '')
58
+
59
+ env == 'gitlab.com' ? 'production' : env
60
+ end
61
+ urgent = urgent_environments.all? { |env| envs.include?(env) }
53
62
 
54
63
  emoji_awards = api_client.fetch_awarded_emojis(failure.iid).find do |e|
55
64
  e.name == emoji && e.to_h.dig('user', 'username') == username
@@ -61,32 +70,27 @@ module Dri
61
70
  end
62
71
 
63
72
  if @options[:urgent]
64
- labels = failure.labels
65
-
66
- labels.each do |label|
67
- if label.include?('found:canary.gitlab.com' && 'found:canary.staging.gitlab.com')
68
- urgent << [title, triaged, author, url]
69
- end
70
- end
73
+ failures << [title, triaged, envs.first, author, url] if urgent
74
+ else
75
+ failures << [title, triaged, envs.first, author, url]
71
76
  end
72
-
73
- failures << [title, triaged, author, url]
74
-
75
- failures.sort_by! { |e| e[SORT_BY_OPTIONS[@options[:sort_by].to_sym]] } if @options[:sort_by]
76
77
  end
77
- end
78
78
 
79
- if @options[:urgent]
80
- print_table(labels, urgent, alignments: [:left, :center, :center, :left])
81
- output.puts(<<~MSG)
82
- Found: #{urgent.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
83
- MSG
84
- else
85
- print_table(labels, failures, alignments: [:left, :center, :center, :left])
86
- output.puts(<<~MSG)
87
- Found: #{failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
88
- MSG
79
+ failures.sort_by! { |failure| failure[SORT_BY_OPTIONS[@options[:sort_by]&.to_sym || :environment]] }
89
80
  end
81
+
82
+ msg = if @options[:urgent]
83
+ <<~MSG
84
+ Found: #{failures.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
85
+ MSG
86
+ else
87
+ <<~MSG
88
+ Found: #{failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
89
+ MSG
90
+ end
91
+
92
+ print_table(labels, failures, alignments: [:left, :center, :center, :left])
93
+ output.puts(msg)
90
94
  end
91
95
  end
92
96
  end
@@ -0,0 +1,260 @@
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
+ logger.warn "This command needs a large window to correctly print the table"
26
+ pipelines = []
27
+ table_labels = define_table_labels
28
+
29
+ spinner.run do
30
+ Dri::Utils::Constants::PIPELINE_ENVIRONMENTS.each do |environment, details|
31
+ logger.info "Fetching last executed #{environment} pipeline"
32
+ pipelines << fetch_pipeline(pipeline_name: environment.to_s, details: details)
33
+ logger.info "Fetching complete for #{environment} ✓"
34
+ end
35
+ end
36
+
37
+ print_table(
38
+ table_labels,
39
+ pipelines,
40
+ alignments: [:center, :center, :center, :center, :center],
41
+ padding: [1, 1, 1, 1]
42
+ )
43
+ pipelines # Returning the array mainly for spec
44
+ end
45
+
46
+ private
47
+
48
+ # Format a past date
49
+ # @param [Integer] hours_ago the amount of hours from now
50
+ # @return [String] formatted datetime
51
+ def past_timestamp(hours_ago)
52
+ timestamp = Time.now - (hours_ago * 60 * 60)
53
+ timestamp.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
54
+ end
55
+
56
+ # Get the first downstream pipeline of a project
57
+ # @param [Integer] project_id the id of the project
58
+ # @param [Integer] pipeline_id the pipeline id
59
+ # @return [Gitlab::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
60
+ def bridge_pipeline(project_id, pipeline_id)
61
+ bridges = api_client.pipeline_bridges(project_id, pipeline_id)
62
+ return if bridges.empty? # If downstream pipeline doesn't exist, which triggers the QA tests, return
63
+
64
+ bridges.first["downstream_pipeline"]
65
+ end
66
+
67
+ # Get jobs from a pipeline
68
+ # @param [Integer] project_id the id of the project
69
+ # @param [Integer] pipeline_id the pipeline id
70
+ # @param [Boolean] ops true if ops instance
71
+ # @return [Array::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
72
+ def jobs(project_id:, pipeline_id:, ops: false)
73
+ api_client(ops: ops).pipeline_jobs(project_id, pipeline_id)
74
+ end
75
+
76
+ # Checks if tests count exceeds threshold in a pipeline
77
+ # @param [Integer] project_id the id of the project
78
+ # @param [Integer] pipeline_id the pipeline id
79
+ # @param [Boolean] ops true if ops instance
80
+ # @return [Boolean] true if count exceeds threshold defined in constant NUM_OF_TESTS_LIVE_ENV
81
+ def tests_exceed_threshold?(project_id:, pipeline_id:, ops: true)
82
+ api_client(ops: ops).pipeline_test_report(project_id, pipeline_id).total_count > NUM_OF_TESTS_LIVE_ENV
83
+ end
84
+
85
+ # Checks if a job is present in an array of jobs
86
+ # @param [Array] jobs
87
+ # @param [String] job_name name of the job
88
+ # @return [Boolean] true if job is present
89
+ def contains_job?(jobs, job_name:)
90
+ jobs.any? { |job| job["name"].include?(job_name) }
91
+ end
92
+
93
+ # Checks if a stage is present from a list of jobs
94
+ # @param [Array] jobs
95
+ # @param [String] stage_name name of the stage
96
+ # @return [Boolean] true if stage is present
97
+ def contains_stage?(jobs, stage_name)
98
+ jobs.any? { |job| job["stage"].include?(stage_name) }
99
+ end
100
+
101
+ # Checks if pipeline ran only the QA smoke tests
102
+ # @param [Array] jobs
103
+ # @param [Integer] project_id
104
+ # @param [Integer] pipeline_id
105
+ # @param [Boolean] ops true if ops instance
106
+ def smoke_run?(jobs:, project_id:, pipeline_id:, ops:)
107
+ contains_stage?(jobs, "sanity") &&
108
+ !tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
109
+ end
110
+
111
+ # Checks if pipeline ran full suite of qa tests
112
+ # @param [Array] jobs
113
+ # @param [Integer] project_id
114
+ # @param [Integer] pipeline_id
115
+ # @param [Boolean] ops true if ops instance
116
+ def full_run?(jobs:, project_id:, pipeline_id:, ops:)
117
+ if ops
118
+ (contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")) &&
119
+ tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
120
+ else
121
+ contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")
122
+ # Nightly pipeline does not execute full E2E suite if sanity fails so can't check tests count
123
+ end
124
+ end
125
+
126
+ # Combined logic to check if a pipeline was a sanity run for all pipeline types - ie., live environment
127
+ # and gitlab-qa-mirror pipelines
128
+ # @param [Array] pipeline_jobs
129
+ # @param [Object] pipeline
130
+ # @param [Boolean] ops
131
+ def sanity?(pipeline_jobs:, pipeline:, ops:)
132
+ return true if ops && smoke_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
133
+ pipeline_id: pipeline.id, ops: ops)
134
+
135
+ false if full_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
136
+ pipeline_id: pipeline.id, ops: ops)
137
+ end
138
+
139
+ # Constructs allure report url for each pipeline
140
+ # @param [String] pipeline_name
141
+ # @param [Integer] pipeline_id
142
+ # @param [Boolean] sanity
143
+ def allure_report(pipeline_name:, pipeline_id:, sanity:)
144
+ "https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}"\
145
+ "/master/#{pipeline_id}/index.html"
146
+ end
147
+
148
+ # Returns the GCP bucket name for different pipeline types
149
+ # @param [String] pipeline_name
150
+ # @param [Boolean] sanity
151
+ def allure_bucket_name(pipeline_name, sanity)
152
+ case pipeline_name
153
+ when "master"
154
+ "package-and-qa"
155
+ when "nightly"
156
+ pipeline_name
157
+ when "pre_prod"
158
+ "preprod-#{run_type(sanity)}"
159
+ else
160
+ "#{pipeline_name.sub('_', '-')}-#{run_type(sanity)}"
161
+ end
162
+ end
163
+
164
+ def run_type(sanity)
165
+ sanity == true ? "sanity" : "full"
166
+ end
167
+
168
+ # Returns table headers
169
+ # @return [Array]
170
+ def define_table_labels
171
+ name = add_color("Pipeline", :magenta)
172
+ pipeline_last_executed = add_color("Last executed at", :magenta)
173
+ url = add_color("Pipeline Url", :magenta)
174
+ report = add_color("Last report", :magenta)
175
+ result = add_color("Result", :magenta)
176
+ [name, pipeline_last_executed, url, report, result]
177
+ end
178
+
179
+ # Checks if pipeline is running on ops.gitlab.net or gitlab.com
180
+ # @param [String] url
181
+ def ops_pipeline?(url)
182
+ url.include?("ops.gitlab.net")
183
+ end
184
+
185
+ def notify_slack_job_name(pipeline_name, ops)
186
+ return "notify-slack-qa-fail" if ops
187
+
188
+ pipeline_name.to_s.include?("master") ? "notify_slack" : "notify-slack-fail"
189
+ end
190
+
191
+ # Returns child pipeline if it is master pipeline
192
+ # @param [Gitlab::ObjectifiedHash] pipeline
193
+ def pipeline_with_qa_tests(pipeline)
194
+ if pipeline.web_url.to_s.include?("gitlab-qa-mirror")
195
+ bridge_pipeline(pipeline.project_id, pipeline.id)
196
+ else
197
+ pipeline
198
+ end
199
+ end
200
+
201
+ # Returns query options for pipelines api call
202
+ # @param [Hash] details
203
+ # @param [Boolean] ops
204
+ def options(details, ops)
205
+ options = { order_by: "updated_at", scope: "finished",
206
+ updated_after: past_timestamp(details[:search_hours_ago]) }
207
+ options.merge(username: "gitlab-bot") if ops
208
+ options
209
+ end
210
+
211
+ def emoji_for_success_failure(status)
212
+ return add_color("✓", :green) if status.include?("success")
213
+
214
+ add_color("x", :red)
215
+ end
216
+
217
+ # @param [String] pipeline_name
218
+ # @param [Hash] details Pipeline environment details
219
+ # @return [Array] Array of last executed pipeline details
220
+ # rubocop:disable Metrics/PerceivedComplexity
221
+ def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity
222
+ ops = ops_pipeline?(details[:url])
223
+ options = options(details, ops)
224
+ # instance is ops.gitlab.net or gitlab.com
225
+ response = api_client(ops: ops).pipelines(project_id: details[:project_id],
226
+ options: options, auto_paginate: true)
227
+ return [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] if response.empty?
228
+
229
+ # Return empty data to the table if no matching pipelines were found from the query
230
+ response.each do |pipeline|
231
+ pipeline_to_scan = pipeline_with_qa_tests(pipeline) # Fetch child pipeline if it is master
232
+ next if pipeline_to_scan.nil?
233
+
234
+ pipeline_jobs = jobs(project_id: pipeline.project_id, pipeline_id: pipeline_to_scan.id, ops: ops)
235
+ next unless contains_job?(pipeline_jobs, job_name: notify_slack_job_name(pipeline_name, ops))
236
+
237
+ # Need to know if it is a sanity or a full run to construct allure report url
238
+ sanity = sanity?(pipeline_jobs: pipeline_jobs, pipeline: pipeline_to_scan,
239
+ ops: ops)
240
+
241
+ next if sanity.nil? # To filter out some "clean up" pipelines present in live environments
242
+
243
+ next if sanity && @options[:full_runs_only] # Filter out sanity runs if --full-runs-only option is passed
244
+
245
+ name = ops ? "#{pipeline_name}_#{run_type(sanity)}" : pipeline_name
246
+ pipeline_last_executed = pipeline_to_scan.updated_at
247
+ url = pipeline_to_scan.web_url
248
+ report = allure_report(pipeline_name: pipeline_name, pipeline_id: pipeline_to_scan.id, sanity: sanity)
249
+ result = emoji_for_success_failure(pipeline_to_scan.status)
250
+ return [name, pipeline_last_executed, url, report, result]
251
+ end
252
+
253
+ [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] # Parsed through all of the response and
254
+ # no matching pipelines found
255
+ end
256
+ # rubocop:enable Metrics/PerceivedComplexity
257
+ end
258
+ end
259
+ end
260
+ 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
data/lib/dri/report.rb CHANGED
@@ -95,7 +95,7 @@ module Dri
95
95
  end
96
96
 
97
97
  unless pipeline_link.empty?
98
- pipeline_link_sanitized = pipeline_link.join.strip.chop
98
+ pipeline_link_sanitized = pipeline_link.join.strip
99
99
  pipeline_markdown = "[#{pipeline_markdown}](#{pipeline_link_sanitized})"
100
100
  end
101
101
 
@@ -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.0"
4
+ VERSION = "0.6.1"
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.0
4
+ version: 0.6.1
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-06-09 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,7 +354,7 @@ files:
337
354
  - lib/dri/gitlab/issues.rb
338
355
  - lib/dri/refinements/truncate.rb
339
356
  - lib/dri/report.rb
340
- - lib/dri/templates/incidents/.gitkeep
357
+ - lib/dri/utils/constants.rb
341
358
  - lib/dri/utils/feature_flag_consts.rb
342
359
  - lib/dri/utils/markdown_lists.rb
343
360
  - lib/dri/utils/table.rb
@@ -1 +0,0 @@
1
- #