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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/lib/standard_automation_library/api_clients/app_center.rb +104 -0
  3. data/lib/standard_automation_library/api_clients/bugsnag.rb +94 -0
  4. data/lib/standard_automation_library/api_clients/github.rb +371 -0
  5. data/lib/standard_automation_library/api_clients/jira.rb +326 -0
  6. data/lib/standard_automation_library/api_clients/pagerduty.rb +101 -0
  7. data/lib/standard_automation_library/api_clients/slack.rb +499 -0
  8. data/lib/standard_automation_library/danger/danger_jira.rb +169 -0
  9. data/lib/standard_automation_library/errors/slack_api_error.rb +6 -0
  10. data/lib/standard_automation_library/personnel/release_management_team.rb +85 -0
  11. data/lib/standard_automation_library/personnel/team.rb +41 -0
  12. data/lib/standard_automation_library/personnel/user.rb +68 -0
  13. data/lib/standard_automation_library/services/bugsnag_service.rb +251 -0
  14. data/lib/standard_automation_library/services/jira_service.rb +64 -0
  15. data/lib/standard_automation_library/services/merge_driver_service.rb +48 -0
  16. data/lib/standard_automation_library/services/mobile_tech_debt_logging_service.rb +176 -0
  17. data/lib/standard_automation_library/services/monorepo_platform_service.rb +18 -0
  18. data/lib/standard_automation_library/services/perf_tracker_logging_service.rb +87 -0
  19. data/lib/standard_automation_library/services/platform_service.rb +34 -0
  20. data/lib/standard_automation_library/services/repo_service.rb +17 -0
  21. data/lib/standard_automation_library/services/slack_service.rb +383 -0
  22. data/lib/standard_automation_library/util/automerge_configuration.rb +134 -0
  23. data/lib/standard_automation_library/util/bundler.rb +18 -0
  24. data/lib/standard_automation_library/util/datetime_helper.rb +23 -0
  25. data/lib/standard_automation_library/util/file_content.rb +15 -0
  26. data/lib/standard_automation_library/util/git.rb +235 -0
  27. data/lib/standard_automation_library/util/git_merge_error_message_cleaner.rb +27 -0
  28. data/lib/standard_automation_library/util/network.rb +39 -0
  29. data/lib/standard_automation_library/util/path_container.rb +17 -0
  30. data/lib/standard_automation_library/util/platform_picker.rb +150 -0
  31. data/lib/standard_automation_library/util/shared_constants.rb +27 -0
  32. data/lib/standard_automation_library/util/shell_helper.rb +54 -0
  33. data/lib/standard_automation_library/util/slack_constants.rb +40 -0
  34. data/lib/standard_automation_library/util/version.rb +31 -0
  35. data/lib/standard_automation_library/version.rb +5 -0
  36. data/lib/standard_automation_library.rb +8 -0
  37. 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