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 +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 +23 -3
- data/dri.gemspec +2 -1
- data/lib/dri/api_client.rb +111 -20
- data/lib/dri/cli.rb +3 -0
- data/lib/dri/command.rb +10 -2
- data/lib/dri/commands/analyze/stack_traces.rb +106 -0
- data/lib/dri/commands/analyze.rb +20 -0
- data/lib/dri/commands/fetch/pipelines.rb +272 -0
- data/lib/dri/commands/fetch.rb +13 -1
- data/lib/dri/commands/init.rb +7 -2
- data/lib/dri/commands/profile.rb +1 -0
- data/lib/dri/commands/publish/report.rb +30 -9
- data/lib/dri/commands/rm/emoji.rb +17 -7
- data/lib/dri/commands/rm/reports.rb +1 -1
- data/lib/dri/report.rb +18 -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 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6ee21fc2a0243e0850f7bab281b0611021807a1a4559e89433741b1faed40f2
|
|
4
|
+
data.tar.gz: 5b97afcdbc1e1e9307b084eab6e2afddbff94556cc102f2b174a0dedc3f51c33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5b54fee2e4f0d99d0d013d0c9a211306eda149dabb6c0cedf05ec0c5a92274dd4038377fc270b6185782ba2dfdbe5c2be6028136c77a4e77dceb21989a82894
|
|
7
|
+
data.tar.gz: c2d5056697c04ea976e00db46f183cd716091e3681687ad369c6dc3c11dab7ecd5592022460948a19ce779da7b114c2c90c4ba3c712a0bfe6256524cf6859934
|
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.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.
|
|
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
|
|
|
@@ -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.
|
|
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'
|
data/lib/dri/api_client.rb
CHANGED
|
@@ -3,20 +3,29 @@
|
|
|
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
|
+
|
|
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(
|
|
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
|
-
|
|
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
|
-
@
|
|
161
|
-
|
|
162
|
-
|
|
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
|
data/lib/dri/commands/fetch.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Dri
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
desc 'triaged', '
|
|
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
|
data/lib/dri/commands/init.rb
CHANGED
|
@@ -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 🚀"
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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#{
|
|
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(
|
|
145
|
-
report_path = "
|
|
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
|
-
|
|
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
|
-
|
|
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 #{
|
|
51
|
+
logger.success "Removed #{emoji} from #{cleared_issues_counter} issue(s)."
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
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
|
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.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-
|
|
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
|