dri 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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