dri 0.6.1 → 0.8.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: 990d9ed4c79b5b7c7ff2e5535dba71599a3189d4c742b918f98d3063a6e15c9f
4
- data.tar.gz: 15e48f44496a55d5d13185409a91d9ddd525d127fd8dbe73265dc176c6d14634
3
+ metadata.gz: 3b4a86b6e658b2426bfce682d92f34f1441a9a1812f0cc805088b8c250991d4d
4
+ data.tar.gz: 3cf2b62c9aa5b1ef8de5cd51f8fdd927fa5335c9a1299b4947ab162b8afe2526
5
5
  SHA512:
6
- metadata.gz: 22a9838a3821c90cb24346f6f98b6158e2a71ec9bd0c4e7b5d9e447a75b676202be1d365e1603d7d12a8441f6b5d448f26337c33163b60f346819933da24fea3
7
- data.tar.gz: 6b117a44436b2c4c064c3b19b22deb454a42c8d6d460492af7813e72299600d50ad2cef612e5e1f2a84566588f560d31c82da3c5c92b35f1d8749ea445e9664e
6
+ metadata.gz: 4d79cdc2db7e1f959fee5b80988516677455a1d18e151a300923b2d19a58f0bb4fc4342db5e3785c34b5ef4475655b37cb10ad8850e488429ab12cfd8ad03fb8
7
+ data.tar.gz: 5120e934a2e8a45a51b4328bf5a1ae3af1a55b19e42023e538f65c958bfad0a8810f89844b7f2ef40608d0c7233d64f652ba9f101cff37f203a900d940084771
data/.gitlab-ci.yml CHANGED
@@ -15,12 +15,12 @@
15
15
  - vendor/bundle
16
16
  policy: pull
17
17
  rules:
18
- - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
18
+ - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
19
19
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
20
20
 
21
21
  include:
22
22
  - project: gitlab-org/quality/pipeline-common
23
- ref: 0.15.1
23
+ ref: 1.2.3
24
24
  file:
25
25
  - /ci/gem-release.yml
26
26
  - /ci/ref-update.gitlab-ci.yml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dri (0.6.1)
4
+ dri (0.8.0)
5
5
  amatch (~> 0.4.1)
6
6
  gitlab (~> 4.18)
7
7
  httparty (~> 0.20.0)
data/README.md CHANGED
@@ -146,6 +146,7 @@ $ dri publish report
146
146
  ```
147
147
 
148
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.
149
150
 
150
151
  **Options**
151
152
 
@@ -155,12 +156,13 @@ $ dri publish report --format=table # formats the report in a table (default)
155
156
  $ dri publish report --dry-run # the report is only generated locally
156
157
  $ dri publish report --actions # activate the actions prompt for each failure
157
158
  $ dri publish report --feature-flags # includes a summary of the feature flag changes on each environment
159
+ $ dri publish report --update # update the report note if has already been posted
158
160
  ```
159
161
 
160
162
  **Note:** These options above can be combined like:
161
163
 
162
164
  ```shell
163
- $ dri publish report --format=list --dry-run --actions
165
+ $ dri publish report --format=list --dry-run --actions --update
164
166
  ```
165
167
 
166
168
  **Actions**
@@ -6,16 +6,17 @@ require "tty-config"
6
6
  require "cgi"
7
7
  require "gitlab"
8
8
 
9
+ require_relative './utils/constants'
10
+
9
11
  module Dri
10
12
  TokenNotProvidedError = Class.new(StandardError)
11
13
  class ApiClient # rubocop:disable Metrics/ClassLength
14
+ include Dri::Utils::Constants::ProjectIDs
15
+ include Dri::Utils::Constants::Triage::Labels
16
+
12
17
  API_URL = "https://gitlab.com/api/v4"
13
18
  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
+
19
20
  def initialize(config, ops = false)
20
21
  @token = config.read.dig("settings", "token")
21
22
  @ops_token = config.read.dig("settings", "ops_token")
@@ -27,14 +28,31 @@ module Dri
27
28
  @ops_instance = ops
28
29
  end
29
30
 
30
- # Fetch triaged failures
31
+ # Fetch all triaged failures
31
32
  #
32
33
  # @param [String] emoji
33
34
  # @param [String] state
34
- # @return [Gitlab::ObjectifiedHash]
35
- def fetch_triaged_failures(emoji:, state:)
35
+ # @return [Array<Gitlab::ObjectifiedHash>]
36
+ def fetch_all_triaged_failures(emoji:, state:)
37
+ project_ids = [GITLAB_PROJECT_ID, CUSTOMERSDOT_PROJECT_ID]
38
+ failures = []
39
+
40
+ project_ids.each do |project_id|
41
+ failures += fetch_triaged_failures(project_id: project_id, emoji: emoji, state: state)
42
+ end
43
+
44
+ failures
45
+ end
46
+
47
+ # Fetch triaged failures for a given project
48
+ #
49
+ # @param [Integer] project_id
50
+ # @param [String] emoji
51
+ # @param [String] state
52
+ # @return [Array<Gitlab::ObjectifiedHash>]
53
+ def fetch_triaged_failures(project_id:, emoji:, state:)
36
54
  gitlab.issues(
37
- GITLAB_PROJECT_ID,
55
+ project_id,
38
56
  order_by: "updated_at",
39
57
  my_reaction_emoji: emoji,
40
58
  scope: "all",
@@ -42,12 +60,26 @@ module Dri
42
60
  )
43
61
  end
44
62
 
63
+ # Fetch triaged incidents
64
+ #
65
+ # @param [String] emoji
66
+ def fetch_triaged_incidents(emoji:)
67
+ gitlab.issues(
68
+ INFRA_TEAM_PROD_PROJECT_ID,
69
+ order_by: "updated_at",
70
+ my_reaction_emoji: emoji,
71
+ state: "all",
72
+ labels: "incident"
73
+ )
74
+ end
75
+
45
76
  # Fetch award emojis
46
77
  #
47
78
  # @param [Integer] issue_iid
79
+ # @param [Integer] project_id
48
80
  # @return [Array<Gitlab::ObjectifiedHash>]
49
- def fetch_awarded_emojis(issue_iid)
50
- gitlab.award_emojis(GITLAB_PROJECT_ID, issue_iid, "issue")
81
+ def fetch_awarded_emojis(issue_iid, project_id:)
82
+ gitlab.award_emojis(project_id, issue_iid, "issue")
51
83
  end
52
84
 
53
85
  # Fetch failing testcases
@@ -61,14 +93,14 @@ module Dri
61
93
  labels: "#{pipeline}::failed",
62
94
  state: state,
63
95
  scope: "all",
64
- "not[labels]": "quarantine"
96
+ "not[labels]": QUARANTINE
65
97
  ).auto_paginate
66
98
  end
67
99
 
68
100
  # Fetch issues related to failing test cases
69
101
  #
70
102
  # @return [Array<Gitlab::ObjectifiedHash>]
71
- def fetch_test_failure_issues(labels: 'failure::new')
103
+ def fetch_test_failure_issues(labels: FAILURE_NEW)
72
104
  gitlab.issues(
73
105
  GITLAB_PROJECT_ID,
74
106
  labels: labels,
@@ -79,10 +111,11 @@ module Dri
79
111
 
80
112
  # Fetch related issue mrs
81
113
  #
114
+ # @param [Integer] project_id
82
115
  # @param [Integer] issue_iid
83
116
  # @return [Array<Gitlab::ObjectifiedHash>]
84
- def fetch_related_mrs(issue_iid)
85
- gitlab.related_issue_merge_requests(GITLAB_PROJECT_ID, issue_iid)
117
+ def fetch_related_mrs(project_id, issue_iid)
118
+ gitlab.related_issue_merge_requests(project_id, issue_iid)
86
119
  end
87
120
 
88
121
  # Fetch MRs
@@ -115,15 +148,42 @@ module Dri
115
148
  gitlab.create_issue_note(TRIAGE_PROJECT_ID, iid, body)
116
149
  end
117
150
 
118
- # Fetch new failures
151
+ # Update triage report note
152
+ #
153
+ # @param [Integer] iid
154
+ # @param [Integer] note_id
155
+ # @param [String] body
156
+ # @return [Gitlab::ObjectifiedHash]
157
+ def update_triage_report_note(iid:, note_id:, body:)
158
+ gitlab.edit_issue_note(TRIAGE_PROJECT_ID, iid, note_id, body)
159
+ end
160
+
161
+ # Fetch all new failures
119
162
  #
120
163
  # @param [String] date
121
164
  # @param [String] state
122
165
  # @return [Array<Gitlab::ObjectifiedHash>]
123
- def fetch_failures(date:, state:)
166
+ def fetch_all_new_failures(date:, state:)
167
+ project_ids = [GITLAB_PROJECT_ID, CUSTOMERSDOT_PROJECT_ID]
168
+ failures = []
169
+
170
+ project_ids.each do |project_id|
171
+ failures += fetch_new_failures(project_id: project_id, date: date, state: state)
172
+ end
173
+
174
+ failures
175
+ end
176
+
177
+ # Fetch new failures for a given project
178
+ #
179
+ # @param [Integer] project_id
180
+ # @param [String] date
181
+ # @param [String] state
182
+ # @return [Array<Gitlab::ObjectifiedHash>]
183
+ def fetch_new_failures(project_id:, date:, state:)
124
184
  gitlab.issues(
125
- GITLAB_PROJECT_ID,
126
- labels: "failure::new",
185
+ project_id,
186
+ labels: FAILURE_NEW,
127
187
  order_by: "updated_at",
128
188
  state: state,
129
189
  scope: "all",
@@ -134,20 +194,22 @@ module Dri
134
194
 
135
195
  # Fetch failure notes
136
196
  #
197
+ # @param [Integer] project_id
137
198
  # @param [Integer] issue_iid
138
199
  # @return [Array<Gitlab::ObjectifiedHash>]
139
- def fetch_failure_notes(issue_iid)
140
- gitlab.issue_notes(GITLAB_PROJECT_ID, issue_iid, per_page: 100).auto_paginate
200
+ def fetch_failure_notes(project_id, issue_iid)
201
+ gitlab.issue_notes(project_id, issue_iid, per_page: 100).auto_paginate
141
202
  end
142
203
 
143
204
  # Delete award emoji
144
205
  #
145
206
  # @param [Integer] issue_iid
146
207
  # @param [Integer] emoji_id
208
+ # @param [Integer] project_id
147
209
  # @return [Gitlab::ObjectifiedHash]
148
- def delete_award_emoji(issue_iid, emoji_id)
210
+ def delete_award_emoji(issue_iid, emoji_id:, project_id: GITLAB_PROJECT_ID)
149
211
  gitlab.delete_award_emoji(
150
- GITLAB_PROJECT_ID,
212
+ project_id,
151
213
  issue_iid,
152
214
  "issue",
153
215
  emoji_id
data/lib/dri/command.rb CHANGED
@@ -60,6 +60,10 @@ module Dri
60
60
  @timezone ||= profile["settings"]["timezone"]
61
61
  end
62
62
 
63
+ def handover_report_path
64
+ @handover_report_path ||= profile["settings"]["handover_report_path"]
65
+ end
66
+
63
67
  def verify_config_exists
64
68
  return if config.exist?
65
69
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/table'
5
+ require_relative '../../utils/constants'
5
6
  require 'amatch'
6
7
  require 'fileutils'
7
8
 
@@ -11,10 +12,11 @@ module Dri
11
12
  class StackTraces < Dri::Command
12
13
  include Amatch
13
14
  include Dri::Utils::Table
15
+ include Dri::Utils::Constants::Triage::Labels
14
16
 
15
17
  def initialize(options)
16
18
  @options = options
17
- @labels = options[:labels] || 'failure::new'
19
+ @labels = options[:labels] || FAILURE_NEW
18
20
  @similarity_score_threshold = options[:similarity_score_threshold] || 0.9
19
21
  end
20
22
 
@@ -29,7 +31,7 @@ module Dri
29
31
  logger.info "#{Time.now.utc} API response completed"
30
32
 
31
33
  if response.empty?
32
- logger.info 'There are no failure::new issues identified!'
34
+ logger.info "There are no #{FAILURE_NEW} issues identified!"
33
35
  exit 0
34
36
  end
35
37
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/table'
5
+ require_relative '../../utils/constants'
5
6
 
6
7
  module Dri
7
8
  module Commands
8
9
  class Fetch
9
10
  class Failures < Dri::Command
10
11
  include Dri::Utils::Table
12
+ include Dri::Utils::Constants::Triage::Labels
11
13
  using Refinements
12
14
 
13
15
  SORT_BY_OPTIONS = {
@@ -34,33 +36,34 @@ module Dri
34
36
  author = add_color('Author', :bright_yellow)
35
37
  url = add_color('URL', :bright_yellow)
36
38
 
37
- failures = []
39
+ sorted_failures = []
38
40
  labels = [title, triaged, environment, author, url]
39
41
  triaged_counter = 0
40
42
 
41
43
  logger.info "Fetching today's failures..."
42
44
 
43
45
  spinner.run do # rubocop:disable Metrics/BlockLength
44
- response = api_client.fetch_failures(date: @today_iso_format, state: 'opened')
46
+ failures = api_client.fetch_all_new_failures(date: @today_iso_format, state: 'opened')
45
47
 
46
- if response.empty?
48
+ if failures.empty?
47
49
  logger.info 'Life is great, there are no new failures today!'
48
50
  exit 0
49
51
  end
50
52
 
51
- response.each do |failure|
53
+ failures.each do |failure|
54
+ project_id = failure.project_id
52
55
  title = failure.title.truncate(60)
53
56
  author = failure.to_h.dig('author', 'username')
54
57
  url = failure.web_url
55
58
  triaged = add_color('x', :red)
56
- envs = failure.labels.select { |l| l.include?('found:') }.map do |l|
59
+ envs = failure.labels.select { |l| l.include?(FOUND) }.map do |l|
57
60
  env = l.split(':').last.gsub('.gitlab.com', '')
58
61
 
59
62
  env == 'gitlab.com' ? 'production' : env
60
63
  end
61
64
  urgent = urgent_environments.all? { |env| envs.include?(env) }
62
65
 
63
- emoji_awards = api_client.fetch_awarded_emojis(failure.iid).find do |e|
66
+ emoji_awards = api_client.fetch_awarded_emojis(failure.iid, project_id: project_id).find do |e|
64
67
  e.name == emoji && e.to_h.dig('user', 'username') == username
65
68
  end
66
69
 
@@ -70,26 +73,26 @@ module Dri
70
73
  end
71
74
 
72
75
  if @options[:urgent]
73
- failures << [title, triaged, envs.first, author, url] if urgent
76
+ sorted_failures << [title, triaged, envs.first, author, url] if urgent
74
77
  else
75
- failures << [title, triaged, envs.first, author, url]
78
+ sorted_failures << [title, triaged, envs.first, author, url]
76
79
  end
77
80
  end
78
81
 
79
- failures.sort_by! { |failure| failure[SORT_BY_OPTIONS[@options[:sort_by]&.to_sym || :environment]] }
82
+ sorted_failures.sort_by! { |failure| failure[SORT_BY_OPTIONS[@options[:sort_by]&.to_sym || :environment]] }
80
83
  end
81
84
 
82
85
  msg = if @options[:urgent]
83
86
  <<~MSG
84
- Found: #{failures.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
87
+ Found: #{sorted_failures.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
85
88
  MSG
86
89
  else
87
90
  <<~MSG
88
- Found: #{failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
91
+ Found: #{sorted_failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
89
92
  MSG
90
93
  end
91
94
 
92
- print_table(labels, failures, alignments: [:left, :center, :center, :left])
95
+ print_table(labels, sorted_failures, alignments: [:left, :center, :center, :left])
93
96
  output.puts(msg)
94
97
  end
95
98
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/table'
5
- require_relative '../../utils/feature_flag_consts'
5
+ require_relative '../../utils/constants'
6
6
  require_relative '../../feature_flag_report'
7
7
 
8
8
  module Dri
@@ -10,7 +10,7 @@ module Dri
10
10
  class Fetch
11
11
  class FeatureFlags < Dri::Command
12
12
  include Dri::Utils::Table
13
- include Dri::Utils::FeatureFlagConsts
13
+ include Dri::Utils::Constants::FeatureFlag
14
14
 
15
15
  def initialize(options)
16
16
  @options = options
@@ -10,6 +10,7 @@ module Dri
10
10
  class Fetch
11
11
  class Pipelines < Dri::Command # rubocop:disable Metrics/ClassLength
12
12
  include Dri::Utils::Table
13
+ include Dri::Utils::Constants
13
14
  using Refinements
14
15
 
15
16
  NUM_OF_TESTS_LIVE_ENV = 1000
@@ -27,7 +28,7 @@ module Dri
27
28
  table_labels = define_table_labels
28
29
 
29
30
  spinner.run do
30
- Dri::Utils::Constants::PIPELINE_ENVIRONMENTS.each do |environment, details|
31
+ PIPELINE_ENVIRONMENTS.each do |environment, details|
31
32
  logger.info "Fetching last executed #{environment} pipeline"
32
33
  pipelines << fetch_pipeline(pipeline_name: environment.to_s, details: details)
33
34
  logger.info "Fetching complete for #{environment} ✓"
@@ -61,7 +62,7 @@ module Dri
61
62
  bridges = api_client.pipeline_bridges(project_id, pipeline_id)
62
63
  return if bridges.empty? # If downstream pipeline doesn't exist, which triggers the QA tests, return
63
64
 
64
- bridges.first["downstream_pipeline"]
65
+ bridges.find { |it| it.name == "e2e:package-and-test" }&.downstream_pipeline
65
66
  end
66
67
 
67
68
  # Get jobs from a pipeline
@@ -136,6 +137,14 @@ module Dri
136
137
  pipeline_id: pipeline.id, ops: ops)
137
138
  end
138
139
 
140
+ # Master pipeline run
141
+ #
142
+ # @param [String] pipeline_name
143
+ # @return [Boolean]
144
+ def master?(pipeline_name)
145
+ pipeline_name == "master"
146
+ end
147
+
139
148
  # Constructs allure report url for each pipeline
140
149
  # @param [String] pipeline_name
141
150
  # @param [Integer] pipeline_id
@@ -151,7 +160,7 @@ module Dri
151
160
  def allure_bucket_name(pipeline_name, sanity)
152
161
  case pipeline_name
153
162
  when "master"
154
- "package-and-qa"
163
+ "e2e-package-and-test"
155
164
  when "nightly"
156
165
  pipeline_name
157
166
  when "pre_prod"
@@ -182,16 +191,19 @@ module Dri
182
191
  url.include?("ops.gitlab.net")
183
192
  end
184
193
 
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"
194
+ # Slack notification job name
195
+ #
196
+ # @param [Boolean] ops
197
+ # @return [String]
198
+ def notify_job_name(ops)
199
+ ops ? "notify-slack-qa-fail" : "notify-slack-fail"
189
200
  end
190
201
 
191
202
  # Returns child pipeline if it is master pipeline
203
+ # @param [String] pipeline_name
192
204
  # @param [Gitlab::ObjectifiedHash] pipeline
193
- def pipeline_with_qa_tests(pipeline)
194
- if pipeline.web_url.to_s.include?("gitlab-qa-mirror")
205
+ def pipeline_with_qa_tests(pipeline_name, pipeline)
206
+ if master?(pipeline_name)
195
207
  bridge_pipeline(pipeline.project_id, pipeline.id)
196
208
  else
197
209
  pipeline
@@ -202,9 +214,13 @@ module Dri
202
214
  # @param [Hash] details
203
215
  # @param [Boolean] ops
204
216
  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
217
+ options = {
218
+ order_by: "updated_at",
219
+ scope: "finished",
220
+ ref: "master",
221
+ updated_after: past_timestamp(details[:search_hours_ago])
222
+ }
223
+ options.merge(username: "gitlab-bot") if ops || master?(details[:name])
208
224
  options
209
225
  end
210
226
 
@@ -218,7 +234,7 @@ module Dri
218
234
  # @param [Hash] details Pipeline environment details
219
235
  # @return [Array] Array of last executed pipeline details
220
236
  # rubocop:disable Metrics/PerceivedComplexity
221
- def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity
237
+ def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
222
238
  ops = ops_pipeline?(details[:url])
223
239
  options = options(details, ops)
224
240
  # instance is ops.gitlab.net or gitlab.com
@@ -228,18 +244,15 @@ module Dri
228
244
 
229
245
  # Return empty data to the table if no matching pipelines were found from the query
230
246
  response.each do |pipeline|
231
- pipeline_to_scan = pipeline_with_qa_tests(pipeline) # Fetch child pipeline if it is master
247
+ pipeline_to_scan = pipeline_with_qa_tests(pipeline_name, pipeline) # Fetch child pipeline if it is master
232
248
  next if pipeline_to_scan.nil?
233
249
 
234
250
  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))
251
+ next unless master?(pipeline_name) || contains_job?(pipeline_jobs, job_name: notify_job_name(ops))
236
252
 
237
253
  # 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
-
254
+ sanity = sanity?(pipeline_jobs: pipeline_jobs, pipeline: pipeline_to_scan, ops: ops)
241
255
  next if sanity.nil? # To filter out some "clean up" pipelines present in live environments
242
-
243
256
  next if sanity && @options[:full_runs_only] # Filter out sanity runs if --full-runs-only option is passed
244
257
 
245
258
  name = ops ? "#{pipeline_name}_#{run_type(sanity)}" : pipeline_name
@@ -2,12 +2,14 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/table'
5
+ require_relative '../../utils/constants'
5
6
 
6
7
  module Dri
7
8
  module Commands
8
9
  class Fetch
9
10
  class Quarantines < Dri::Command
10
11
  include Dri::Utils::Table
12
+ include Dri::Utils::Constants::Triage::Labels
11
13
  using Refinements
12
14
 
13
15
  def initialize(options, search: '[QUARANTINE]')
@@ -30,7 +32,7 @@ module Dri
30
32
  spinner.run do
31
33
  response = api_client.fetch_mrs(
32
34
  project_id: 'gitlab-org/gitlab',
33
- labels: 'QA,Quality',
35
+ labels: "#{QA},#{QUALITY}",
34
36
  search: @search,
35
37
  in: :title,
36
38
  state: :opened
@@ -2,12 +2,14 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/table'
5
+ require_relative '../../utils/constants'
5
6
 
6
7
  module Dri
7
8
  module Commands
8
9
  class Fetch
9
10
  class Triaged < Dri::Command
10
11
  include Dri::Utils::Table
12
+ include Dri::Utils::Constants::Triage::Labels
11
13
  using Refinements
12
14
 
13
15
  def initialize(options)
@@ -27,21 +29,21 @@ module Dri
27
29
  logger.info "Fetching your triaged failures..."
28
30
  spinner.start
29
31
 
30
- response = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
32
+ failures = api_client.fetch_all_triaged_failures(emoji: emoji, state: 'opened')
31
33
 
32
- if response.empty?
34
+ if failures.empty?
33
35
  logger.info "There are no failures triaged yet with #{add_color(emoji, :black, :on_white)}."
34
36
  exit 0
35
37
  end
36
38
 
37
- response.each do |triaged|
39
+ failures.each do |triaged|
38
40
  title = triaged["title"].truncate(70)
39
41
  url = triaged["web_url"]
40
42
  labels = triaged["labels"]
41
43
  type = ""
42
44
 
43
45
  labels.each do |label|
44
- type = label.gsub!('failure::', ' ').to_s if label.include? "failure::"
46
+ type = label.gsub!(FAILURE, ' ').to_s if label.include? FAILURE
45
47
  end
46
48
 
47
49
  failures_triaged << [title, url, type]
@@ -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(*)
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative '../command'
4
4
  require_relative '../utils/table'
5
+ require_relative '../utils/constants'
5
6
 
6
7
  module Dri
7
8
  module Commands
8
9
  class Incidents < Dri::Command
9
10
  include Dri::Utils::Table
11
+ include Dri::Utils::Constants::Triage::Labels
10
12
  using Refinements
11
13
 
12
14
  def initialize(options)
@@ -42,8 +44,8 @@ module Dri
42
44
  service = "N/A"
43
45
 
44
46
  labels.each do |label|
45
- status = label.gsub!('Incident::', ' ').to_s if label.include? "Incident::"
46
- service = label.gsub!('Service::', ' ').to_s if label.include? "Service::"
47
+ status = label.gsub!(INCIDENT, ' ').to_s if label.include? INCIDENT
48
+ service = label.gsub!(SERVICE, ' ').to_s if label.include? SERVICE
47
49
  end
48
50
 
49
51
  incidents << [title, service, status, url]
@@ -34,6 +34,8 @@ module Dri
34
34
  @ops_token = prompt.mask("Please provide your ops.gitlab.net personal access token:")
35
35
  @timezone = prompt.select("Choose your current timezone?", %w[EMEA AMER APAC])
36
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")
37
39
 
38
40
  if (@emoji || @token || @username || @ops_token).nil?
39
41
  logger.error "Please provide a username, gitlab token, ops token, timezone and emoji used for triage."
@@ -45,6 +47,7 @@ module Dri
45
47
  config.set(:settings, :ops_token, value: @ops_token)
46
48
  config.set(:settings, :timezone, value: @timezone)
47
49
  config.set(:settings, :emoji, value: @emoji)
50
+ config.set(:settings, :handover_report_path, value: @handover_report_path)
48
51
  config.write(force: true)
49
52
 
50
53
  logger.success "✅ We're ready to go 🚀"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '../../command'
4
4
  require_relative '../../utils/markdown_lists'
5
- require_relative '../../utils/feature_flag_consts'
5
+ require_relative '../../utils/constants'
6
6
  require_relative '../../report'
7
7
  require_relative '../../feature_flag_report'
8
8
 
@@ -14,7 +14,7 @@ module Dri
14
14
  module Commands
15
15
  class Publish
16
16
  class Report < Dri::Command # rubocop:disable Metrics/ClassLength
17
- include Dri::Utils::FeatureFlagConsts
17
+ include Dri::Utils::Constants::FeatureFlag
18
18
 
19
19
  def initialize(options)
20
20
  @options = options
@@ -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
- issues = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
35
+ failures = api_client.fetch_all_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 failures.empty? && incidents.empty?
41
+ logger.warn "Found no failures 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',
@@ -57,32 +58,52 @@ module Dri
57
58
 
58
59
  spinner.start
59
60
 
60
- issues.each do |issue|
61
+ failures.each do |failure|
61
62
  actions = []
62
63
 
63
64
  if @options[:actions]
64
65
  actions = prompt.multi_select(
65
- "Please mark the actions on #{add_color(issue.title, :yellow)}: ",
66
+ "Please mark the actions on #{add_color(failure.title, :yellow)}: ",
66
67
  action_options,
67
68
  per_page: 9
68
69
  )
69
70
  end
70
71
 
71
- report.add_failure(issue, actions)
72
+ report.add_failure(failure, actions)
73
+ end
74
+
75
+ unless incidents.empty?
76
+ incidents.each do |issue|
77
+ report.add_incident(issue)
78
+ end
72
79
  end
73
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
@@ -159,11 +180,29 @@ module Dri
159
180
  issues = api_client.fetch_current_triage_issue
160
181
  current_issue_iid = issues.first.iid
161
182
 
162
- api_client.post_triage_report_note(iid: current_issue_iid, body: note)
183
+ note_action = 'posted'
184
+ posted_note = nil
185
+ if @options[:update]
186
+ report_file = File.read("handover_reports/.tmp/report-#{@date}.json")
187
+ report_json = JSON.parse(report_file) if report_file
188
+ posted_note = api_client.update_triage_report_note(
189
+ iid: current_issue_iid, note_id: report_json['id'], body: note
190
+ )
191
+ note_action = 'updated'
192
+ else
193
+ posted_note = api_client.post_triage_report_note(iid: current_issue_iid, body: note)
194
+
195
+ FileUtils.mkdir_p("#{Dir.pwd}/handover_reports/.tmp")
196
+ report_path = "handover_reports/.tmp/report-#{@date}.json"
197
+
198
+ File.open(report_path, 'w') do |out_file|
199
+ out_file.write(posted_note.to_h.to_json)
200
+ end
201
+ end
163
202
 
164
203
  output.puts "Done! ✅\n"
165
204
  logger.success(<<~MSG)
166
- Thanks @#{username}, your report was posted at https://gitlab.com/gitlab-org/quality/pipeline-triage/-/issues/#{current_issue_iid} 🎉
205
+ Thanks @#{username}, your report was #{note_action} at https://gitlab.com/gitlab-org/quality/pipeline-triage/-/issues/#{current_issue_iid}#note_#{posted_note.id} 🎉
167
206
  MSG
168
207
  end
169
208
 
@@ -16,6 +16,9 @@ module Dri
16
16
  desc: 'Updates actions on failures'
17
17
  method_option :feature_flags, type: :boolean,
18
18
  desc: 'Adds summary of feature flag changes'
19
+ method_option :update, aliases: '-u', type: :boolean,
20
+ desc: 'Updates an existing report'
21
+
19
22
  def report(*)
20
23
  if options[:help]
21
24
  invoke :help, ['report']
@@ -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_all_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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './utils/feature_flag_consts'
3
+ require_relative './utils/constants'
4
4
 
5
5
  module Dri
6
6
  class FeatureFlagReport
7
- include Dri::Utils::FeatureFlagConsts
7
+ include Dri::Utils::Constants::FeatureFlag::Labels
8
8
 
9
9
  attr_reader :header, :labels, :prod, :staging, :staging_ref, :preprod
10
10
 
data/lib/dri/report.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './utils/constants'
4
+
3
5
  module Dri
4
6
  class Report # rubocop:disable Metrics/ClassLength
7
+ include Dri::Utils::Constants::Triage::Labels
5
8
  using Refinements
6
9
 
7
- attr_reader :header, :failures, :labels
10
+ attr_reader :header, :failures, :labels, :labels_incidents, :incidents
8
11
 
9
12
  def initialize(config)
10
13
  @labels = ['Title', 'Issue', 'Pipelines', 'Stack Trace', 'Actions']
14
+ @labels_incidents = %w[Incident Service Status URL]
11
15
  @failures = []
16
+ @incidents = []
12
17
  @date = Date.today
13
18
  @today = Date.today.strftime("%Y-%m-%d")
14
19
  @weekday = Date.today.strftime("%A")
@@ -21,17 +26,31 @@ module Dri
21
26
  @header = "# #{timezone}, #{@weekday} - #{@date}\n posted by: @#{username}"
22
27
  end
23
28
 
29
+ def add_incident(incident)
30
+ title = incident["title"]
31
+ url = incident["web_url"]
32
+ labels = incident["labels"]
33
+ status = 'N/A'
34
+ service = 'N/A'
35
+
36
+ labels.each do |label|
37
+ status = label.gsub!(INCIDENT, ' ').to_s if label.include? INCIDENT
38
+ service = label.gsub!(SERVICE, ' ').to_s if label.include? SERVICE
39
+ end
40
+
41
+ @incidents << [title, service, status, url]
42
+ end
43
+
24
44
  def add_failure(failure, actions_opts = [])
45
+ project_id = failure["project_id"]
25
46
  iid = failure["iid"]
26
47
  title = format_title(failure["title"])
27
48
  link = failure["web_url"]
28
49
  labels = failure["labels"]
29
- created_at = failure["created_at"]
30
- assignees = failure["assignees"]
31
50
  description = failure["description"]
32
51
 
33
- related_mrs = @api_client.fetch_related_mrs(iid)
34
- emoji = classify_failure_emoji(created_at)
52
+ related_mrs = @api_client.fetch_related_mrs(project_id, iid)
53
+ emoji = classify_failure_emoji(failure["created_at"])
35
54
  emojified_link = "#{emoji} #{link}"
36
55
 
37
56
  stack_blob = if description.empty?
@@ -43,10 +62,10 @@ module Dri
43
62
  stack_trace = ":link:[`#{stack_blob}...`](#{link}#stack-trace)"
44
63
 
45
64
  failure_type = filter_failure_type_labels(labels)
46
- assigned_status = assigned?(assignees)
65
+ assigned_status = assigned?(failure["assignees"])
47
66
  pipelines = filter_pipeline_labels(labels)
48
67
 
49
- linked_pipelines = link_pipelines(iid, pipelines, description)
68
+ linked_pipelines = link_pipelines(project_id, iid, pipelines, description)
50
69
 
51
70
  actions_status = actions_status_template(failure_type, assigned_status, actions_opts)
52
71
  actions_fixes = actions_fixes_template(related_mrs)
@@ -56,7 +75,7 @@ module Dri
56
75
 
57
76
  private
58
77
 
59
- def link_pipelines(iid, pipelines, description) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
78
+ def link_pipelines(project_id, iid, pipelines, description) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
60
79
  linked = []
61
80
  label_pipeline_map = {
62
81
  'gitlab.com' => '/quality/production',
@@ -71,7 +90,7 @@ module Dri
71
90
  'release' => '/quality/release'
72
91
  }
73
92
 
74
- failure_notes = @api_client.fetch_failure_notes(iid)
93
+ failure_notes = @api_client.fetch_failure_notes(project_id, iid)
75
94
 
76
95
  return if pipelines.empty?
77
96
 
@@ -156,10 +175,10 @@ module Dri
156
175
  pipelines = []
157
176
 
158
177
  labels.each do |label|
159
- matchers = { 'found:' => ' ' }
178
+ matchers = { FOUND => ' ' }
160
179
 
161
- if label.include? "found:"
162
- pipeline = label.gsub(/found:/) { |match| matchers[match] }
180
+ if label.include? FOUND
181
+ pipeline = label.gsub(/#{FOUND}/o) { |match| matchers[match] }
163
182
  pipelines << pipeline.strip
164
183
  end
165
184
  end
@@ -168,7 +187,7 @@ module Dri
168
187
 
169
188
  def filter_failure_type_labels(labels)
170
189
  labels.each do |label|
171
- @type = label.gsub!('failure::', ' ').to_s if label.include? "failure::"
190
+ @type = label.gsub!(FAILURE, ' ').to_s if label.include? FAILURE
172
191
  end
173
192
  @type
174
193
  end
@@ -3,6 +3,39 @@
3
3
  module Dri
4
4
  module Utils
5
5
  module Constants
6
+ module ProjectIDs
7
+ TESTCASES_PROJECT_ID = 11229385
8
+ TRIAGE_PROJECT_ID = 15291320
9
+ GITLAB_PROJECT_ID = 278964
10
+ CUSTOMERSDOT_PROJECT_ID = 2670515
11
+ FEATURE_FLAG_LOG_PROJECT_ID = 15208716
12
+ INFRA_TEAM_PROD_PROJECT_ID = 7444821
13
+ end
14
+
15
+ module Triage
16
+ module Labels
17
+ FOUND = 'found:'
18
+ FAILURE = 'failure::'
19
+ FAILURE_NEW = "#{FAILURE}new"
20
+ INCIDENT = 'Incident::'
21
+ SERVICE = 'Service::'
22
+ QA = 'QA'
23
+ QUALITY = 'Quality'
24
+ QUARANTINE = 'quarantine'
25
+ end
26
+ end
27
+
28
+ module FeatureFlag
29
+ module Labels
30
+ PRODUCTION = 'host::gitlab.com'
31
+ STAGING = 'host::staging.gitlab.com'
32
+ STAGING_REF = 'host::staging-ref.gitlab.com'
33
+ PREPROD = 'host::pre.gitlab.com'
34
+ end
35
+
36
+ TITLE_SUBSTRINGS = ["set to \"true\"", "set to \"false\""].freeze
37
+ end
38
+
6
39
  PIPELINE_ENVIRONMENTS =
7
40
  {
8
41
  production: {
@@ -49,9 +82,9 @@ module Dri
49
82
  },
50
83
  master: {
51
84
  name: "master",
52
- url: "https://gitlab.com/gitlab-org/gitlab-qa-mirror",
53
- project_id: "14707715",
54
- search_hours_ago: 6
85
+ url: "https://gitlab.com/gitlab-org/gitlab",
86
+ project_id: "278964",
87
+ search_hours_ago: 3
55
88
  }
56
89
  }.freeze
57
90
  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.6.1"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.8.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-14 00:00:00.000000000 Z
11
+ date: 2022-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amatch
@@ -355,7 +355,6 @@ files:
355
355
  - lib/dri/refinements/truncate.rb
356
356
  - lib/dri/report.rb
357
357
  - lib/dri/utils/constants.rb
358
- - lib/dri/utils/feature_flag_consts.rb
359
358
  - lib/dri/utils/markdown_lists.rb
360
359
  - lib/dri/utils/table.rb
361
360
  - lib/dri/version.rb
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dri
4
- module Utils
5
- module FeatureFlagConsts
6
- PRODUCTION = 'host::gitlab.com'
7
- STAGING = 'host::staging.gitlab.com'
8
- STAGING_REF = 'host::staging-ref.gitlab.com'
9
- PREPROD = 'host::pre.gitlab.com'
10
- TITLE_SUBSTRINGS = ["set to \"true\"", "set to \"false\""].freeze
11
- end
12
- end
13
- end