dri 0.5.0 → 0.6.1
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 +4 -4
- data/.gitignore +1 -0
- data/.gitlab-ci.yml +15 -12
- data/.tool-versions +1 -1
- data/Gemfile.lock +19 -5
- data/README.md +22 -3
- data/dri.gemspec +2 -1
- data/lib/dri/api_client.rb +91 -16
- data/lib/dri/cli.rb +3 -0
- data/lib/dri/command.rb +6 -2
- data/lib/dri/commands/analyze/stack_traces.rb +106 -0
- data/lib/dri/commands/analyze.rb +20 -0
- data/lib/dri/commands/fetch/failures.rb +30 -26
- data/lib/dri/commands/fetch/pipelines.rb +260 -0
- data/lib/dri/commands/fetch.rb +12 -0
- data/lib/dri/commands/init.rb +4 -2
- data/lib/dri/commands/profile.rb +1 -0
- data/lib/dri/report.rb +1 -1
- data/lib/dri/utils/constants.rb +59 -0
- data/lib/dri/utils/table.rb +2 -2
- data/lib/dri/version.rb +1 -1
- metadata +24 -7
- data/lib/dri/templates/incidents/.gitkeep +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 990d9ed4c79b5b7c7ff2e5535dba71599a3189d4c742b918f98d3063a6e15c9f
|
|
4
|
+
data.tar.gz: 15e48f44496a55d5d13185409a91d9ddd525d127fd8dbe73265dc176c6d14634
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22a9838a3821c90cb24346f6f98b6158e2a71ec9bd0c4e7b5d9e447a75b676202be1d365e1603d7d12a8441f6b5d448f26337c33163b60f346819933da24fea3
|
|
7
|
+
data.tar.gz: 6b117a44436b2c4c064c3b19b22deb454a42c8d6d460492af7813e72299600d50ad2cef612e5e1f2a84566588f560d31c82da3c5c92b35f1d8749ea445e9664e
|
data/.gitignore
CHANGED
data/.gitlab-ci.yml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
.job_base:
|
|
2
|
-
image: ruby:2.
|
|
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.
|
|
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.
|
|
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
|
+
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.
|
|
37
|
-
httparty (~> 0.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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'
|
data/lib/dri/api_client.rb
CHANGED
|
@@ -3,20 +3,28 @@
|
|
|
3
3
|
require "httparty"
|
|
4
4
|
require "json"
|
|
5
5
|
require "tty-config"
|
|
6
|
-
require
|
|
7
|
-
require
|
|
6
|
+
require "cgi"
|
|
7
|
+
require "gitlab"
|
|
8
8
|
|
|
9
9
|
module Dri
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
@
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
data/lib/dri/commands/fetch.rb
CHANGED
|
@@ -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
|
data/lib/dri/commands/init.rb
CHANGED
|
@@ -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)
|
data/lib/dri/commands/profile.rb
CHANGED
|
@@ -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
|
@@ -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
|
data/lib/dri/utils/table.rb
CHANGED
|
@@ -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
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.
|
|
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-
|
|
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/
|
|
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
|
-
#
|