dri 0.3.0 → 0.5.0

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