dri 0.4.0 → 0.6.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 +91 -16
- data/lib/dri/cli.rb +3 -0
- data/lib/dri/command.rb +7 -3
- data/lib/dri/commands/analyze/stack_traces.rb +106 -0
- data/lib/dri/commands/analyze.rb +20 -0
- data/lib/dri/commands/fetch/failures.rb +30 -26
- data/lib/dri/commands/fetch/featureflags.rb +12 -36
- data/lib/dri/commands/fetch/pipelines.rb +254 -0
- data/lib/dri/commands/fetch.rb +12 -0
- data/lib/dri/commands/init.rb +4 -2
- data/lib/dri/commands/profile.rb +1 -0
- data/lib/dri/commands/publish/report.rb +91 -17
- data/lib/dri/commands/publish.rb +2 -0
- data/lib/dri/feature_flag_report.rb +47 -0
- data/lib/dri/report.rb +1 -1
- data/lib/dri/utils/constants.rb +59 -0
- data/lib/dri/utils/feature_flag_consts.rb +13 -0
- data/lib/dri/utils/table.rb +2 -2
- data/lib/dri/version.rb +1 -1
- metadata +26 -7
- data/lib/dri/templates/incidents/.gitkeep +0 -1
@@ -2,34 +2,29 @@
|
|
2
2
|
|
3
3
|
require_relative '../../command'
|
4
4
|
require_relative '../../utils/table'
|
5
|
+
require_relative '../../utils/feature_flag_consts'
|
6
|
+
require_relative '../../feature_flag_report'
|
5
7
|
|
6
8
|
module Dri
|
7
9
|
module Commands
|
8
10
|
class Fetch
|
9
11
|
class FeatureFlags < Dri::Command
|
10
12
|
include Dri::Utils::Table
|
11
|
-
|
12
|
-
PRODUCTION = 'host::gitlab.com'
|
13
|
-
STAGING = 'host::staging.gitlab.com'
|
14
|
-
STAGING_REF = 'host::staging-ref.gitlab.com'
|
15
|
-
PREPROD = 'host::pre.gitlab.com'
|
13
|
+
include Dri::Utils::FeatureFlagConsts
|
16
14
|
|
17
15
|
def initialize(options)
|
18
16
|
@options = options
|
19
17
|
@today_iso_format = Time.now.strftime('%Y-%m-%dT00:00:00Z')
|
20
18
|
end
|
21
19
|
|
22
|
-
def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
20
|
+
def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
23
21
|
verify_config_exists
|
24
22
|
|
25
23
|
summary = add_color('Summary', :bright_yellow)
|
26
24
|
changed_on = add_color('Changed(UTC)', :bright_yellow)
|
27
25
|
url = add_color('URL', :bright_yellow)
|
28
26
|
|
29
|
-
|
30
|
-
staging_feature_flags = []
|
31
|
-
staging_ref_feature_flags = []
|
32
|
-
preprod_feature_flags = []
|
27
|
+
report = Dri::FeatureFlagReport.new
|
33
28
|
|
34
29
|
headers = [summary, changed_on, url]
|
35
30
|
|
@@ -37,42 +32,23 @@ module Dri
|
|
37
32
|
|
38
33
|
spinner.run do
|
39
34
|
response = api_client.fetch_feature_flag_logs(@today_iso_format)
|
35
|
+
|
40
36
|
if response.empty?
|
41
37
|
logger.info 'It\'s been quiet...no feature flag changes for today 👀'
|
42
38
|
break
|
43
39
|
end
|
44
40
|
|
45
41
|
response.each do |feature_flag|
|
46
|
-
|
47
|
-
|
48
|
-
substrings = ["set to \"true\"", "set to \"false\""]
|
49
|
-
next unless substrings.any? { |substr| summary.include?(substr) }
|
50
|
-
|
51
|
-
changed_on = feature_flag.description[/(?<=Changed on \(in UTC\): ).+?(?=\n)/].delete('`')
|
52
|
-
url = feature_flag.web_url
|
53
|
-
|
54
|
-
feature_flag_data = [summary, changed_on, url]
|
55
|
-
|
56
|
-
labels = feature_flag.labels
|
57
|
-
host_label = labels.select { |label| /^host::/.match(label) }.join('')
|
42
|
+
next unless TITLE_SUBSTRINGS.any? { |substr| feature_flag.title.include?(substr) }
|
58
43
|
|
59
|
-
|
60
|
-
when PRODUCTION
|
61
|
-
prod_feature_flags << feature_flag_data
|
62
|
-
when STAGING
|
63
|
-
staging_feature_flags << feature_flag_data
|
64
|
-
when STAGING_REF
|
65
|
-
staging_ref_feature_flags << feature_flag_data
|
66
|
-
when PREPROD
|
67
|
-
preprod_feature_flags << feature_flag_data
|
68
|
-
end
|
44
|
+
report.add_change(feature_flag)
|
69
45
|
end
|
70
46
|
end
|
71
47
|
|
72
|
-
print_results('Production', headers,
|
73
|
-
print_results('Staging', headers,
|
74
|
-
print_results('Staging Ref', headers,
|
75
|
-
print_results('Preprod', headers,
|
48
|
+
print_results('Production', headers, report.prod, output) unless report.prod.empty?
|
49
|
+
print_results('Staging', headers, report.staging, output) unless report.staging.empty?
|
50
|
+
print_results('Staging Ref', headers, report.staging_ref, output) unless report.staging_ref.empty?
|
51
|
+
print_results('Preprod', headers, report.preprod, output) unless report.preprod.empty?
|
76
52
|
end
|
77
53
|
|
78
54
|
private
|
@@ -0,0 +1,254 @@
|
|
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
|
+
pipelines = []
|
26
|
+
table_labels = define_table_labels
|
27
|
+
|
28
|
+
spinner.run do
|
29
|
+
Dri::Utils::Constants::PIPELINE_ENVIRONMENTS.each do |environment, details|
|
30
|
+
logger.info "Fetching last executed #{environment} pipeline"
|
31
|
+
pipelines << fetch_pipeline(pipeline_name: environment.to_s, details: details)
|
32
|
+
logger.info "Fetching complete for #{environment}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
print_table(table_labels, pipelines, alignments: [:left, :center, :center, :left])
|
37
|
+
pipelines # Returning the array mainly for spec
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Format a past date
|
43
|
+
# @param [Integer] hours_ago the amount of hours from now
|
44
|
+
# @return [String] formatted datetime
|
45
|
+
def past_timestamp(hours_ago)
|
46
|
+
timestamp = Time.now - (hours_ago * 60 * 60)
|
47
|
+
timestamp.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the first downstream pipeline of a project
|
51
|
+
# @param [Integer] project_id the id of the project
|
52
|
+
# @param [Integer] pipeline_id the pipeline id
|
53
|
+
# @return [Gitlab::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
|
54
|
+
def bridge_pipeline(project_id, pipeline_id)
|
55
|
+
bridges = api_client.pipeline_bridges(project_id, pipeline_id)
|
56
|
+
return if bridges.empty? # If downstream pipeline doesn't exist, which triggers the QA tests, return
|
57
|
+
|
58
|
+
bridges.first["downstream_pipeline"]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get jobs from a pipeline
|
62
|
+
# @param [Integer] project_id the id of the project
|
63
|
+
# @param [Integer] pipeline_id the pipeline id
|
64
|
+
# @param [Boolean] ops true if ops instance
|
65
|
+
# @return [Array::ObjectifiedHash,nil] nil if downstream (bridge) pipeline does not exist
|
66
|
+
def jobs(project_id:, pipeline_id:, ops: false)
|
67
|
+
api_client(ops: ops).pipeline_jobs(project_id, pipeline_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Checks if tests count exceeds threshold in a pipeline
|
71
|
+
# @param [Integer] project_id the id of the project
|
72
|
+
# @param [Integer] pipeline_id the pipeline id
|
73
|
+
# @param [Boolean] ops true if ops instance
|
74
|
+
# @return [Boolean] true if count exceeds threshold defined in constant NUM_OF_TESTS_LIVE_ENV
|
75
|
+
def tests_exceed_threshold?(project_id:, pipeline_id:, ops: true)
|
76
|
+
api_client(ops: ops).pipeline_test_report(project_id, pipeline_id).total_count > NUM_OF_TESTS_LIVE_ENV
|
77
|
+
end
|
78
|
+
|
79
|
+
# Checks if a job is present in an array of jobs
|
80
|
+
# @param [Array] jobs
|
81
|
+
# @param [String] job_name name of the job
|
82
|
+
# @return [Boolean] true if job is present
|
83
|
+
def contains_job?(jobs, job_name:)
|
84
|
+
jobs.any? { |job| job["name"].include?(job_name) }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks if a stage is present from a list of jobs
|
88
|
+
# @param [Array] jobs
|
89
|
+
# @param [String] stage_name name of the stage
|
90
|
+
# @return [Boolean] true if stage is present
|
91
|
+
def contains_stage?(jobs, stage_name)
|
92
|
+
jobs.any? { |job| job["stage"].include?(stage_name) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Checks if pipeline ran only the QA smoke tests
|
96
|
+
# @param [Array] jobs
|
97
|
+
# @param [Integer] project_id
|
98
|
+
# @param [Integer] pipeline_id
|
99
|
+
# @param [Boolean] ops true if ops instance
|
100
|
+
def smoke_run?(jobs:, project_id:, pipeline_id:, ops:)
|
101
|
+
contains_stage?(jobs, "sanity") &&
|
102
|
+
!tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Checks if pipeline ran full suite of qa tests
|
106
|
+
# @param [Array] jobs
|
107
|
+
# @param [Integer] project_id
|
108
|
+
# @param [Integer] pipeline_id
|
109
|
+
# @param [Boolean] ops true if ops instance
|
110
|
+
def full_run?(jobs:, project_id:, pipeline_id:, ops:)
|
111
|
+
if ops
|
112
|
+
(contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")) &&
|
113
|
+
tests_exceed_threshold?(project_id: project_id, pipeline_id: pipeline_id, ops: ops)
|
114
|
+
else
|
115
|
+
contains_stage?(jobs, "qa") || contains_stage?(jobs, "test")
|
116
|
+
# Nightly pipeline does not execute full E2E suite if sanity fails so can't check tests count
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Combined logic to check if a pipeline was a sanity run for all pipeline types - ie., live environment
|
121
|
+
# and gitlab-qa-mirror pipelines
|
122
|
+
# @param [Array] pipeline_jobs
|
123
|
+
# @param [Object] pipeline
|
124
|
+
# @param [Boolean] ops
|
125
|
+
def sanity?(pipeline_jobs:, pipeline:, ops:)
|
126
|
+
return true if ops && smoke_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
|
127
|
+
pipeline_id: pipeline.id, ops: ops)
|
128
|
+
|
129
|
+
false if full_run?(jobs: pipeline_jobs, project_id: pipeline.project_id,
|
130
|
+
pipeline_id: pipeline.id, ops: ops)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Constructs allure report url for each pipeline
|
134
|
+
# @param [String] pipeline_name
|
135
|
+
# @param [Integer] pipeline_id
|
136
|
+
# @param [Boolean] sanity
|
137
|
+
def allure_report(pipeline_name:, pipeline_id:, sanity:)
|
138
|
+
"https://storage.googleapis.com/gitlab-qa-allure-reports/#{allure_bucket_name(pipeline_name, sanity)}"\
|
139
|
+
"/master/#{pipeline_id}/index.html"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the GCP bucket name for different pipeline types
|
143
|
+
# @param [String] pipeline_name
|
144
|
+
# @param [Boolean] sanity
|
145
|
+
def allure_bucket_name(pipeline_name, sanity)
|
146
|
+
case pipeline_name
|
147
|
+
when "master"
|
148
|
+
"package-and-qa"
|
149
|
+
when "nightly"
|
150
|
+
pipeline_name
|
151
|
+
when "pre_prod"
|
152
|
+
"preprod-#{run_type(sanity)}"
|
153
|
+
else
|
154
|
+
"#{pipeline_name.sub('_', '-')}-#{run_type(sanity)}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def run_type(sanity)
|
159
|
+
sanity == true ? "sanity" : "full"
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns table headers
|
163
|
+
# @return [Array]
|
164
|
+
def define_table_labels
|
165
|
+
name = add_color("Pipeline", :magenta)
|
166
|
+
pipeline_last_executed = add_color("Last executed at", :magenta)
|
167
|
+
url = add_color("Pipeline Url", :magenta)
|
168
|
+
report = add_color("Last report", :magenta)
|
169
|
+
result = add_color("Result", :magenta)
|
170
|
+
[name, pipeline_last_executed, url, report, result]
|
171
|
+
end
|
172
|
+
|
173
|
+
# Checks if pipeline is running on ops.gitlab.net or gitlab.com
|
174
|
+
# @param [String] url
|
175
|
+
def ops_pipeline?(url)
|
176
|
+
url.include?("ops.gitlab.net")
|
177
|
+
end
|
178
|
+
|
179
|
+
def notify_slack_job_name(pipeline_name, ops)
|
180
|
+
return "notify-slack-qa-fail" if ops
|
181
|
+
|
182
|
+
pipeline_name.to_s.include?("master") ? "notify_slack" : "notify-slack-fail"
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns child pipeline if it is master pipeline
|
186
|
+
# @param [Gitlab::ObjectifiedHash] pipeline
|
187
|
+
def pipeline_with_qa_tests(pipeline)
|
188
|
+
if pipeline.web_url.to_s.include?("gitlab-qa-mirror")
|
189
|
+
bridge_pipeline(pipeline.project_id, pipeline.id)
|
190
|
+
else
|
191
|
+
pipeline
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns query options for pipelines api call
|
196
|
+
# @param [Hash] details
|
197
|
+
# @param [Boolean] ops
|
198
|
+
def options(details, ops)
|
199
|
+
options = { order_by: "updated_at", scope: "finished",
|
200
|
+
updated_after: past_timestamp(details[:search_hours_ago]) }
|
201
|
+
options.merge(username: "gitlab-bot") if ops
|
202
|
+
options
|
203
|
+
end
|
204
|
+
|
205
|
+
def emoji_for_success_failure(status)
|
206
|
+
return add_color("✓", :green) if status.include?("success")
|
207
|
+
|
208
|
+
add_color("x", :red)
|
209
|
+
end
|
210
|
+
|
211
|
+
# @param [String] pipeline_name
|
212
|
+
# @param [Hash] details Pipeline environment details
|
213
|
+
# @return [Array] Array of last executed pipeline details
|
214
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
215
|
+
def fetch_pipeline(pipeline_name:, details:) # rubocop:disable Metrics/CyclomaticComplexity
|
216
|
+
ops = ops_pipeline?(details[:url])
|
217
|
+
options = options(details, ops)
|
218
|
+
# instance is ops.gitlab.net or gitlab.com
|
219
|
+
response = api_client(ops: ops).pipelines(project_id: details[:project_id],
|
220
|
+
options: options, auto_paginate: true)
|
221
|
+
return [pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] if response.empty?
|
222
|
+
|
223
|
+
# Return empty data to the table if no matching pipelines were found from the query
|
224
|
+
response.each do |pipeline|
|
225
|
+
pipeline_to_scan = pipeline_with_qa_tests(pipeline) # Fetch child pipeline if it is master
|
226
|
+
next if pipeline_to_scan.nil?
|
227
|
+
|
228
|
+
pipeline_jobs = jobs(project_id: pipeline.project_id, pipeline_id: pipeline_to_scan.id, ops: ops)
|
229
|
+
next unless contains_job?(pipeline_jobs, job_name: notify_slack_job_name(pipeline_name, ops))
|
230
|
+
|
231
|
+
# Need to know if it is a sanity or a full run to construct allure report url
|
232
|
+
sanity = sanity?(pipeline_jobs: pipeline_jobs, pipeline: pipeline_to_scan,
|
233
|
+
ops: ops)
|
234
|
+
|
235
|
+
next if sanity.nil? # To filter out some "clean up" pipelines present in live environments
|
236
|
+
|
237
|
+
next if sanity && @options[:full_runs_only] # Filter out sanity runs if --full-runs-only option is passed
|
238
|
+
|
239
|
+
name = ops ? "#{pipeline_name}_#{run_type(sanity)}" : pipeline_name
|
240
|
+
pipeline_last_executed = pipeline_to_scan.updated_at
|
241
|
+
url = pipeline_to_scan.web_url
|
242
|
+
report = allure_report(pipeline_name: pipeline_name, pipeline_id: pipeline_to_scan.id, sanity: sanity)
|
243
|
+
result = emoji_for_success_failure(pipeline_to_scan.status)
|
244
|
+
return [name, pipeline_last_executed, url, report, result]
|
245
|
+
end
|
246
|
+
|
247
|
+
[pipeline_name, NOT_FOUND, NOT_FOUND, NOT_FOUND, NOT_FOUND] # Parsed through all of the response and
|
248
|
+
# no matching pipelines found
|
249
|
+
end
|
250
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
data/lib/dri/commands/fetch.rb
CHANGED
@@ -82,6 +82,18 @@ module Dri
|
|
82
82
|
require_relative 'fetch/quarantines'
|
83
83
|
Dri::Commands::Fetch::Quarantines.new(options, search: '[DEQUARANTINE]').execute
|
84
84
|
end
|
85
|
+
|
86
|
+
desc 'pipelines', 'Display status of pipelines'
|
87
|
+
method_option :help, aliases: '-h', type: :boolean,
|
88
|
+
desc: 'Display pipelines usage information'
|
89
|
+
method_option :full_runs_only, type: :boolean,
|
90
|
+
desc: 'Displays full pipeline runs only'
|
91
|
+
def pipelines(*)
|
92
|
+
return invoke :help, %w[pipelines] if options[:help]
|
93
|
+
|
94
|
+
require_relative 'fetch/pipelines'
|
95
|
+
Dri::Commands::Fetch::Pipelines.new(options).execute
|
96
|
+
end
|
85
97
|
end
|
86
98
|
end
|
87
99
|
end
|
data/lib/dri/commands/init.rb
CHANGED
@@ -31,16 +31,18 @@ module Dri
|
|
31
31
|
|
32
32
|
@username = prompt.ask("What is your GitLab username?")
|
33
33
|
@token = prompt.mask("Please provide your GitLab personal access token:")
|
34
|
+
@ops_token = prompt.mask("Please provide your ops.gitlab.net personal access token:")
|
34
35
|
@timezone = prompt.select("Choose your current timezone?", %w[EMEA AMER APAC])
|
35
36
|
@emoji = prompt.ask("Have a triage emoji?")
|
36
37
|
|
37
|
-
if (@emoji || @token || @username).nil?
|
38
|
-
logger.error "Please provide a username, token, timezone and emoji used for triage."
|
38
|
+
if (@emoji || @token || @username || @ops_token).nil?
|
39
|
+
logger.error "Please provide a username, gitlab token, ops token, timezone and emoji used for triage."
|
39
40
|
exit 1
|
40
41
|
end
|
41
42
|
|
42
43
|
config.set(:settings, :user, value: @username)
|
43
44
|
config.set(:settings, :token, value: @token)
|
45
|
+
config.set(:settings, :ops_token, value: @ops_token)
|
44
46
|
config.set(:settings, :timezone, value: @timezone)
|
45
47
|
config.set(:settings, :emoji, value: @emoji)
|
46
48
|
config.write(force: true)
|
data/lib/dri/commands/profile.rb
CHANGED
@@ -38,6 +38,7 @@ module Dri
|
|
38
38
|
def pretty_print_profile
|
39
39
|
<<~PROFILE
|
40
40
|
#{add_color('User:', :bright_cyan)} #{username}\n #{add_color('Token:', :bright_cyan)} #{token}
|
41
|
+
#{add_color('OpsToken:', :bright_cyan)} #{ops_token}
|
41
42
|
#{add_color('Timezone:', :bright_cyan)} #{timezone}
|
42
43
|
#{add_color('Emoji:', :bright_cyan)} #{emoji}
|
43
44
|
PROFILE
|
@@ -2,21 +2,26 @@
|
|
2
2
|
|
3
3
|
require_relative '../../command'
|
4
4
|
require_relative '../../utils/markdown_lists'
|
5
|
-
require_relative
|
5
|
+
require_relative '../../utils/feature_flag_consts'
|
6
|
+
require_relative '../../report'
|
7
|
+
require_relative '../../feature_flag_report'
|
6
8
|
|
7
9
|
require 'markdown-tables'
|
8
10
|
require 'fileutils'
|
9
|
-
require
|
11
|
+
require 'uri'
|
10
12
|
|
11
13
|
module Dri
|
12
14
|
module Commands
|
13
15
|
class Publish
|
14
|
-
class Report < Dri::Command
|
16
|
+
class Report < Dri::Command # rubocop:disable Metrics/ClassLength
|
17
|
+
include Dri::Utils::FeatureFlagConsts
|
18
|
+
|
15
19
|
def initialize(options)
|
16
20
|
@options = options
|
17
21
|
|
18
22
|
@date = Date.today
|
19
23
|
@time = Time.now.to_i
|
24
|
+
@today_iso_format = Time.now.strftime('%Y-%m-%dT00:00:00Z')
|
20
25
|
end
|
21
26
|
|
22
27
|
def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
@@ -26,7 +31,9 @@ module Dri
|
|
26
31
|
logger.info "Fetching triaged failures with award emoji #{emoji}..."
|
27
32
|
|
28
33
|
spinner.start
|
34
|
+
|
29
35
|
issues = api_client.fetch_triaged_failures(emoji: emoji, state: 'opened')
|
36
|
+
|
30
37
|
spinner.stop
|
31
38
|
|
32
39
|
if issues.empty?
|
@@ -34,27 +41,28 @@ module Dri
|
|
34
41
|
exit 1
|
35
42
|
end
|
36
43
|
|
37
|
-
logger.info
|
44
|
+
logger.info 'Assembling the failures report... '
|
38
45
|
# sets each failure on the table
|
39
46
|
action_options = [
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
'pinged SET',
|
48
|
+
'reproduced',
|
49
|
+
'transient',
|
50
|
+
'quarantined',
|
51
|
+
'active investigation',
|
52
|
+
'blocking pipelines',
|
53
|
+
'awaiting for a fix to merge',
|
54
|
+
'notified the team',
|
55
|
+
'due to feature flag'
|
49
56
|
]
|
50
57
|
|
51
58
|
spinner.start
|
59
|
+
|
52
60
|
issues.each do |issue|
|
53
61
|
actions = []
|
54
62
|
|
55
63
|
if @options[:actions]
|
56
64
|
actions = prompt.multi_select(
|
57
|
-
"Please mark the actions on #{add_color(issue
|
65
|
+
"Please mark the actions on #{add_color(issue.title, :yellow)}: ",
|
58
66
|
action_options,
|
59
67
|
per_page: 9
|
60
68
|
)
|
@@ -77,14 +85,59 @@ module Dri
|
|
77
85
|
end
|
78
86
|
end
|
79
87
|
|
88
|
+
spinner.stop
|
89
|
+
|
90
|
+
if @options[:feature_flags]
|
91
|
+
logger.info 'Fetching today\'s feature flag changes...'
|
92
|
+
|
93
|
+
feature_flag_report = Dri::FeatureFlagReport.new
|
94
|
+
|
95
|
+
spinner.start
|
96
|
+
|
97
|
+
feature_flags = api_client.fetch_feature_flag_logs(@today_iso_format)
|
98
|
+
|
99
|
+
feature_flags.each do |feature_flag|
|
100
|
+
next unless TITLE_SUBSTRINGS.any? { |substr| feature_flag.title.include?(substr) }
|
101
|
+
|
102
|
+
feature_flag_report.add_change(feature_flag)
|
103
|
+
end
|
104
|
+
|
105
|
+
spinner.stop
|
106
|
+
|
107
|
+
logger.info 'Assembling the feature flags report...'
|
108
|
+
|
109
|
+
spinner.start
|
110
|
+
|
111
|
+
feature_flag_note = "\n\n## Feature Flag Changes"
|
112
|
+
feature_flag_changes = ''
|
113
|
+
|
114
|
+
format_type = @options[:format] == 'list' ? :list : :table
|
115
|
+
|
116
|
+
feature_flag_report.get_all_flag_changes.each do |env, changes|
|
117
|
+
next if changes.empty?
|
118
|
+
|
119
|
+
feature_flag_changes += format_feature_flag_changes(
|
120
|
+
env, changes, feature_flag_report.labels, format_type
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
feature_flag_note += if feature_flag_changes.empty?
|
125
|
+
"\n\nNo changes found today"
|
126
|
+
else
|
127
|
+
"\n\n<details><summary>Click to expand</summary>#{feature_flag_changes}</details>"
|
128
|
+
end
|
129
|
+
|
130
|
+
spinner.stop
|
131
|
+
end
|
132
|
+
|
80
133
|
report.set_header(timezone, username)
|
81
134
|
note = "#{report.header}\n\n#{format_style}"
|
82
135
|
|
83
|
-
|
136
|
+
note += feature_flag_note if @options[:feature_flags]
|
84
137
|
|
85
138
|
# creates an .md file with the report locally in /handover_reports
|
86
139
|
if @options[:dry_run]
|
87
|
-
logger.info
|
140
|
+
logger.info 'Downloading the report... '
|
88
141
|
|
89
142
|
spinner.start
|
90
143
|
|
@@ -104,7 +157,7 @@ module Dri
|
|
104
157
|
|
105
158
|
# sends note to the weekly triage report
|
106
159
|
issues = api_client.fetch_current_triage_issue
|
107
|
-
current_issue_iid = issues
|
160
|
+
current_issue_iid = issues.first.iid
|
108
161
|
|
109
162
|
api_client.post_triage_report_note(iid: current_issue_iid, body: note)
|
110
163
|
|
@@ -113,6 +166,27 @@ module Dri
|
|
113
166
|
Thanks @#{username}, your report was posted at https://gitlab.com/gitlab-org/quality/pipeline-triage/-/issues/#{current_issue_iid} 🎉
|
114
167
|
MSG
|
115
168
|
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def format_feature_flag_changes(env, changes, labels, format_type)
|
173
|
+
unless format_type == :table || format_type == :list
|
174
|
+
raise ArgumentError, 'format_type must be one of type :table or :list'
|
175
|
+
end
|
176
|
+
|
177
|
+
case format_type
|
178
|
+
when :list
|
179
|
+
formatted_changes = Utils::MarkdownLists.make_list(labels, changes)
|
180
|
+
when :table
|
181
|
+
formatted_changes = MarkdownTables.make_table(
|
182
|
+
labels,
|
183
|
+
changes,
|
184
|
+
is_rows: true, align: %w[l l l]
|
185
|
+
)
|
186
|
+
end
|
187
|
+
|
188
|
+
"\n\n### #{env.to_s.capitalize.tr('_', ' ')}\n\n#{formatted_changes}"
|
189
|
+
end
|
116
190
|
end
|
117
191
|
end
|
118
192
|
end
|
data/lib/dri/commands/publish.rb
CHANGED
@@ -14,6 +14,8 @@ module Dri
|
|
14
14
|
desc: 'Formats the report'
|
15
15
|
method_option :actions, type: :boolean,
|
16
16
|
desc: 'Updates actions on failures'
|
17
|
+
method_option :feature_flags, type: :boolean,
|
18
|
+
desc: 'Adds summary of feature flag changes'
|
17
19
|
def report(*)
|
18
20
|
if options[:help]
|
19
21
|
invoke :help, ['report']
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './utils/feature_flag_consts'
|
4
|
+
|
5
|
+
module Dri
|
6
|
+
class FeatureFlagReport
|
7
|
+
include Dri::Utils::FeatureFlagConsts
|
8
|
+
|
9
|
+
attr_reader :header, :labels, :prod, :staging, :staging_ref, :preprod
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@header = '## Feature Flag Changes'
|
13
|
+
@labels = %w[Summary Changed(UTC) URL]
|
14
|
+
@prod = []
|
15
|
+
@staging = []
|
16
|
+
@staging_ref = []
|
17
|
+
@preprod = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_change(feature_flag)
|
21
|
+
summary = feature_flag.title
|
22
|
+
|
23
|
+
changed_on = feature_flag.description[/(?<=Changed on \(in UTC\): ).+?(?=\n)/].delete('`')
|
24
|
+
url = feature_flag.web_url
|
25
|
+
|
26
|
+
feature_flag_data = [summary, changed_on, url]
|
27
|
+
|
28
|
+
labels = feature_flag.labels
|
29
|
+
host_label = labels.select { |label| /^host::/.match(label) }.join('')
|
30
|
+
|
31
|
+
case host_label
|
32
|
+
when PRODUCTION
|
33
|
+
@prod << feature_flag_data
|
34
|
+
when STAGING
|
35
|
+
@staging << feature_flag_data
|
36
|
+
when STAGING_REF
|
37
|
+
@staging_ref << feature_flag_data
|
38
|
+
when PREPROD
|
39
|
+
@preprod << feature_flag_data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_all_flag_changes
|
44
|
+
{ production: @prod, staging: @staging, staging_ref: @staging_ref, preprod: @preprod }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/dri/report.rb
CHANGED