dri 0.5.1 → 0.7.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: e6ee21fc2a0243e0850f7bab281b0611021807a1a4559e89433741b1faed40f2
4
+ data.tar.gz: 5b97afcdbc1e1e9307b084eab6e2afddbff94556cc102f2b174a0dedc3f51c33
5
5
  SHA512:
6
- metadata.gz: 6c2d592d0906e3764ec1ab2fe298a42a1798be1d58dd216261ed2a88227cfe007a97a175ae199f5ee4f4db16f7efe2dd6f99521e8febc740e191b61c0f2ef8cc
7
- data.tar.gz: 1106b7fc11be59d38c72f471f8a5148eb4e724a81665b4382185da5c22a4e554fc7e1b1cc274a980b9ceeb6071f7fa5da0397e3657655132a6d21f2a31cf9712
6
+ metadata.gz: f5b54fee2e4f0d99d0d013d0c9a211306eda149dabb6c0cedf05ec0c5a92274dd4038377fc270b6185782ba2dfdbe5c2be6028136c77a4e77dceb21989a82894
7
+ data.tar.gz: c2d5056697c04ea976e00db46f183cd716091e3681687ad369c6dc3c11dab7ecd5592022460948a19ce779da7b114c2c90c4ba3c712a0bfe6256524cf6859934
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.7.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
 
@@ -137,6 +146,7 @@ $ dri publish report
137
146
  ```
138
147
 
139
148
  Publishes a handover report on the latest triage issue, in the [pipeline-triage](https://gitlab.com/gitlab-org/quality/pipeline-triage) project.
149
+ The report includes automatically both triaged failures and incidents.
140
150
 
141
151
  **Options**
142
152
 
@@ -191,7 +201,17 @@ $ dri incidents
191
201
 
192
202
  Have a quick look at currently active/mitigated incidents on GitLab services.
193
203
 
194
- #### 7. version
204
+ #### 7. analyze
205
+
206
+ ```shell
207
+ $ dri analyze stacktraces
208
+ ```
209
+ Searches through any open test failure issues and publishes a report that identifies
210
+ issues that have similar stack traces.
211
+ This may be useful to identify situations where a common test failure is presenting
212
+ itself across multiple individual test cases, over a period of time.
213
+
214
+ #### 8. version
195
215
 
196
216
  ```shell
197
217
  $ 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,29 @@
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
+
20
+ def initialize(config, ops = false)
19
21
  @token = config.read.dig("settings", "token")
22
+ @ops_token = config.read.dig("settings", "ops_token")
23
+ if @token.nil? || @ops_token.nil?
24
+ raise TokenNotProvidedError, "Gitlab API client cannot be initialized without both access tokens. " \
25
+ "Run `dri init` again or `dri profile --edit` to add an ops_token entry."
26
+ end
27
+
28
+ @ops_instance = ops
20
29
  end
21
30
 
22
31
  # Fetch triaged failures
@@ -34,12 +43,26 @@ module Dri
34
43
  )
35
44
  end
36
45
 
46
+ # Fetch triaged incidents
47
+ #
48
+ # @param [String] emoji
49
+ def fetch_triaged_incidents(emoji:)
50
+ gitlab.issues(
51
+ INFRA_TEAM_PROD_PROJECT_ID,
52
+ order_by: "updated_at",
53
+ my_reaction_emoji: emoji,
54
+ state: "all",
55
+ labels: "incident"
56
+ )
57
+ end
58
+
37
59
  # Fetch award emojis
38
60
  #
39
61
  # @param [Integer] issue_iid
62
+ # @param [Integer] project_id
40
63
  # @return [Array<Gitlab::ObjectifiedHash>]
41
- def fetch_awarded_emojis(issue_iid)
42
- gitlab.award_emojis(GITLAB_PROJECT_ID, issue_iid, "issue")
64
+ def fetch_awarded_emojis(issue_iid, project_id: GITLAB_PROJECT_ID)
65
+ gitlab.award_emojis(project_id, issue_iid, "issue")
43
66
  end
44
67
 
45
68
  # Fetch failing testcases
@@ -57,6 +80,18 @@ module Dri
57
80
  ).auto_paginate
58
81
  end
59
82
 
83
+ # Fetch issues related to failing test cases
84
+ #
85
+ # @return [Array<Gitlab::ObjectifiedHash>]
86
+ def fetch_test_failure_issues(labels: 'failure::new')
87
+ gitlab.issues(
88
+ GITLAB_PROJECT_ID,
89
+ labels: labels,
90
+ state: 'opened',
91
+ scope: "all"
92
+ ).auto_paginate
93
+ end
94
+
60
95
  # Fetch related issue mrs
61
96
  #
62
97
  # @param [Integer] issue_iid
@@ -124,10 +159,11 @@ module Dri
124
159
  #
125
160
  # @param [Integer] issue_iid
126
161
  # @param [Integer] emoji_id
162
+ # @param [Integer] project_id
127
163
  # @return [Gitlab::ObjectifiedHash]
128
- def delete_award_emoji(issue_iid, emoji_id)
164
+ def delete_award_emoji(issue_iid, emoji_id:, project_id: GITLAB_PROJECT_ID)
129
165
  gitlab.delete_award_emoji(
130
- GITLAB_PROJECT_ID,
166
+ project_id,
131
167
  issue_iid,
132
168
  "issue",
133
169
  emoji_id
@@ -149,18 +185,73 @@ module Dri
149
185
  gitlab.issues(INFRA_TEAM_PROD_PROJECT_ID, order_by: "updated_at", state: "opened", labels: "incident")
150
186
  end
151
187
 
188
+ # Fetch pipelines
189
+ #
190
+ # @param [Integer] project_id
191
+ # @return [Array<Gitlab::ObjectifiedHash>]
192
+ def pipelines(project_id:, options:, auto_paginate: false)
193
+ if auto_paginate
194
+ gitlab.pipelines(project_id, options).auto_paginate
195
+ else
196
+ gitlab.pipelines(project_id, options)
197
+ end
198
+ end
199
+
200
+ # Fetch single pipeline
201
+ #
202
+ # @param [Integer] project_id
203
+ # @param [Integer] pipeline_id
204
+ # @return [<Gitlab::ObjectifiedHash>]
205
+ def pipeline(project_id, pipeline_id)
206
+ gitlab.pipeline(project_id, pipeline_id)
207
+ end
208
+
209
+ # Fetch test report from a pipeline
210
+ #
211
+ # @param [Integer] project_id
212
+ # @param [Integer] pipeline_id
213
+ # @return [<Gitlab::ObjectifiedHash>]
214
+ def pipeline_test_report(project_id, pipeline_id)
215
+ gitlab.pipeline_test_report(project_id, pipeline_id)
216
+ end
217
+
218
+ # Fetch pipeline bridges/downstream pipelines
219
+ #
220
+ # @param [Integer] project_id
221
+ # @param [Integer] pipeline_id
222
+ # @return [Array<Gitlab::ObjectifiedHash>]
223
+ def pipeline_bridges(project_id, pipeline_id, options = {})
224
+ gitlab.pipeline_bridges(project_id, pipeline_id, options).auto_paginate
225
+ end
226
+
227
+ # Fetch jobs from a pipeline
228
+ #
229
+ # @param [Integer] project_id
230
+ # @param [Integer] pipeline_id
231
+ # @return [Array<Gitlab::ObjectifiedHash>]
232
+ def pipeline_jobs(project_id, pipeline_id, options = {})
233
+ gitlab.pipeline_jobs(project_id, pipeline_id, options).auto_paginate
234
+ end
235
+
152
236
  private
153
237
 
154
- attr_reader :token
238
+ attr_reader :token, :ops_token
155
239
 
156
240
  # Gitlab client
157
241
  #
158
242
  # @return [Gitlab::Client]
159
243
  def gitlab
160
- @gitlab ||= Gitlab.client(
161
- endpoint: API_URL,
162
- private_token: token
163
- )
244
+ if @ops_instance
245
+ @ops_client ||= Gitlab.client(
246
+ endpoint: OPS_API_URL,
247
+ private_token: ops_token
248
+ )
249
+ else
250
+ @gitlab_client ||= Gitlab.client(
251
+ endpoint: API_URL,
252
+ private_token: token
253
+ )
254
+ end
164
255
  end
165
256
  end
166
257
  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,10 +52,18 @@ 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
58
62
 
63
+ def handover_report_path
64
+ @handover_report_path ||= profile["settings"]["handover_report_path"]
65
+ end
66
+
59
67
  def verify_config_exists
60
68
  return if config.exist?
61
69
 
@@ -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,272 @@
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.find { |it| it.name == "e2e:package-and-test" }&.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
+ # Master pipeline run
140
+ #
141
+ # @param [String] pipeline_name
142
+ # @return [Boolean]
143
+ def master?(pipeline_name)
144
+ pipeline_name == "master"
145
+ end
146
+
147
+ # Constructs allure report url for each pipeline
148
+ # @param [String] pipeline_name
149
+ # @param [Integer] pipeline_id
150
+ # @param [Boolean] sanity
151
+ def allure_report(pipeline_name:, pipeline_id:, sanity:)
152
+ "https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}"\
153
+ "/master/#{pipeline_id}/index.html"
154
+ end
155
+
156
+ # Returns the GCP bucket name for different pipeline types
157
+ # @param [String] pipeline_name
158
+ # @param [Boolean] sanity
159
+ def allure_bucket_name(pipeline_name, sanity)
160
+ case pipeline_name
161
+ when "master"
162
+ "e2e-package-and-test"
163
+ when "nightly"
164
+ pipeline_name
165
+ when "pre_prod"
166
+ "preprod-#{run_type(sanity)}"
167
+ else
168
+ "#{pipeline_name.sub('_', '-')}-#{run_type(sanity)}"
169
+ end
170
+ end
171
+
172
+ def run_type(sanity)
173
+ sanity == true ? "sanity" : "full"
174
+ end
175
+
176
+ # Returns table headers
177
+ # @return [Array]
178
+ def define_table_labels
179
+ name = add_color("Pipeline", :magenta)
180
+ pipeline_last_executed = add_color("Last executed at", :magenta)
181
+ url = add_color("Pipeline Url", :magenta)
182
+ report = add_color("Last report", :magenta)
183
+ result = add_color("Result", :magenta)
184
+ [name, pipeline_last_executed, url, report, result]
185
+ end
186
+
187
+ # Checks if pipeline is running on ops.gitlab.net or gitlab.com
188
+ # @param [String] url
189
+ def ops_pipeline?(url)
190
+ url.include?("ops.gitlab.net")
191
+ end
192
+
193
+ # Slack notification job name
194
+ #
195
+ # @param [Boolean] ops
196
+ # @return [String]
197
+ def notify_job_name(ops)
198
+ ops ? "notify-slack-qa-fail" : "notify-slack-fail"
199
+ end
200
+
201
+ # Returns child pipeline if it is master pipeline
202
+ # @param [String] pipeline_name
203
+ # @param [Gitlab::ObjectifiedHash] pipeline
204
+ def pipeline_with_qa_tests(pipeline_name, pipeline)
205
+ if master?(pipeline_name)
206
+ bridge_pipeline(pipeline.project_id, pipeline.id)
207
+ else
208
+ pipeline
209
+ end
210
+ end
211
+
212
+ # Returns query options for pipelines api call
213
+ # @param [Hash] details
214
+ # @param [Boolean] ops
215
+ def options(details, ops)
216
+ options = {
217
+ order_by: "updated_at",
218
+ scope: "finished",
219
+ ref: "master",
220
+ updated_after: past_timestamp(details[:search_hours_ago])
221
+ }
222
+ options.merge(username: "gitlab-bot") if ops || master?(details[:name])
223
+ options
224
+ end
225
+
226
+ def emoji_for_success_failure(status)
227
+ return add_color("✓", :green) if status.include?("success")
228
+
229
+ add_color("x", :red)
230
+ end
231
+
232
+ # @param [String] pipeline_name
233
+ # @param [Hash] details Pipeline environment details
234
+ # @return [Array] Array of last executed pipeline details
235
+ # rubocop:disable Metrics/PerceivedComplexity
236
+ def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
237
+ ops = ops_pipeline?(details[:url])
238
+ options = options(details, ops)
239
+ # instance is ops.gitlab.net or gitlab.com
240
+ response = api_client(ops: ops).pipelines(project_id: details[:project_id],
241
+ options: options, auto_paginate: true)
242
+ return [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] if response.empty?
243
+
244
+ # Return empty data to the table if no matching pipelines were found from the query
245
+ response.each do |pipeline|
246
+ pipeline_to_scan = pipeline_with_qa_tests(pipeline_name, pipeline) # Fetch child pipeline if it is master
247
+ next if pipeline_to_scan.nil?
248
+
249
+ pipeline_jobs = jobs(project_id: pipeline.project_id, pipeline_id: pipeline_to_scan.id, ops: ops)
250
+ next unless master?(pipeline_name) || contains_job?(pipeline_jobs, job_name: notify_job_name(ops))
251
+
252
+ # Need to know if it is a sanity or a full run to construct allure report url
253
+ sanity = sanity?(pipeline_jobs: pipeline_jobs, pipeline: pipeline_to_scan, ops: ops)
254
+ next if sanity.nil? # To filter out some "clean up" pipelines present in live environments
255
+ next if sanity && @options[:full_runs_only] # Filter out sanity runs if --full-runs-only option is passed
256
+
257
+ name = ops ? "#{pipeline_name}_#{run_type(sanity)}" : pipeline_name
258
+ pipeline_last_executed = pipeline_to_scan.updated_at
259
+ url = pipeline_to_scan.web_url
260
+ report = allure_report(pipeline_name: pipeline_name, pipeline_id: pipeline_to_scan.id, sanity: sanity)
261
+ result = emoji_for_success_failure(pipeline_to_scan.status)
262
+ return [name, pipeline_last_executed, url, report, result]
263
+ end
264
+
265
+ [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] # Parsed through all of the response and
266
+ # no matching pipelines found
267
+ end
268
+ # rubocop:enable Metrics/PerceivedComplexity
269
+ end
270
+ end
271
+ end
272
+ end
@@ -19,7 +19,7 @@ module Dri
19
19
  end
20
20
  end
21
21
 
22
- desc 'triaged', 'Command description...'
22
+ desc 'triaged', 'Display triaged failures'
23
23
  method_option :help, aliases: '-h', type: :boolean,
24
24
  desc: 'Display usage information'
25
25
  def triaged(*)
@@ -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,18 +31,23 @@ 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?")
37
+ @handover_report_path = prompt.ask("Please provide a path for handover report",
38
+ default: "#{Dir.pwd}/handover_reports")
36
39
 
37
- if (@emoji || @token || @username).nil?
38
- logger.error "Please provide a username, token, timezone and emoji used for triage."
40
+ if (@emoji || @token || @username || @ops_token).nil?
41
+ logger.error "Please provide a username, gitlab token, ops token, timezone and emoji used for triage."
39
42
  exit 1
40
43
  end
41
44
 
42
45
  config.set(:settings, :user, value: @username)
43
46
  config.set(:settings, :token, value: @token)
47
+ config.set(:settings, :ops_token, value: @ops_token)
44
48
  config.set(:settings, :timezone, value: @timezone)
45
49
  config.set(:settings, :emoji, value: @emoji)
50
+ config.set(:settings, :handover_report_path, value: @handover_report_path)
46
51
  config.write(force: true)
47
52
 
48
53
  logger.success "✅ We're ready to go 🚀"
@@ -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
@@ -28,20 +28,21 @@ module Dri
28
28
  verify_config_exists
29
29
  report = Dri::Report.new(config)
30
30
 
31
- logger.info "Fetching triaged failures with award emoji #{emoji}..."
31
+ logger.info "Fetching triaged failures and incidents with award emoji #{emoji}..."
32
32
 
33
33
  spinner.start
34
34
 
35
35
  issues = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
36
+ incidents = api_client.fetch_triaged_incidents(emoji: emoji)
36
37
 
37
38
  spinner.stop
38
39
 
39
- if issues.empty?
40
- logger.warn "Found no issues associated with \"#{emoji}\" emoji. Will exit. Bye 👋"
40
+ if issues.empty? && incidents.empty?
41
+ logger.warn "Found no issues nor incidents associated with \"#{emoji}\" emoji. Will exit. Bye 👋"
41
42
  exit 1
42
43
  end
43
44
 
44
- logger.info 'Assembling the failures report... '
45
+ logger.info 'Assembling the handover report... '
45
46
  # sets each failure on the table
46
47
  action_options = [
47
48
  'pinged SET',
@@ -71,18 +72,38 @@ module Dri
71
72
  report.add_failure(issue, actions)
72
73
  end
73
74
 
75
+ unless incidents.empty?
76
+ incidents.each do |issue|
77
+ report.add_incident(issue)
78
+ end
79
+ end
80
+
74
81
  if @options[:format] == 'list'
75
82
  # generates markdown list with failures
76
- format_style = Utils::MarkdownLists.make_list(report.labels, report.failures) unless report.failures.empty?
83
+ unless report.failures.empty?
84
+ failure_report = Utils::MarkdownLists.make_list(report.labels, report.failures)
85
+ end
86
+ # generates markdown list with incidents
87
+ unless report.incidents.empty?
88
+ incidents_report = Utils::MarkdownLists.make_list(report.labels_incidents, report.incidents)
89
+ end
77
90
  else
78
91
  # generates markdown table with rows as failures
79
92
  unless report.failures.empty?
80
- format_style = MarkdownTables.make_table(
93
+ failure_report = MarkdownTables.make_table(
81
94
  report.labels,
82
95
  report.failures,
83
96
  is_rows: true, align: %w[l l l l l]
84
97
  )
85
98
  end
99
+ # generates markdown table with rows as incidents
100
+ unless report.incidents.empty?
101
+ incidents_report = MarkdownTables.make_table(
102
+ report.labels_incidents,
103
+ report.incidents,
104
+ is_rows: true, align: %w[l l l l]
105
+ )
106
+ end
86
107
  end
87
108
 
88
109
  spinner.stop
@@ -131,7 +152,7 @@ module Dri
131
152
  end
132
153
 
133
154
  report.set_header(timezone, username)
134
- note = "#{report.header}\n\n#{format_style}"
155
+ note = "#{report.header}\n\n#{failure_report}\n\n#{incidents_report}"
135
156
 
136
157
  note += feature_flag_note if @options[:feature_flags]
137
158
 
@@ -141,8 +162,8 @@ module Dri
141
162
 
142
163
  spinner.start
143
164
 
144
- FileUtils.mkdir_p("#{Dir.pwd}/handover_reports")
145
- report_path = "handover_reports/report-#{@date}-#{@time}.md"
165
+ FileUtils.mkdir_p(handover_report_path)
166
+ report_path = "#{handover_report_path.chomp('/')}/report-#{@date}-#{@time}.md"
146
167
 
147
168
  File.open(report_path, 'a') do |out_file|
148
169
  out_file.puts note
@@ -20,25 +20,35 @@ module Dri
20
20
  exit 0
21
21
  end
22
22
 
23
- logger.info "Removing #{emoji} from issues..."
23
+ logger.info "Removing #{emoji} emoji from issues..."
24
24
 
25
25
  spinner.start
26
26
 
27
- issues_with_award_emoji = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
27
+ failures_with_award_emoji = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
28
+ incidents_with_award_emoji = api_client.fetch_triaged_incidents(emoji: emoji)
29
+
30
+ issues_with_award_emoji = failures_with_award_emoji + incidents_with_award_emoji
28
31
 
29
32
  spinner.stop
30
33
 
34
+ cleared_issues_counter = 0
31
35
  issues_with_award_emoji.each do |issue|
32
- logger.info "Removing #{emoji} from #{issue.web_url}..."
33
-
34
- response = api_client.fetch_awarded_emojis(issue.iid)
36
+ logger.info "Removing #{emoji} emoji from #{issue.web_url}"
35
37
 
38
+ response = api_client.fetch_awarded_emojis(issue.iid, project_id: issue.project_id)
36
39
  emoji_found = response.find { |e| e.name == emoji && e.to_h.dig('user', 'username') == username }
37
40
 
38
- api_client.delete_award_emoji(issue.iid, emoji_found.id) unless emoji_found.nil?
41
+ if emoji_found.nil?
42
+ logger.error "Emoji #{add_color(emoji, :red)} not found for username: #{add_color(username, :red)}"
43
+ next
44
+ else
45
+ api_client.delete_award_emoji(issue.iid, emoji_id: emoji_found.id, project_id: issue.project_id)
46
+ cleared_issues_counter += 1
47
+ end
39
48
  end
49
+
40
50
  output.puts "Done! ✅"
41
- logger.success "Removed #{emoji} from #{issues_with_award_emoji.size} issue(s)."
51
+ logger.success "Removed #{emoji} from #{cleared_issues_counter} issue(s)."
42
52
  end
43
53
  end
44
54
  end
@@ -12,7 +12,7 @@ module Dri
12
12
  end
13
13
 
14
14
  def execute(input: $stdin, output: $stdout)
15
- FileUtils.rm_rf("#{Dir.pwd}/handover_reports")
15
+ FileUtils.rm_rf(handover_report_path) if prompt.yes?("Remove everything in #{handover_report_path}?")
16
16
  end
17
17
  end
18
18
  end
data/lib/dri/report.rb CHANGED
@@ -4,11 +4,13 @@ module Dri
4
4
  class Report # rubocop:disable Metrics/ClassLength
5
5
  using Refinements
6
6
 
7
- attr_reader :header, :failures, :labels
7
+ attr_reader :header, :failures, :labels, :labels_incidents, :incidents
8
8
 
9
9
  def initialize(config)
10
10
  @labels = ['Title', 'Issue', 'Pipelines', 'Stack Trace', 'Actions']
11
+ @labels_incidents = %w[Incident Service Status URL]
11
12
  @failures = []
13
+ @incidents = []
12
14
  @date = Date.today
13
15
  @today = Date.today.strftime("%Y-%m-%d")
14
16
  @weekday = Date.today.strftime("%A")
@@ -21,6 +23,21 @@ module Dri
21
23
  @header = "# #{timezone}, #{@weekday} - #{@date}\n posted by: @#{username}"
22
24
  end
23
25
 
26
+ def add_incident(incident)
27
+ title = incident["title"]
28
+ url = incident["web_url"]
29
+ labels = incident["labels"]
30
+ status = 'N/A'
31
+ service = 'N/A'
32
+
33
+ labels.each do |label|
34
+ status = label.gsub!('Incident::', ' ').to_s if label.include? "Incident::"
35
+ service = label.gsub!('Service::', ' ').to_s if label.include? "Service::"
36
+ end
37
+
38
+ @incidents << [title, service, status, url]
39
+ end
40
+
24
41
  def add_failure(failure, actions_opts = [])
25
42
  iid = failure["iid"]
26
43
  title = format_title(failure["title"])
@@ -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",
53
+ project_id: "278964",
54
+ search_hours_ago: 3
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.7.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.7.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-09-12 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