dri 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb71a1d13a00cb33c436608e3c34e40a4202505bad341d31d5d85803c36495d4
4
- data.tar.gz: 73648bafa41eac28824435b62a41ead17494c144bbd99c6af52062e616379f2a
3
+ metadata.gz: 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