dri 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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