dri 0.3.1 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +14 -2
- data/README.md +1 -0
- data/dri.gemspec +2 -0
- data/lib/dri/api_client.rb +105 -105
- data/lib/dri/command.rb +2 -1
- data/lib/dri/commands/fetch/failures.rb +36 -33
- data/lib/dri/commands/fetch/featureflags.rb +18 -49
- data/lib/dri/commands/fetch/quarantines.rb +2 -2
- data/lib/dri/commands/fetch/testcases.rb +2 -2
- data/lib/dri/commands/incidents.rb +4 -4
- data/lib/dri/commands/publish/report.rb +91 -17
- data/lib/dri/commands/publish.rb +2 -0
- data/lib/dri/commands/rm/emoji.rb +4 -9
- data/lib/dri/feature_flag_report.rb +47 -0
- data/lib/dri/gitlab/issues.rb +19 -0
- data/lib/dri/report.rb +6 -6
- data/lib/dri/utils/feature_flag_consts.rb +13 -0
- data/lib/dri/version.rb +1 -1
- metadata +33 -3
- data/lib/dri/templates/incidents/.gitkeep +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb71a1d13a00cb33c436608e3c34e40a4202505bad341d31d5d85803c36495d4
|
4
|
+
data.tar.gz: 73648bafa41eac28824435b62a41ead17494c144bbd99c6af52062e616379f2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c2d592d0906e3764ec1ab2fe298a42a1798be1d58dd216261ed2a88227cfe007a97a175ae199f5ee4f4db16f7efe2dd6f99521e8febc740e191b61c0f2ef8cc
|
7
|
+
data.tar.gz: 1106b7fc11be59d38c72f471f8a5148eb4e724a81665b4382185da5c22a4e554fc7e1b1cc274a980b9ceeb6071f7fa5da0397e3657655132a6d21f2a31cf9712
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.4
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dri (0.
|
4
|
+
dri (0.5.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.
|
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"
|
data/lib/dri/api_client.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
135
|
-
options = {
|
136
|
-
body: { body: body },
|
137
|
-
headers: {
|
138
|
-
"Authorization" => "Bearer #{@token}"
|
139
|
-
}
|
140
|
-
}
|
154
|
+
attr_reader :token
|
141
155
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
@@ -13,8 +13,9 @@ module Dri
|
|
13
13
|
SORT_BY_OPTIONS = {
|
14
14
|
title: 0,
|
15
15
|
triaged: 1,
|
16
|
-
|
17
|
-
|
16
|
+
environment: 2,
|
17
|
+
author: 3,
|
18
|
+
url: 4
|
18
19
|
}.freeze
|
19
20
|
|
20
21
|
def initialize(options)
|
@@ -25,14 +26,16 @@ module Dri
|
|
25
26
|
def execute(input: $stdin, output: $stdout) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
26
27
|
verify_config_exists
|
27
28
|
|
29
|
+
urgent_environments = %w[canary canary.staging]
|
30
|
+
|
28
31
|
title = add_color('Title', :bright_yellow)
|
29
32
|
triaged = add_color('Triaged?', :bright_yellow)
|
33
|
+
environment = add_color('Environment', :bright_yellow)
|
30
34
|
author = add_color('Author', :bright_yellow)
|
31
35
|
url = add_color('URL', :bright_yellow)
|
32
36
|
|
33
37
|
failures = []
|
34
|
-
|
35
|
-
labels = [title, triaged, author, url]
|
38
|
+
labels = [title, triaged, environment, author, url]
|
36
39
|
triaged_counter = 0
|
37
40
|
|
38
41
|
logger.info "Fetching today's failures..."
|
@@ -40,20 +43,25 @@ module Dri
|
|
40
43
|
spinner.run do # rubocop:disable Metrics/BlockLength
|
41
44
|
response = api_client.fetch_failures(date: @today_iso_format, state: 'opened')
|
42
45
|
|
43
|
-
if response.
|
46
|
+
if response.empty?
|
44
47
|
logger.info 'Life is great, there are no new failures today!'
|
45
48
|
exit 0
|
46
49
|
end
|
47
50
|
|
48
51
|
response.each do |failure|
|
49
|
-
title = failure
|
50
|
-
author = failure
|
51
|
-
url = failure
|
52
|
-
award_emoji_url = failure['_links']['award_emoji']
|
52
|
+
title = failure.title.truncate(60)
|
53
|
+
author = failure.to_h.dig('author', 'username')
|
54
|
+
url = failure.web_url
|
53
55
|
triaged = add_color('x', :red)
|
56
|
+
envs = failure.labels.select { |l| l.include?('found:') }.map do |l|
|
57
|
+
env = l.split(':').last.gsub('.gitlab.com', '')
|
58
|
+
|
59
|
+
env == 'gitlab.com' ? 'production' : env
|
60
|
+
end
|
61
|
+
urgent = urgent_environments.all? { |env| envs.include?(env) }
|
54
62
|
|
55
|
-
emoji_awards = api_client.fetch_awarded_emojis(
|
56
|
-
|
63
|
+
emoji_awards = api_client.fetch_awarded_emojis(failure.iid).find do |e|
|
64
|
+
e.name == emoji && e.to_h.dig('user', 'username') == username
|
57
65
|
end
|
58
66
|
|
59
67
|
if emoji_awards
|
@@ -62,32 +70,27 @@ module Dri
|
|
62
70
|
end
|
63
71
|
|
64
72
|
if @options[:urgent]
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
if label.include?('found:canary.gitlab.com' && 'found:canary.staging.gitlab.com')
|
69
|
-
urgent << [title, triaged, author, url]
|
70
|
-
end
|
71
|
-
end
|
73
|
+
failures << [title, triaged, envs.first, author, url] if urgent
|
74
|
+
else
|
75
|
+
failures << [title, triaged, envs.first, author, url]
|
72
76
|
end
|
73
|
-
|
74
|
-
failures << [title, triaged, author, url]
|
75
|
-
|
76
|
-
failures.sort_by! { |e| e[SORT_BY_OPTIONS[@options[:sort_by].to_sym]] } if @options[:sort_by]
|
77
77
|
end
|
78
|
-
end
|
79
78
|
|
80
|
-
|
81
|
-
print_table(labels, urgent, alignments: [:left, :center, :center, :left])
|
82
|
-
output.puts(<<~MSG)
|
83
|
-
Found: #{urgent.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
|
84
|
-
MSG
|
85
|
-
else
|
86
|
-
print_table(labels, failures, alignments: [:left, :center, :center, :left])
|
87
|
-
output.puts(<<~MSG)
|
88
|
-
Found: #{failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
|
89
|
-
MSG
|
79
|
+
failures.sort_by! { |failure| failure[SORT_BY_OPTIONS[@options[:sort_by]&.to_sym || :environment]] }
|
90
80
|
end
|
81
|
+
|
82
|
+
msg = if @options[:urgent]
|
83
|
+
<<~MSG
|
84
|
+
Found: #{failures.size} urgent failures, occurring in both canary.gitlab.com and canary.staging.gitlab.com.
|
85
|
+
MSG
|
86
|
+
else
|
87
|
+
<<~MSG
|
88
|
+
Found: #{failures.size} failures, of these #{triaged_counter} have been triaged with a #{emoji}.
|
89
|
+
MSG
|
90
|
+
end
|
91
|
+
|
92
|
+
print_table(labels, failures, alignments: [:left, :center, :center, :left])
|
93
|
+
output.puts(msg)
|
91
94
|
end
|
92
95
|
end
|
93
96
|
end
|
@@ -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
|
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
|
|
36
31
|
logger.info "Fetching today's feature flag changes..."
|
37
32
|
|
38
|
-
spinner.run do
|
39
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
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,
|
80
|
-
print_results('Staging', headers,
|
81
|
-
print_results('Staging Ref', headers,
|
82
|
-
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?
|
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
|
41
|
-
url = mr
|
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 |
|
51
|
-
output.puts "#{title_label} #{
|
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
|
-
|
34
|
+
break
|
35
35
|
end
|
36
36
|
|
37
37
|
response.each do |incident|
|
38
|
-
title = incident
|
39
|
-
url = incident
|
40
|
-
labels = incident
|
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
|
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']
|
@@ -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
|
32
|
+
logger.info "Removing #{emoji} from #{issue.web_url}..."
|
33
33
|
|
34
|
-
|
34
|
+
response = api_client.fetch_awarded_emojis(issue.iid)
|
35
35
|
|
36
|
-
|
36
|
+
emoji_found = response.find { |e| e.name == emoji && e.to_h.dig('user', 'username') == username }
|
37
37
|
|
38
|
-
emoji_found
|
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
@@ -30,7 +30,7 @@ module Dri
|
|
30
30
|
assignees = failure["assignees"]
|
31
31
|
description = failure["description"]
|
32
32
|
|
33
|
-
related_mrs = @api_client.fetch_related_mrs(
|
33
|
+
related_mrs = @api_client.fetch_related_mrs(iid)
|
34
34
|
emoji = classify_failure_emoji(created_at)
|
35
35
|
emojified_link = "#{emoji} #{link}"
|
36
36
|
|
@@ -71,7 +71,7 @@ module Dri
|
|
71
71
|
'release' => '/quality/release'
|
72
72
|
}
|
73
73
|
|
74
|
-
failure_notes = @api_client.fetch_failure_notes(
|
74
|
+
failure_notes = @api_client.fetch_failure_notes(iid)
|
75
75
|
|
76
76
|
return if pipelines.empty?
|
77
77
|
|
@@ -83,10 +83,10 @@ module Dri
|
|
83
83
|
pipeline_markdown = pipeline.gsub(/.gitlab.com/, '')
|
84
84
|
|
85
85
|
failure_notes.each do |note|
|
86
|
-
next unless note
|
86
|
+
next unless note.body.include?(label_pipeline_map.fetch(pipeline))
|
87
87
|
|
88
88
|
pipeline_in_notes_found = true
|
89
|
-
pipeline_link = URI.extract(note
|
89
|
+
pipeline_link = URI.extract(note.body, %w[https])
|
90
90
|
end
|
91
91
|
|
92
92
|
unless pipeline_in_notes_found
|
@@ -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
|
98
|
+
pipeline_link_sanitized = pipeline_link.join.strip
|
99
99
|
pipeline_markdown = "[#{pipeline_markdown}](#{pipeline_link_sanitized})"
|
100
100
|
end
|
101
101
|
|
@@ -193,7 +193,7 @@ module Dri
|
|
193
193
|
path = "Failure in `#{path.strip}`" if path.delete_prefix!('Failure in ')
|
194
194
|
path = path.strip.gsub(' ', ' ') if desc
|
195
195
|
|
196
|
-
|
196
|
+
[path, desc].compact.join(' | ')
|
197
197
|
end
|
198
198
|
end
|
199
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
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.
|
4
|
+
version: 0.5.1
|
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-
|
11
|
+
date: 2022-07-04 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,11 @@ 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
|
-
- lib/dri/
|
340
|
+
- lib/dri/utils/feature_flag_consts.rb
|
311
341
|
- lib/dri/utils/markdown_lists.rb
|
312
342
|
- lib/dri/utils/table.rb
|
313
343
|
- lib/dri/version.rb
|
@@ -1 +0,0 @@
|
|
1
|
-
#
|