dri 0.3.0 → 0.5.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: ebb53c09c3a4e7f7c6daa06acd9e8fb1be1c5aef4fffb97db6e84029aafe481e
4
- data.tar.gz: f7cb0fc63042a33766c02a787cb10af4188839fb9f96d9aa650f5addc279cac9
3
+ metadata.gz: 879c4292254a2a17a0ebcd8e2c0a3f8bbc6e654a69c153ad66423c71eb750fc4
4
+ data.tar.gz: 51f8e1acec1894ce4a6ab264ef690cf063259f46512d743609bf2f5a6529132f
5
5
  SHA512:
6
- metadata.gz: 01f7bbef2920e466cde321720d210541cbb60b377240ae28a74e6fdd66bd89f70a5b4aba5e69d622466d5379ed6e59adce3933844d08171449496b9638dfe1f6
7
- data.tar.gz: c263146aa4a230cab0b8cdebc7c2a6b1d278baa3cf63b5d356fa7c8ee3143472c0e69ec3eaa7006317f74249c66167f4e03a72dfccab0f2d52ea5fde4e1d972f
6
+ metadata.gz: ad020b17eb8d05322800332e59b5d286dcb8712df055dab50be7f234df2864a5ba719f900d61b0ba25308a1ea130fa2b586b7f01215beec90eafb614cc8c6da1
7
+ data.tar.gz: c0ef20575dcc1f9fa3312df5970b79a8b6fa11b8401a1a80d30561c8a764c7bcd140d4e649c619fbce1be6b9f71c943fb51aa5a48c20b18c5f480af4a821bc07
data/.gitlab-ci.yml CHANGED
@@ -45,3 +45,11 @@ rspec:
45
45
  extends: .job_base
46
46
  script:
47
47
  - bundle exec rspec --color
48
+
49
+ rspec 3.0:
50
+ stage: test
51
+ extends: .job_base
52
+ image: ruby:3.0
53
+ script:
54
+ - bundle exec rspec --color
55
+
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.5
1
+ 3.0.4
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dri (0.1.3)
4
+ dri (0.4.0)
5
+ gitlab (~> 4.18)
5
6
  httparty (~> 0.20.0)
6
7
  json (~> 2.6.1)
7
8
  markdown-tables (~> 1.1.1)
@@ -27,10 +28,14 @@ GEM
27
28
  addressable (2.8.0)
28
29
  public_suffix (>= 2.0.2, < 5.0)
29
30
  ast (2.4.2)
31
+ coderay (1.1.3)
30
32
  concurrent-ruby (1.1.10)
31
33
  crack (0.4.5)
32
34
  rexml
33
35
  diff-lcs (1.5.0)
36
+ gitlab (4.18.0)
37
+ httparty (~> 0.18)
38
+ terminal-table (>= 1.5.1)
34
39
  gitlab-styles (7.0.0)
35
40
  rubocop (~> 0.91, >= 0.91.1)
36
41
  rubocop-gitlab-security (~> 0.1.1)
@@ -44,8 +49,9 @@ GEM
44
49
  multi_xml (>= 0.5.2)
45
50
  i18n (1.10.0)
46
51
  concurrent-ruby (~> 1.0)
47
- json (2.6.1)
52
+ json (2.6.2)
48
53
  markdown-tables (1.1.1)
54
+ method_source (1.0.0)
49
55
  mime-types (3.4.1)
50
56
  mime-types-data (~> 3.2015)
51
57
  mime-types-data (3.2022.0105)
@@ -56,6 +62,9 @@ GEM
56
62
  ast (~> 2.4.1)
57
63
  pastel (0.8.0)
58
64
  tty-color (~> 0.5)
65
+ pry (0.14.1)
66
+ coderay (~> 1.1)
67
+ method_source (~> 1.0)
59
68
  public_suffix (4.0.6)
60
69
  rack (2.2.3)
61
70
  rainbow (3.1.1)
@@ -106,6 +115,8 @@ GEM
106
115
  unicode-display_width (>= 1.5, < 3.0)
107
116
  unicode_utils (~> 1.4)
108
117
  strings-ansi (0.2.0)
118
+ terminal-table (3.0.2)
119
+ unicode-display_width (>= 1.1.1, < 3)
109
120
  thor (1.0.1)
110
121
  timecop (0.9.5)
111
122
  tty-box (0.7.0)
@@ -150,6 +161,7 @@ PLATFORMS
150
161
  DEPENDENCIES
151
162
  dri!
152
163
  gitlab-styles (~> 7.0.0)
164
+ pry (~> 0.14.1)
153
165
  rake
154
166
  rspec (~> 3.10.0)
155
167
  timecop (~> 0.9.1)
data/README.md CHANGED
@@ -145,6 +145,7 @@ $ dri publish report --format=list # formats the report in a list
145
145
  $ dri publish report --format=table # formats the report in a table (default)
146
146
  $ dri publish report --dry-run # the report is only generated locally
147
147
  $ dri publish report --actions # activate the actions prompt for each failure
148
+ $ dri publish report --feature-flags # includes a summary of the feature flag changes on each environment
148
149
  ```
149
150
 
150
151
  **Note:** These options above can be combined like:
data/dri.gemspec CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
+ spec.add_dependency "gitlab", "~> 4.18"
25
26
  spec.add_dependency 'httparty', '~> 0.20.0'
26
27
  spec.add_dependency 'json', '~> 2.6.1'
27
28
  spec.add_dependency 'markdown-tables', '~> 1.1.1'
@@ -37,6 +38,7 @@ Gem::Specification.new do |spec|
37
38
  spec.add_dependency 'tty-table', '~> 0.12.0'
38
39
 
39
40
  spec.add_development_dependency 'gitlab-styles', '~> 7.0.0'
41
+ spec.add_development_dependency "pry", "~> 0.14.1"
40
42
  spec.add_development_dependency 'rake'
41
43
  spec.add_development_dependency 'rspec', '~> 3.10.0'
42
44
  spec.add_development_dependency "timecop", "~> 0.9.1"
@@ -4,9 +4,10 @@ require "httparty"
4
4
  require "json"
5
5
  require "tty-config"
6
6
  require 'cgi'
7
+ require 'gitlab'
7
8
 
8
9
  module Dri
9
- class ApiClient # rubocop:disable Metrics/ClassLength
10
+ class ApiClient
10
11
  API_URL = 'https://gitlab.com/api/v4'
11
12
  TESTCASES_PROJECT_ID = '11229385'
12
13
  TRIAGE_PROJECT_ID = '15291320'
@@ -15,41 +16,53 @@ module Dri
15
16
  INFRA_TEAM_PROD_PROJECT_ID = '7444821'
16
17
 
17
18
  def initialize(config)
18
- profile = config.read
19
- @token = profile["settings"]["token"]
20
- end
21
-
22
- def header
23
- @header ||= { 'Content-Type' => 'application/json', "Authorization" => "Bearer #{@token}" }
19
+ @token = config.read.dig("settings", "token")
24
20
  end
25
21
 
22
+ # Fetch triaged failures
23
+ #
24
+ # @param [String] emoji
25
+ # @param [String] state
26
+ # @return [Gitlab::ObjectifiedHash]
26
27
  def fetch_triaged_failures(emoji:, state:)
27
- url = ["#{API_URL}/issues"]
28
- url << "?order_by=updated_at&my_reaction_emoji=#{emoji}"
29
- url << "&scope=all&state=#{state}"
30
-
31
- fetch_json(url.join)
28
+ gitlab.issues(
29
+ GITLAB_PROJECT_ID,
30
+ order_by: "updated_at",
31
+ my_reaction_emoji: emoji,
32
+ scope: "all",
33
+ state: state
34
+ )
32
35
  end
33
36
 
34
- def fetch_awarded_emojis(url)
35
- fetch_json(url)
37
+ # Fetch award emojis
38
+ #
39
+ # @param [Integer] issue_iid
40
+ # @return [Array<Gitlab::ObjectifiedHash>]
41
+ def fetch_awarded_emojis(issue_iid)
42
+ gitlab.award_emojis(GITLAB_PROJECT_ID, issue_iid, "issue")
36
43
  end
37
44
 
45
+ # Fetch failing testcases
46
+ #
47
+ # @param [String] pipeline
48
+ # @param [String] state
49
+ # @return [Array<Gitlab::ObjectifiedHash>]
38
50
  def fetch_failing_testcases(pipeline, state:)
39
- url = ["#{API_URL}/projects/"]
40
- url << "#{TESTCASES_PROJECT_ID}/issues?labels=#{pipeline}::failed"
41
- url << "&state=#{state}&not[labels]=quarantine"
42
- url << "&scope=all"
43
-
44
- fetch_json(url.join)
51
+ gitlab.issues(
52
+ TESTCASES_PROJECT_ID,
53
+ labels: "#{pipeline}::failed",
54
+ state: state,
55
+ scope: "all",
56
+ "not[labels]": "quarantine"
57
+ ).auto_paginate
45
58
  end
46
59
 
47
- def fetch_related_mrs(issue_iid:)
48
- url = ["#{API_URL}/projects/"]
49
- url << "#{GITLAB_PROJECT_ID}/issues/"
50
- url << "#{issue_iid}/related_merge_requests"
51
-
52
- fetch_json(url.join)
60
+ # Fetch related issue mrs
61
+ #
62
+ # @param [Integer] issue_iid
63
+ # @return [Array<Gitlab::ObjectifiedHash>]
64
+ def fetch_related_mrs(issue_iid)
65
+ gitlab.related_issue_merge_requests(GITLAB_PROJECT_ID, issue_iid)
53
66
  end
54
67
 
55
68
  # Fetch MRs
@@ -63,104 +76,91 @@ module Dri
63
76
  # @option options [String] milestone
64
77
  # @option options [String] labels
65
78
  def fetch_mrs(project_id:, **options)
66
- uri = URI(API_URL)
67
- uri.query = HTTParty::HashConversions.to_params(options)
68
-
69
- # CGI.escape('gitlab-org/gitlab') => 'gitlab-org%2Fgitlab'
70
- uri.path = File.join(uri.path, 'projects', CGI.escape(project_id), 'merge_requests')
71
-
72
- fetch_json(uri.to_s)
79
+ gitlab.merge_requests(project_id, per_page: 100, **options).auto_paginate
73
80
  end
74
81
 
82
+ # Fetch current triage issue
83
+ #
84
+ # @return [Gitlab::ObjectifiedHash]
75
85
  def fetch_current_triage_issue
76
- url = ["#{API_URL}/projects/"]
77
- url << "#{TRIAGE_PROJECT_ID}/issues?state=opened"
78
- url << "&order_by=updated_at"
79
-
80
- fetch_json(url.join)
86
+ gitlab.issues(TRIAGE_PROJECT_ID, state: "opened", order_by: "updated_at")
81
87
  end
82
88
 
89
+ # Create triage report note
90
+ #
91
+ # @param [Integer] iid
92
+ # @param [String] body
93
+ # @return [Gitlab::ObjectifiedHash]
83
94
  def post_triage_report_note(iid:, body:)
84
- url = ["#{API_URL}/projects/"]
85
- url << "#{TRIAGE_PROJECT_ID}/issues/#{iid}/notes"
86
-
87
- post_json(url.join, body)
95
+ gitlab.create_issue_note(TRIAGE_PROJECT_ID, iid, body)
88
96
  end
89
97
 
98
+ # Fetch new failures
99
+ #
100
+ # @param [String] date
101
+ # @param [String] state
102
+ # @return [Array<Gitlab::ObjectifiedHash>]
90
103
  def fetch_failures(date:, state:)
91
- url = ["#{API_URL}/issues"]
92
- url << "?labels=failure::new"
93
- url << "&order_by=updated_at&state=#{state}"
94
- url << "&scope=all"
95
- url << "&created_after=#{date}"
96
- url << "&per_page=100"
97
-
98
- fetch_json(url.join)
99
- end
100
-
101
- def fetch_failure_notes(issue_iid:)
102
- url = ["#{API_URL}/projects/"]
103
- url << "#{GITLAB_PROJECT_ID}/issues/"
104
- url << "#{issue_iid}/notes?per_page=15"
105
-
106
- fetch_json(url.join)
107
- end
108
-
109
- def delete_award_emoji(url)
110
- delete_json(url)
104
+ gitlab.issues(
105
+ GITLAB_PROJECT_ID,
106
+ labels: "failure::new",
107
+ order_by: "updated_at",
108
+ state: state,
109
+ scope: "all",
110
+ created_after: date,
111
+ per_page: 100
112
+ )
113
+ end
114
+
115
+ # Fetch failure notes
116
+ #
117
+ # @param [Integer] issue_iid
118
+ # @return [Array<Gitlab::ObjectifiedHash>]
119
+ def fetch_failure_notes(issue_iid)
120
+ gitlab.issue_notes(GITLAB_PROJECT_ID, issue_iid, per_page: 100).auto_paginate
111
121
  end
112
122
 
113
- def fetch_feature_flag_logs(date:, page:)
114
- url = ["#{API_URL}/projects/"]
115
- url << "#{FEATURE_FLAG_LOG_PROJECT_ID}/issues"
116
- url << "?created_after=#{date}"
117
- url << "&per_page=100"
118
- url << "&page=#{page}"
119
-
120
- fetch_json(url.join)
123
+ # Delete award emoji
124
+ #
125
+ # @param [Integer] issue_iid
126
+ # @param [Integer] emoji_id
127
+ # @return [Gitlab::ObjectifiedHash]
128
+ def delete_award_emoji(issue_iid, emoji_id)
129
+ gitlab.delete_award_emoji(
130
+ GITLAB_PROJECT_ID,
131
+ issue_iid,
132
+ "issue",
133
+ emoji_id
134
+ )
135
+ end
136
+
137
+ # Fetch feature flag log issues
138
+ #
139
+ # @param [String] date
140
+ # @return [Array<Gitlab::ObjectifiedHash>]
141
+ def fetch_feature_flag_logs(date)
142
+ gitlab.issues(FEATURE_FLAG_LOG_PROJECT_ID, created_after: date, per_page: 100).auto_paginate
121
143
  end
122
144
 
145
+ # Fetch ongoing incidents
146
+ #
147
+ # @return [Array<Gitlab::ObjectifiedHash>]
123
148
  def incidents
124
- url = ["#{API_URL}/projects/"]
125
- url << "#{INFRA_TEAM_PROD_PROJECT_ID}/issues"
126
- url << "?order_by=updated_at&state=opened"
127
- url << "&labels=incident"
128
-
129
- fetch_json(url.join)
149
+ gitlab.issues(INFRA_TEAM_PROD_PROJECT_ID, order_by: "updated_at", state: "opened", labels: "incident")
130
150
  end
131
151
 
132
152
  private
133
153
 
134
- def post_json(url, body)
135
- options = {
136
- body: { body: body },
137
- headers: {
138
- "Authorization" => "Bearer #{@token}"
139
- }
140
- }
154
+ attr_reader :token
141
155
 
142
- response = HTTParty.post(url, options)
143
- handle_response(response)
144
- end
145
-
146
- def delete_json(url)
147
- response = HTTParty.delete(url, headers: header)
148
- handle_response(response)
149
- end
150
-
151
- def handle_response(response)
152
- response
153
- end
154
-
155
- def fetch_json(url)
156
- response = HTTParty.get(url, headers: header)
157
-
158
- if response.code != 200
159
- puts "Response error code \"#{response.code}\" - Unable to sync with GitLab"
160
- exit 1
161
- end
162
-
163
- handle_response(JSON.parse(response.body))
156
+ # Gitlab client
157
+ #
158
+ # @return [Gitlab::Client]
159
+ def gitlab
160
+ @gitlab ||= Gitlab.client(
161
+ endpoint: API_URL,
162
+ private_token: token
163
+ )
164
164
  end
165
165
  end
166
166
  end
data/lib/dri/command.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'command'
4
4
  require_relative 'api_client'
5
+ require_relative 'gitlab/issues'
5
6
 
6
7
  require 'dri/refinements/truncate'
7
8
 
@@ -25,8 +26,8 @@ module Dri
25
26
  config = TTY::Config.new
26
27
  config.filename = ".dri_profile"
27
28
  config.extname = ".yml"
28
- config.append_path Dir.pwd
29
29
  config.append_path Dir.home
30
+ config.append_path Dir.pwd
30
31
  config
31
32
  end
32
33
  end
@@ -93,7 +94,7 @@ module Dri
93
94
  # @api public
94
95
  def command(**options)
95
96
  require 'tty-command'
96
- TTY::Command.new(options)
97
+ TTY::Command.new(**options)
97
98
  end
98
99
 
99
100
  # The cursor movement
@@ -123,7 +124,7 @@ module Dri
123
124
  # @api public
124
125
  def prompt(**options)
125
126
  require 'tty-prompt'
126
- TTY::Prompt.new(options.merge(interrupt: :exit))
127
+ TTY::Prompt.new(**options.merge(interrupt: :exit))
127
128
  end
128
129
  end
129
130
  end
@@ -40,20 +40,19 @@ module Dri
40
40
  spinner.run do # rubocop:disable Metrics/BlockLength
41
41
  response = api_client.fetch_failures(date: @today_iso_format, state: 'opened')
42
42
 
43
- if response.nil?
43
+ if response.empty?
44
44
  logger.info 'Life is great, there are no new failures today!'
45
45
  exit 0
46
46
  end
47
47
 
48
48
  response.each do |failure|
49
- title = failure['title'].truncate(60)
50
- author = failure['author']['username']
51
- url = failure['web_url']
52
- award_emoji_url = failure['_links']['award_emoji']
49
+ title = failure.title.truncate(60)
50
+ author = failure.to_h.dig('author', 'username')
51
+ url = failure.web_url
53
52
  triaged = add_color('x', :red)
54
53
 
55
- emoji_awards = api_client.fetch_awarded_emojis(award_emoji_url).find do |e|
56
- (e['name'] == emoji) && (e['user']['username'] == username)
54
+ emoji_awards = api_client.fetch_awarded_emojis(failure.iid).find do |e|
55
+ e.name == emoji && e.to_h.dig('user', 'username') == username
57
56
  end
58
57
 
59
58
  if emoji_awards
@@ -62,7 +61,7 @@ module Dri
62
61
  end
63
62
 
64
63
  if @options[:urgent]
65
- labels = failure['labels']
64
+ labels = failure.labels
66
65
 
67
66
  labels.each do |label|
68
67
  if label.include?('found:canary.gitlab.com' && 'found:canary.staging.gitlab.com')
@@ -2,84 +2,53 @@
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
 
36
31
  logger.info "Fetching today's feature flag changes..."
37
32
 
38
- spinner.run do # rubocop:disable Metrics/BlockLength
39
- page = 1
40
- loop do # rubocop:disable Metrics/BlockLength
41
- response = api_client.fetch_feature_flag_logs(date: @today_iso_format, page: page)
42
-
43
- if response.nil? && page == 1
44
- logger.info 'It\'s been quiet...no feature flag changes for today 👀'
45
- exit 0
46
- end
47
-
48
- response.each do |feature_flag|
49
- summary = feature_flag['title']
50
-
51
- substrings = ["set to \"true\"", "set to \"false\""]
52
- next unless substrings.any? { |substr| summary.include?(substr) }
33
+ spinner.run do
34
+ response = api_client.fetch_feature_flag_logs(@today_iso_format)
53
35
 
54
- changed_on = feature_flag['description'][/(?<=Changed on \(in UTC\): ).+?(?=\n)/].delete('`')
55
- url = feature_flag['web_url']
56
-
57
- feature_flag_data = [summary, changed_on, url]
58
-
59
- labels = feature_flag['labels']
60
- host_label = labels.select { |label| /^host::/.match(label) }.join('')
36
+ if response.empty?
37
+ logger.info 'It\'s been quiet...no feature flag changes for today 👀'
38
+ break
39
+ end
61
40
 
62
- case host_label
63
- when PRODUCTION
64
- prod_feature_flags << feature_flag_data
65
- when STAGING
66
- staging_feature_flags << feature_flag_data
67
- when STAGING_REF
68
- staging_ref_feature_flags << feature_flag_data
69
- when PREPROD
70
- preprod_feature_flags << feature_flag_data
71
- end
72
- end
41
+ response.each do |feature_flag|
42
+ next unless TITLE_SUBSTRINGS.any? { |substr| feature_flag.title.include?(substr) }
73
43
 
74
- page += 1
75
- break if response.count < 100 || response.nil?
44
+ report.add_change(feature_flag)
76
45
  end
77
46
  end
78
47
 
79
- print_results('Production', headers, prod_feature_flags, output) unless prod_feature_flags.empty?
80
- print_results('Staging', headers, staging_feature_flags, output) unless staging_feature_flags.empty?
81
- print_results('Staging Ref', headers, staging_ref_feature_flags, output) unless staging_ref_feature_flags.empty? # rubocop:disable Layout/LineLength
82
- 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?
83
52
  end
84
53
 
85
54
  private
@@ -37,8 +37,8 @@ module Dri
37
37
  )
38
38
 
39
39
  mrs = response.each_with_object([]) do |mr, found_mrs|
40
- title = mr['title'][13..].truncate(60) # remove the "[QUARANTINE] " prefix
41
- url = mr['web_url']
40
+ title = mr.title[13..].truncate(60) # remove the "[QUARANTINE] " prefix
41
+ url = mr.web_url
42
42
 
43
43
  found_mrs << [title, url]
44
44
  end
@@ -47,8 +47,8 @@ module Dri
47
47
 
48
48
  output.puts "♦♦♦♦♦ #{add_color(pipeline, :black, :on_white)}♦♦♦♦♦\n\n"
49
49
 
50
- response.each do |pipeline|
51
- output.puts "#{title_label} #{pipeline['title']}\n#{url_label} #{pipeline['web_url']}"
50
+ response.each do |test_case|
51
+ output.puts "#{title_label} #{test_case.title}\n#{url_label} #{test_case.web_url}"
52
52
  output.puts "#{divider}\n"
53
53
  end
54
54
  end
@@ -31,13 +31,13 @@ module Dri
31
31
 
32
32
  if response.nil?
33
33
  logger.info 'Hooray, no active incidents 🎉.'
34
- exit 0
34
+ break
35
35
  end
36
36
 
37
37
  response.each do |incident|
38
- title = incident['title'].truncate(70)
39
- url = incident['web_url']
40
- labels = incident['labels']
38
+ title = incident.title.truncate(70)
39
+ url = incident.web_url
40
+ labels = incident.labels
41
41
  status = "N/A"
42
42
  service = "N/A"
43
43
 
@@ -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']
@@ -29,18 +29,13 @@ module Dri
29
29
  spinner.stop
30
30
 
31
31
  issues_with_award_emoji.each do |issue|
32
- logger.info "Removing #{emoji} from #{issue['web_url']}..."
32
+ logger.info "Removing #{emoji} from #{issue.web_url}..."
33
33
 
34
- award_emoji_url = issue["_links"]["award_emoji"]
34
+ response = api_client.fetch_awarded_emojis(issue.iid)
35
35
 
36
- response = api_client.fetch_awarded_emojis(award_emoji_url)
36
+ emoji_found = response.find { |e| e.name == emoji && e.to_h.dig('user', 'username') == username }
37
37
 
38
- emoji_found = response.find { |e| e['name'] == emoji && e['user']['username'] == username }
39
-
40
- unless emoji_found.nil?
41
- url = "#{award_emoji_url}/#{emoji_found['id']}"
42
- api_client.delete_award_emoji(url)
43
- end
38
+ api_client.delete_award_emoji(issue.iid, emoji_found.id) unless emoji_found.nil?
44
39
  end
45
40
  output.puts "Done! ✅"
46
41
  logger.success "Removed #{emoji} from #{issues_with_award_emoji.size} issue(s)."
@@ -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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gitlab gem monkeypatch for related merge requests
4
+ # TODO: Add method upstream
5
+ #
6
+ class Gitlab::Client
7
+ module Issues
8
+ # List related merge requests
9
+ #
10
+ # @example
11
+ # Gitlab.related_issue_merge_requests(3, 42)
12
+ #
13
+ # @param [Integer, String] project The ID or name of a project.
14
+ # @param [Integer] id The ID of an issue.
15
+ def related_issue_merge_requests(project, id)
16
+ get("/projects/#{url_encode project}/issues/#{id}/related_merge_requests")
17
+ end
18
+ end
19
+ end
data/lib/dri/report.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Dri
4
4
  class Report # rubocop:disable Metrics/ClassLength
5
+ using Refinements
6
+
5
7
  attr_reader :header, :failures, :labels
6
8
 
7
9
  def initialize(config)
@@ -28,7 +30,7 @@ module Dri
28
30
  assignees = failure["assignees"]
29
31
  description = failure["description"]
30
32
 
31
- related_mrs = @api_client.fetch_related_mrs(issue_iid: iid)
33
+ related_mrs = @api_client.fetch_related_mrs(iid)
32
34
  emoji = classify_failure_emoji(created_at)
33
35
  emojified_link = "#{emoji} #{link}"
34
36
 
@@ -69,7 +71,7 @@ module Dri
69
71
  'release' => '/quality/release'
70
72
  }
71
73
 
72
- failure_notes = @api_client.fetch_failure_notes(issue_iid: iid)
74
+ failure_notes = @api_client.fetch_failure_notes(iid)
73
75
 
74
76
  return if pipelines.empty?
75
77
 
@@ -81,10 +83,10 @@ module Dri
81
83
  pipeline_markdown = pipeline.gsub(/.gitlab.com/, '')
82
84
 
83
85
  failure_notes.each do |note|
84
- next unless note["body"].include?(label_pipeline_map.fetch(pipeline))
86
+ next unless note.body.include?(label_pipeline_map.fetch(pipeline))
85
87
 
86
88
  pipeline_in_notes_found = true
87
- pipeline_link = URI.extract(note["body"], %w[https])
89
+ pipeline_link = URI.extract(note.body, %w[https])
88
90
  end
89
91
 
90
92
  unless pipeline_in_notes_found
@@ -191,7 +193,7 @@ module Dri
191
193
  path = "Failure in `#{path.strip}`" if path.delete_prefix!('Failure in ')
192
194
  path = path.strip.gsub(' ', '&nbsp;') if desc
193
195
 
194
- "#{path} | #{desc}"
196
+ [path, desc].compact.join(' | ')
195
197
  end
196
198
  end
197
199
  end
@@ -0,0 +1,13 @@
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
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.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.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-05-11 00:00:00.000000000 Z
11
+ date: 2022-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gitlab
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.18'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.18'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: httparty
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -206,6 +220,20 @@ dependencies:
206
220
  - - "~>"
207
221
  - !ruby/object:Gem::Version
208
222
  version: 7.0.0
223
+ - !ruby/object:Gem::Dependency
224
+ name: pry
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: 0.14.1
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: 0.14.1
209
237
  - !ruby/object:Gem::Dependency
210
238
  name: rake
211
239
  requirement: !ruby/object:Gem::Requirement
@@ -305,9 +333,12 @@ files:
305
333
  - lib/dri/commands/rm/emoji.rb
306
334
  - lib/dri/commands/rm/profile.rb
307
335
  - lib/dri/commands/rm/reports.rb
336
+ - lib/dri/feature_flag_report.rb
337
+ - lib/dri/gitlab/issues.rb
308
338
  - lib/dri/refinements/truncate.rb
309
339
  - lib/dri/report.rb
310
340
  - lib/dri/templates/incidents/.gitkeep
341
+ - lib/dri/utils/feature_flag_consts.rb
311
342
  - lib/dri/utils/markdown_lists.rb
312
343
  - lib/dri/utils/table.rb
313
344
  - lib/dri/version.rb