standard_automation_library 0.2.1.pre.temp
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 +7 -0
- data/lib/standard_automation_library/api_clients/app_center.rb +104 -0
- data/lib/standard_automation_library/api_clients/bugsnag.rb +94 -0
- data/lib/standard_automation_library/api_clients/github.rb +371 -0
- data/lib/standard_automation_library/api_clients/jira.rb +326 -0
- data/lib/standard_automation_library/api_clients/pagerduty.rb +101 -0
- data/lib/standard_automation_library/api_clients/slack.rb +499 -0
- data/lib/standard_automation_library/danger/danger_jira.rb +169 -0
- data/lib/standard_automation_library/errors/slack_api_error.rb +6 -0
- data/lib/standard_automation_library/personnel/release_management_team.rb +85 -0
- data/lib/standard_automation_library/personnel/team.rb +41 -0
- data/lib/standard_automation_library/personnel/user.rb +68 -0
- data/lib/standard_automation_library/services/bugsnag_service.rb +251 -0
- data/lib/standard_automation_library/services/jira_service.rb +64 -0
- data/lib/standard_automation_library/services/merge_driver_service.rb +48 -0
- data/lib/standard_automation_library/services/mobile_tech_debt_logging_service.rb +176 -0
- data/lib/standard_automation_library/services/monorepo_platform_service.rb +18 -0
- data/lib/standard_automation_library/services/perf_tracker_logging_service.rb +87 -0
- data/lib/standard_automation_library/services/platform_service.rb +34 -0
- data/lib/standard_automation_library/services/repo_service.rb +17 -0
- data/lib/standard_automation_library/services/slack_service.rb +383 -0
- data/lib/standard_automation_library/util/automerge_configuration.rb +134 -0
- data/lib/standard_automation_library/util/bundler.rb +18 -0
- data/lib/standard_automation_library/util/datetime_helper.rb +23 -0
- data/lib/standard_automation_library/util/file_content.rb +15 -0
- data/lib/standard_automation_library/util/git.rb +235 -0
- data/lib/standard_automation_library/util/git_merge_error_message_cleaner.rb +27 -0
- data/lib/standard_automation_library/util/network.rb +39 -0
- data/lib/standard_automation_library/util/path_container.rb +17 -0
- data/lib/standard_automation_library/util/platform_picker.rb +150 -0
- data/lib/standard_automation_library/util/shared_constants.rb +27 -0
- data/lib/standard_automation_library/util/shell_helper.rb +54 -0
- data/lib/standard_automation_library/util/slack_constants.rb +40 -0
- data/lib/standard_automation_library/util/version.rb +31 -0
- data/lib/standard_automation_library/version.rb +5 -0
- data/lib/standard_automation_library.rb +8 -0
- metadata +296 -0
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'jira-ruby'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
# Wrapper around the jira-ruby gem https://github.com/sumoheavy/jira-ruby
|
5
|
+
class Jira
|
6
|
+
def initialize(username, api_key, site: 'https://jira.autodesk.com', is_dry_run: false)
|
7
|
+
options = {
|
8
|
+
site: site,
|
9
|
+
username: username,
|
10
|
+
password: api_key,
|
11
|
+
context_path: '',
|
12
|
+
auth_type: :basic
|
13
|
+
}
|
14
|
+
@client = JIRA::Client.new(options)
|
15
|
+
@is_dry_run = is_dry_run
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :client
|
19
|
+
|
20
|
+
# Get a JiraIssue representing the supplied issue key
|
21
|
+
# Will be nil if the issue does not exist or the credentials are wrong.
|
22
|
+
def issue(issue_key)
|
23
|
+
issue = @client.Issue.find(issue_key)
|
24
|
+
JiraIssue.new(@client, issue, is_dry_run: @is_dry_run)
|
25
|
+
rescue JIRA::HTTPError => e
|
26
|
+
puts "Error getting Jira issue: #{e.response}"
|
27
|
+
nil
|
28
|
+
rescue StandardError => e
|
29
|
+
puts "Error getting Jira project keys: #{e}"
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get a JiraProject representing the supplied project key
|
34
|
+
# Will be nil if the project does not exist or the credentials are wrong.
|
35
|
+
def project(project_key)
|
36
|
+
project = @client.Project.find(project_key)
|
37
|
+
JiraProject.new(@client, project, is_dry_run: @is_dry_run)
|
38
|
+
rescue JIRA::HTTPError => e
|
39
|
+
puts "Error getting Jira project: #{e.response}"
|
40
|
+
nil
|
41
|
+
rescue StandardError => e
|
42
|
+
puts "Error getting Jira project keys: #{e}"
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def all_project_keys
|
47
|
+
Set.new(@client.Project.all.map(&:key))
|
48
|
+
rescue JIRA::HTTPError => e
|
49
|
+
puts "Error getting Jira project keys: #{e.response}"
|
50
|
+
Set.new
|
51
|
+
rescue StandardError => e
|
52
|
+
puts "Error getting Jira project keys: #{e}"
|
53
|
+
Set.new
|
54
|
+
end
|
55
|
+
|
56
|
+
# https://github.com/sumoheavy/jira-ruby/blob/master/example.rb
|
57
|
+
def all_unverified_tickets(fix_version, platform = nil)
|
58
|
+
jql = all_unverified_tickets_jql(fix_version, platform)
|
59
|
+
# Specify a hardcoded key for local testing purposes
|
60
|
+
# jql = 'key = SCMP-4638'
|
61
|
+
@client.Issue.jql(
|
62
|
+
jql,
|
63
|
+
# rubocop:disable Naming/VariableNumber
|
64
|
+
fields: %i[key assignee summary reporter customfield_11882], # customfield_11882 = 'QA Owner'
|
65
|
+
# rubocop:enable Naming/VariableNumber
|
66
|
+
max_results: 50 # More than 50 tickets points to a programming or process problem. Let's avoid the spam
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
# The JQL is inspired by the QA Kanbans for iOS & Android
|
71
|
+
# https://jira.autodesk.com/secure/RapidBoard.jspa?rapidView=12177
|
72
|
+
# https://jira.autodesk.com/secure/RapidBoard.jspa?rapidView=12175
|
73
|
+
def all_unverified_tickets_jql(fix_version, platform = nil)
|
74
|
+
product_family = '("Product Family" in (PlanGrid, "Autodesk Build", "ACC Platform") OR "Product Family" = EMPTY)'
|
75
|
+
fix_version = "fixVersion = #{fix_version}"
|
76
|
+
verified_statuses = [
|
77
|
+
'Closed',
|
78
|
+
'Done',
|
79
|
+
"Won't Do",
|
80
|
+
'Ready for Launch',
|
81
|
+
'Live',
|
82
|
+
'Signed off',
|
83
|
+
'Resolved',
|
84
|
+
'Ready For Prod',
|
85
|
+
'Ready for staging',
|
86
|
+
'In staging',
|
87
|
+
'In prod',
|
88
|
+
'Ready for Engineering',
|
89
|
+
'Remediation done',
|
90
|
+
'Waiting for Deployment',
|
91
|
+
'Waiting for Deploy',
|
92
|
+
'Awaiting Deployment',
|
93
|
+
'Ready for Deploy'
|
94
|
+
]
|
95
|
+
.map { |status| "\"#{status}\"" }
|
96
|
+
.join(', ')
|
97
|
+
unverified = "status not in (#{verified_statuses})"
|
98
|
+
jql = "#{product_family} AND #{fix_version} AND #{unverified}"
|
99
|
+
|
100
|
+
# Common Mobile is added independent of the platform in case of platform specification
|
101
|
+
jql += " AND Platform IN ('Common Mobile', '#{platform}')" if platform
|
102
|
+
|
103
|
+
puts "JQL: #{jql}"
|
104
|
+
jql
|
105
|
+
end
|
106
|
+
|
107
|
+
def unverified_ticket_comment(assignee, fix_version, release_date)
|
108
|
+
assignee_at_mention = "[~#{assignee.name}]"
|
109
|
+
"Hello #{assignee_at_mention}, the #{fix_version} RC is currently being verified with a scheduled " \
|
110
|
+
"release date of #{release_date}. This ticket is not yet verified and will block the release. Please " \
|
111
|
+
'complete and verify the ticket if it is part of the release. If the functionality is behind a feature ' \
|
112
|
+
end
|
113
|
+
|
114
|
+
def comment_on_all_unverified_tickets_for_version(fix_version, release_date)
|
115
|
+
unverified_tickets = all_unverified_tickets(fix_version)
|
116
|
+
if @is_dry_run
|
117
|
+
puts "Would have commented on the following #{unverified_tickets.size} unverified tickets:"
|
118
|
+
unverified_tickets.each do |issue|
|
119
|
+
ticket_url = "https://jira.autodesk.com/browse/#{issue.key}\n"
|
120
|
+
comment = unverified_ticket_comment(issue.assignee, fix_version, release_date)
|
121
|
+
puts ticket_url
|
122
|
+
puts comment
|
123
|
+
end
|
124
|
+
else
|
125
|
+
puts "Commenting on the following #{unverified_tickets.size} unverified tickets:"
|
126
|
+
unverified_tickets.each do |issue|
|
127
|
+
ticket_url = "https://jira.autodesk.com/browse/#{issue.key}\n"
|
128
|
+
comment = unverified_ticket_comment(issue.assignee, fix_version, release_date)
|
129
|
+
puts ticket_url
|
130
|
+
puts comment
|
131
|
+
issue.comments.build.save(body: comment)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Wrapper around the jira-ruby gem's issue api. Represents a set of issue related convenience methods.
|
138
|
+
# Object represents the state of the issue at the time of the object's creation.
|
139
|
+
class JiraIssue
|
140
|
+
CREATEMETA_V2_ENDPOINT = '/issue/createmeta/'.freeze
|
141
|
+
|
142
|
+
def initialize(client, issue, is_dry_run: false)
|
143
|
+
@client = client
|
144
|
+
@issue = issue
|
145
|
+
@is_dry_run = is_dry_run
|
146
|
+
end
|
147
|
+
|
148
|
+
def api_issue
|
149
|
+
@issue
|
150
|
+
end
|
151
|
+
|
152
|
+
def key
|
153
|
+
@issue.key
|
154
|
+
end
|
155
|
+
|
156
|
+
def status
|
157
|
+
@issue.fields['status']['name']
|
158
|
+
end
|
159
|
+
|
160
|
+
def project_key
|
161
|
+
@issue.project.key
|
162
|
+
end
|
163
|
+
|
164
|
+
def fix_versions
|
165
|
+
@issue.fields['fixVersions'].map { |fix_version| fix_version['name'] }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns a boolean indicating success of comment addition
|
169
|
+
def add_comment(comment_text)
|
170
|
+
if @is_dry_run
|
171
|
+
puts "Would have added comment '#{comment_text}' to issue '#{key}'"
|
172
|
+
true
|
173
|
+
else
|
174
|
+
@issue.comments.build.save(body: comment_text)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def contains_comment?(comment_text)
|
179
|
+
@issue.comments.each do |comment|
|
180
|
+
return true if comment.body.include?(comment_text)
|
181
|
+
end
|
182
|
+
false
|
183
|
+
end
|
184
|
+
|
185
|
+
def fix_version?(specific_version = nil)
|
186
|
+
if specific_version
|
187
|
+
!fix_versions.find { |fix_version| fix_version == specific_version }.nil?
|
188
|
+
else
|
189
|
+
!fix_versions.empty?
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns a boolean indicating success of fix version addition
|
194
|
+
def add_fix_version(fix_version)
|
195
|
+
if @is_dry_run
|
196
|
+
puts "Would have added fix version: #{fix_version} to issue #{key}"
|
197
|
+
true
|
198
|
+
else
|
199
|
+
begin
|
200
|
+
@issue.save!(update: { fixVersions: [add: { name: fix_version }] })
|
201
|
+
@issue.fetch(true)
|
202
|
+
true
|
203
|
+
rescue JIRA::HTTPError => e
|
204
|
+
puts "Error adding fix version to issue #{key}: #{e.response}"
|
205
|
+
false
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get the value of the custom field if the field exists and it has a value. Nil otherwise.
|
211
|
+
def custom_field_data(field_name)
|
212
|
+
field_array = metadata_field_array
|
213
|
+
field_index = custom_field_index(field_array, field_name)
|
214
|
+
|
215
|
+
return unless field_index
|
216
|
+
|
217
|
+
@issue.fields[metadata_field_array[field_index]['fieldId']]
|
218
|
+
end
|
219
|
+
|
220
|
+
# Set the custom field for the issue to the given value.
|
221
|
+
# Will return true if successful or throw an error explaining why the given
|
222
|
+
# value cannot be set. Only supports 'array' and 'option' type fields.
|
223
|
+
def update_custom_field(field_name, value)
|
224
|
+
field_array = metadata_field_array
|
225
|
+
field_index = custom_field_index(field_array, field_name)
|
226
|
+
|
227
|
+
raise "Cannot modify '#{field_name}' field for JIRA issue #{@issue.key}. No such field exists." unless field_index
|
228
|
+
|
229
|
+
platform_not_allowed = field_array[field_index]['allowedValues'].find do |allowed_value|
|
230
|
+
allowed_value['value'] == value
|
231
|
+
end.nil?
|
232
|
+
if platform_not_allowed
|
233
|
+
raise "Cannot modify '#{field_name}' field for JIRA issue #{@issue.key}. " \
|
234
|
+
"Supplied value '#{value}' is not an allowed value."
|
235
|
+
end
|
236
|
+
|
237
|
+
if @is_dry_run
|
238
|
+
puts "Would have updated issue '#{key}' with #{field_name}: #{value}"
|
239
|
+
true
|
240
|
+
else
|
241
|
+
schema_type = field_array[field_index]['schema']['type']
|
242
|
+
operation = :set
|
243
|
+
operation = :add if schema_type == 'array'
|
244
|
+
puts "Operation #{operation}, Value: #{value}"
|
245
|
+
@issue.save(update: { field_array[field_index]['fieldId'] => [operation => { value: value }] })
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Workaround for https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
|
250
|
+
# Which is not supported by the jira-ruby gem yet.
|
251
|
+
def metadata_field_array
|
252
|
+
params = {
|
253
|
+
maxResults: 100,
|
254
|
+
expand: 'projects.issuetypes.fields'
|
255
|
+
}
|
256
|
+
project_key = @issue.project.key
|
257
|
+
issue_type_id = @issue.issuetype.id
|
258
|
+
endpoint_path = "#{CREATEMETA_V2_ENDPOINT}#{project_key}/issuetypes/#{issue_type_id}"
|
259
|
+
query_params = JIRA::Base.hash_to_query_string(params)
|
260
|
+
create_meta_url = "#{@client.options[:rest_base_path]}#{endpoint_path}?#{query_params}"
|
261
|
+
|
262
|
+
response = @client.get(create_meta_url)
|
263
|
+
json = JIRA::Base.parse_json(response.body)
|
264
|
+
fields = json['values']
|
265
|
+
|
266
|
+
raise "JIRA metadata not found for project #{project_key} and issuetype #{@issue.issuetype.name}." unless fields
|
267
|
+
|
268
|
+
fields
|
269
|
+
end
|
270
|
+
|
271
|
+
def custom_field_index(field_array, field_name)
|
272
|
+
# Assume there's only 1 field with the given name. If not then there's a much bigger problem.
|
273
|
+
field_array.find_index { |dict| dict['name'] == field_name }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Wrapper around the jira-ruby gem's project api. Represents a set of project related convenience methods.
|
278
|
+
# Object represents the state of the project at the time of the object's creation.
|
279
|
+
class JiraProject
|
280
|
+
def initialize(client, project, is_dry_run: false)
|
281
|
+
@client = client
|
282
|
+
@project = project
|
283
|
+
@is_dry_run = is_dry_run
|
284
|
+
end
|
285
|
+
|
286
|
+
def api_project
|
287
|
+
@project
|
288
|
+
end
|
289
|
+
|
290
|
+
def fix_version?(specific_version = nil)
|
291
|
+
if specific_version
|
292
|
+
!@project.versions.find { |fix_version| fix_version.name == specific_version }.nil?
|
293
|
+
else
|
294
|
+
!@project.versions.empty?
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def key
|
299
|
+
@project.key
|
300
|
+
end
|
301
|
+
|
302
|
+
def style
|
303
|
+
@project.style
|
304
|
+
rescue StandardError
|
305
|
+
# JIRA instances that do not have a concept of project style will throw an error
|
306
|
+
nil
|
307
|
+
end
|
308
|
+
|
309
|
+
# Requires that the api_key have admin privileges for the project.
|
310
|
+
# Returns a boolean indicating the success of the version creation.
|
311
|
+
def add_fix_version(fix_version)
|
312
|
+
if @is_dry_run
|
313
|
+
puts "Would have created fix version: #{fix_version} in project: #{key}"
|
314
|
+
true
|
315
|
+
else
|
316
|
+
begin
|
317
|
+
@client.Version.build.save!(name: fix_version, projectId: @project.id)
|
318
|
+
@project.fetch(true)
|
319
|
+
true
|
320
|
+
rescue JIRA::HTTPError => e
|
321
|
+
puts "Error adding fix version to project: #{e.response}"
|
322
|
+
false
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'pager_duty-connection'
|
2
|
+
|
3
|
+
require_relative '../util/datetime_helper'
|
4
|
+
|
5
|
+
# Client for accessing PagerDuty
|
6
|
+
class PagerDutyClient
|
7
|
+
def initialize(api_key)
|
8
|
+
@client = PagerDuty::Connection.new(api_key)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :client
|
12
|
+
|
13
|
+
# Find out who is on duty for the given schedule id. This method defaults to
|
14
|
+
# who is on duty at the moment this function runs but it can be made to
|
15
|
+
# query for any time by passing in an optional Time argument. The cut off hour
|
16
|
+
# is the hour of the day in the specified time zone at which duty switches
|
17
|
+
# from one person to another.
|
18
|
+
#
|
19
|
+
# This method assumes that the schedule contains non contiguous time blocks
|
20
|
+
# that occur predictably and that though those time blocks do not fill up
|
21
|
+
# an entire day the person whose name is in the time block for that day is
|
22
|
+
# on duty for the entire 24 hour period starting at the cut off hour. This
|
23
|
+
# method also assumes that when duty changes from one person to another
|
24
|
+
# it does so in a predictable way.
|
25
|
+
def who_is_on_duty(schedule_id, cut_off_hour, time_zone: nil, time: nil)
|
26
|
+
time_at_hour = DateTimeHelper.time_at_hour(
|
27
|
+
cut_off_hour,
|
28
|
+
time_zone: time_zone,
|
29
|
+
date: time ? Date.parse(time.to_s) : Date.today
|
30
|
+
)
|
31
|
+
|
32
|
+
# TODO: consider refactor now that Sorbet has been removed from the codebase
|
33
|
+
# This implementation exists because the sorbet sig for in_time_zone requires a String, not a T.nilable(String)
|
34
|
+
if time
|
35
|
+
time_now = time
|
36
|
+
else
|
37
|
+
time_now = Time.now
|
38
|
+
time_now = time_now.in_time_zone(time_zone) if time_zone
|
39
|
+
end
|
40
|
+
|
41
|
+
if time_now < time_at_hour
|
42
|
+
start_time = time_at_hour - (24 * 3600)
|
43
|
+
end_time = time_at_hour
|
44
|
+
else
|
45
|
+
start_time = time_at_hour
|
46
|
+
end_time = time_at_hour + (24 * 3600)
|
47
|
+
end
|
48
|
+
|
49
|
+
return nil if (schedule_response = schedule(schedule_id, start_time, end_time, time_zone)).nil?
|
50
|
+
|
51
|
+
user_name_from_schedule_response(schedule_response)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Find out who is on duty right now for the given schedule id.
|
55
|
+
# The schedule will either have a name associated with that specific time
|
56
|
+
# or there will be no one in which case this method will return nil.
|
57
|
+
def who_is_on_duty_now(schedule_id, time: Time.now)
|
58
|
+
schedule_response = schedule(schedule_id, time, time, nil)
|
59
|
+
user_name_from_schedule_response(schedule_response)
|
60
|
+
end
|
61
|
+
|
62
|
+
def user_name_from_schedule_response(schedule_response)
|
63
|
+
schedule_entries = schedule_response['schedule']['final_schedule']['rendered_schedule_entries']
|
64
|
+
return nil if schedule_entries.empty?
|
65
|
+
|
66
|
+
user_id = schedule_entries[0]['user']['id']
|
67
|
+
user_name(user_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def schedule(schedule_id, start_time, end_time, time_zone)
|
71
|
+
@client.get(
|
72
|
+
"schedules/#{schedule_id}",
|
73
|
+
query_params: {
|
74
|
+
since: start_time.to_s,
|
75
|
+
until: end_time.to_s,
|
76
|
+
time_zone: time_zone
|
77
|
+
}
|
78
|
+
)
|
79
|
+
rescue PagerDuty::Connection::FileNotFoundError
|
80
|
+
raise "No PagerDuty schedule found with schedule id #{schedule_id}"
|
81
|
+
rescue JSON::ParserError => e
|
82
|
+
raise "Error parsing PagerDuty API response: #{e.message}"
|
83
|
+
rescue StandardError => e
|
84
|
+
raise "Error fetching PagerDuty schedule data: #{e.message}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def user_name(user_id)
|
88
|
+
user_response = @client.get("users/#{user_id}", {})
|
89
|
+
user_response['user']['name']
|
90
|
+
rescue PagerDuty::Connection::FileNotFoundError
|
91
|
+
raise "No PagerDuty user found with user id #{user_id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def schedule_id(schedule_name)
|
95
|
+
schedule_response = @client.get('schedules', query_params: { query: schedule_name })
|
96
|
+
schedules = schedule_response['schedules']
|
97
|
+
raise "No PagerDuty schedule found with name #{schedule_name}" if schedules.empty?
|
98
|
+
|
99
|
+
schedules[0]['id']
|
100
|
+
end
|
101
|
+
end
|