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.
@@ -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, Metrics/MethodLength
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
- prod_feature_flags = []
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
- summary = feature_flag.title
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
- case host_label
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, prod_feature_flags, output) unless prod_feature_flags.empty?
73
- print_results('Staging', headers, staging_feature_flags, output) unless staging_feature_flags.empty?
74
- print_results('Staging Ref', headers, staging_ref_feature_flags, output) unless staging_ref_feature_flags.empty? # rubocop:disable Layout/LineLength
75
- print_results('Preprod', headers, preprod_feature_flags, output) unless preprod_feature_flags.empty?
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
@@ -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
@@ -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)
@@ -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 "../../report"
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 "uri"
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 "Assembling the report... "
44
+ logger.info 'Assembling the failures report... '
38
45
  # sets each failure on the table
39
46
  action_options = [
40
- "pinged SET",
41
- "reproduced",
42
- "transient",
43
- "quarantined",
44
- "active investigation",
45
- "blocking pipelines",
46
- "awaiting for a fix to merge",
47
- "notified the team",
48
- "due to feature flag"
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['title'], :yellow)}: ",
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
- spinner.stop
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 "Downloading the report... "
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[0]["iid"]
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
@@ -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
@@ -95,7 +95,7 @@ module Dri
95
95
  end
96
96
 
97
97
  unless pipeline_link.empty?
98
- pipeline_link_sanitized = pipeline_link.join.strip.chop
98
+ pipeline_link_sanitized = pipeline_link.join.strip
99
99
  pipeline_markdown = "[#{pipeline_markdown}](#{pipeline_link_sanitized})"
100
100
  end
101
101