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,383 @@
1
+ require 'yaml'
2
+
3
+ require_relative '../api_clients/slack'
4
+ require_relative '../util/path_container'
5
+ require_relative '../util/shared_constants'
6
+ require_relative '../util/slack_constants'
7
+
8
+ # This class handles all slack related functionality
9
+ class SlackService
10
+ include SlackConstants
11
+
12
+ attr_reader :slack_client
13
+
14
+ def initialize(
15
+ api_key,
16
+ username_mapping_file_path = nil,
17
+ valid_email_suffixes = VALID_EMAIL_SUFFIXES,
18
+ is_dry_run: false
19
+ )
20
+ @slack_client = SlackClient.new(
21
+ api_key,
22
+ username_mapping_file_path ? PathContainer.new([username_mapping_file_path]) : nil,
23
+ valid_email_suffixes,
24
+ is_dry_run: is_dry_run
25
+ )
26
+ end
27
+
28
+ def post_new_rc_build_announcement(version_details:, rc_releases_link:, commit_title:, channels:)
29
+ version = version_details['version']
30
+ build_number = version_details['build_number']
31
+ formatted_version_details = "*#{version}* (#{build_number})"
32
+ pr_number = commit_title.scan(/\(#(\d+)\)/).flatten.first
33
+
34
+ message = "New *RC* build version #{formatted_version_details} is available on " \
35
+ "<#{rc_releases_link}|App Center>.\n"
36
+
37
+ message += if pr_number
38
+ "(PR <https://github.com/plangrid/build-mobile/pull/#{pr_number}/|##{pr_number}>) #{commit_title}"
39
+ else
40
+ "*Commit*: #{commit_title}"
41
+ end
42
+
43
+ send_message(
44
+ channel_strings: channels,
45
+ message: message
46
+ )
47
+ end
48
+
49
+ def post_headsup_announcement(channel_names, version, platform_name)
50
+ post_announcement(channel_names, version, platform_name, HEADSUP)
51
+ end
52
+
53
+ def post_release_announcement(channel_names, version, platform_name, pretext_addition: nil)
54
+ post_announcement(
55
+ channel_names,
56
+ version,
57
+ platform_name,
58
+ RELEASE,
59
+ pretext_addition: pretext_addition
60
+ )
61
+ end
62
+
63
+ # Post an announcement with the release notes.
64
+ def post_announcement(channel_names, version, platform_name, type, pretext_addition: nil)
65
+ pretext = format_pretext(version, platform_name, type, pretext_addition)
66
+ attachments = [{
67
+ pretext: pretext,
68
+ color: type == HEADSUP ? ATTACHMENT_COLOR_YELLOW : ATTACHMENT_COLOR_GOOD,
69
+ mrkdwn_in: %w[text pretext]
70
+ }]
71
+ channel_ids = @slack_client.unique_channel_ids(channel_names)
72
+ with_multiple_channels(channel_ids) do |channel_id|
73
+ @slack_client.update_or_send_message(
74
+ channel_id,
75
+ pretext,
76
+ attachments: attachments
77
+ )
78
+ end
79
+ end
80
+
81
+ def format_pretext(version, platform_name, type, pretext_addition)
82
+ case type
83
+ when HEADSUP
84
+ action = 'is coming soon'
85
+ when RELEASE
86
+ url_prefix = "https://jira.autodesk.com/issues/?jql=platform+%3D+#{platform_name}+AND+fixVersion+%3D+#{version}"
87
+ bugs_suffix = "%20AND%20(issuetype%20%3D%20Bug%20OR%20issuetype%20%3D%20'Escalated%20User%20Issue')"
88
+ features_suffix = "%20AND%20(issuetype%20!%3D%20Bug%20AND%20issuetype%20!%3D%20'Escalated%20User%20Issue')"
89
+
90
+ released_tickets_url = "\nRefer to the following links for the list of tickets for the version:" \
91
+ "\n• <#{url_prefix}#{features_suffix}|Features>" \
92
+ "\n• <#{url_prefix}#{bugs_suffix}|Bugs>"
93
+ action = 'has been released'
94
+ else
95
+ raise "Invalid announcement type: #{type}"
96
+ end
97
+
98
+ pretext = "#{platform_name} version #{version} #{action}! #{pretext_addition}".strip
99
+ emoji = case platform_name
100
+ when IOS
101
+ IOS_SLACK_EMOJI
102
+ when ANDROID
103
+ ANDROID_SLACK_EMOJI
104
+ when WINDOWS
105
+ WINDOWS_SLACK_EMOJI
106
+ else
107
+ ''
108
+ end
109
+ formated_text = "#{emoji} #{pretext} #{emoji}".strip
110
+ formated_text += released_tickets_url if released_tickets_url
111
+ formated_text
112
+ end
113
+
114
+ def user_hash(user_identifiers, error_if_not_found: false)
115
+ user_hash = {}
116
+ user_hash[@slack_client.team_domain] = fetch_user_ids(user_identifiers, error_if_not_found)
117
+ user_hash
118
+ end
119
+
120
+ def user_slack_tags(user_identifiers, error_if_not_found: false)
121
+ fetch_user_ids(user_identifiers, error_if_not_found)
122
+ .map { |user_id| "<@#{user_id}>" }
123
+ .join(' ')
124
+ end
125
+
126
+ # Send a build related slack message. The repo name is the full repo name. For instance: plangrid/plangrid
127
+ # message payload is a hash of key value pairs that will be transformed into slack attachment fields.
128
+ # It is intended that the message_payload be constructed with create_git_message_payload so that it
129
+ # contains the necessary fields.
130
+ def send_build_slack(
131
+ message,
132
+ channel_strings,
133
+ message_payload,
134
+ send_to_channels,
135
+ team,
136
+ build_initiator: nil,
137
+ success: true,
138
+ mention_on_failure: false
139
+ )
140
+ attachment = create_attachment(message, message_payload, success)
141
+ missed_user_ids = 0
142
+ message_recipient = build_initiator || message_payload['Git Author']
143
+
144
+ attachment_clone = attachment.clone
145
+ if send_to_channels
146
+ mention = ''
147
+ if mention_on_failure && !success
148
+ slack_user_ids = team.slack_ids(@slack_client.team_domain)
149
+ slack_user_ids += @slack_client.user_ids([message_recipient]).compact
150
+ mention = slack_user_ids.uniq.map { |slack_id| "<@#{slack_id}>" }.join(' ').strip
151
+ mention = '<!here>' if mention.empty?
152
+ end
153
+ attachment_clone['text'] = "#{mention} #{attachment_clone['text']}".strip
154
+ channel_ids = @slack_client.unique_channel_ids(channel_strings)
155
+ with_multiple_channels(channel_ids) do |channel_id|
156
+ @slack_client.send_message(
157
+ channel_name: channel_id,
158
+ attachments: [attachment_clone]
159
+ )
160
+ end
161
+ else
162
+ user_ids = @slack_client.user_ids([message_recipient]).compact
163
+ if user_ids.empty?
164
+ missed_user_ids += 1
165
+ else
166
+ @slack_client.send_message(
167
+ user_id: user_ids[0],
168
+ attachments: [attachment_clone]
169
+ )
170
+ end
171
+ end
172
+
173
+ return unless missed_user_ids != 0
174
+
175
+ mention_users = team.render_slack_ids(@slack_client.team_domain).join(' ').strip
176
+ mention_users = '<!here>' if mention_users.empty?
177
+ message = "#{mention_users} Could not send message to #{message_recipient}. " \
178
+ 'Ensure their commit author name or jenkins email address can be looked up in slack, ' \
179
+ 'their first and last name can be composed into an autodesk ' \
180
+ 'email address like first.last@autodesk.com, or that they have an entry in the ' \
181
+ '<https://github.com/plangrid/build-mobile/blob/dev/common/fastlane/slack_username_mapping.yml|' \
182
+ "slack_username_mapping.yml>.\n\n" \
183
+ "Please ping the slack user to run the following commands: \n" \
184
+ "`git config --global user.name \"Your Slack Name\"` \n" \
185
+ '`git config --global user.email "first.last@autodesk.com"`'
186
+ channel_ids = @slack_client.unique_channel_ids(channel_strings)
187
+ with_multiple_channels(channel_ids) do |channel_id|
188
+ slack_ids = team.slack_ids(@slack_client.team_domain).compact
189
+ @slack_client.invite_to_channel_if_needed(channel_id, slack_ids)
190
+ @slack_client.send_message(
191
+ channel_name: channel_id,
192
+ message: message,
193
+ attachments: [attachment]
194
+ )
195
+ end
196
+ end
197
+
198
+ def create_attachment(message, message_payload, success)
199
+ attachment = { 'text' => message }
200
+ attachment['fallback'] = message
201
+ attachment['fields'] = message_payload.map do |key, value|
202
+ {
203
+ 'title' => key,
204
+ 'value' => value,
205
+ 'short' => false
206
+ }
207
+ end
208
+ attachment['color'] = if success == WARNING
209
+ ATTACHMENT_COLOR_YELLOW
210
+ else
211
+ success ? ATTACHMENT_COLOR_GOOD : ATTACHMENT_COLOR_DANGER
212
+ end
213
+ attachment['mrkdwn_in'] = %w[text fields]
214
+ attachment
215
+ end
216
+
217
+ # Count test flakes and post a message to the desired channel.
218
+ def send_flake_message(send_channel_names, flake_channel_names, oldest_flake_time)
219
+ attachments = []
220
+ # Search for all channel names at once for efficiency. This primes the channel ID cache.
221
+ @slack_client.unique_channel_ids(flake_channel_names + send_channel_names)
222
+ flake_channel_ids = @slack_client.unique_channel_ids(flake_channel_names)
223
+ return if flake_channel_ids.empty?
224
+
225
+ raise 'Found more than one flake channel in a single slack workspace' if flake_channel_ids.size != 1
226
+
227
+ flake_channel_id = flake_channel_ids[0]
228
+ response_json = @slack_client.client.conversations_history(
229
+ channel: flake_channel_id,
230
+ oldest: oldest_flake_time,
231
+ limit: 500
232
+ )
233
+ error_message = 'Error getting slack channel history for channel id ' \
234
+ "#{flake_channel_id}: #{response_json['error']}"
235
+ raise exception.class, error_message unless response_json['ok']
236
+
237
+ all_flaking_tests = response_json['messages'].flat_map do |message_json|
238
+ next [] unless message_json['type'] == 'message'
239
+
240
+ next [] if message_json.fetch('attachments', []).empty?
241
+
242
+ attachment_text = message_json['attachments'][0].fetch('text', '')
243
+ next [] unless attachment_text.include?('Flaking tests:')
244
+
245
+ attachment_text.gsub('Flaking tests:', '').split("\n")[0].strip.split(',').map(&:strip)
246
+ end.compact
247
+
248
+ test_count_map = all_flaking_tests.uniq.to_h do |test_name|
249
+ [test_name.strip, all_flaking_tests.count(test_name)]
250
+ end
251
+ flake_message = test_count_map.keys.sort { |x, y| test_count_map[x] <=> test_count_map[y] }.reverse.map do |key|
252
+ "#{key}: #{test_count_map[key]}"
253
+ end.join("\n")
254
+ flake_message = "Total flakes: #{all_flaking_tests.size}\n#{flake_message}"
255
+ attachment = {
256
+ 'text' => "Happy Fix-it-Friday! Here are the test flake counts from the past week.\n\n#{flake_message}",
257
+ 'color' => ATTACHMENT_COLOR_YELLOW
258
+ }
259
+ attachments << attachment
260
+
261
+ send_channel_ids = @slack_client.unique_channel_ids(send_channel_names)
262
+ with_multiple_channels(send_channel_ids) do |channel_id|
263
+ attachments.each do |attachment_item|
264
+ @slack_client.send_message(channel_name: channel_id, attachments: [attachment_item])
265
+ end
266
+ end
267
+ end
268
+
269
+ # Iterate over all given channel strings (names or IDs) and call the given block. Only raise
270
+ # an exception if an exception is encountered running the given block over every channel string.
271
+ def with_multiple_channels(channel_strings, &_blk)
272
+ channel_exceptions = []
273
+ channel_strings.each do |channel_string|
274
+ yield channel_string
275
+ rescue StandardError => e
276
+ channel_exceptions << e
277
+ end
278
+ return unless channel_exceptions.size == channel_strings.size && channel_strings.size.positive?
279
+
280
+ error_message = channel_exceptions.map do |exception|
281
+ puts exception.message
282
+ puts exception.backtrace
283
+ exception.message
284
+ end.join("\n\n")
285
+ raise error_message
286
+ end
287
+
288
+ # Set the channel topic for all channel_strings (names or IDs) across all slack clients
289
+ def update_channel_topic(channel_strings, &_blk)
290
+ channel_ids = @slack_client.unique_channel_ids(channel_strings)
291
+ with_multiple_channels(channel_ids) do |channel_id|
292
+ yield @slack_client, channel_id
293
+ end
294
+ end
295
+
296
+ # Send a message to all channel_strings (names or ID's) or user_ids across all slack clients.
297
+ # Only raise an exception if every message sending attempt raises an exception.
298
+ def send_message(channel_strings: [], user_strings: [], message: nil, attachments: nil, icon_url: nil)
299
+ send_message_exceptions = []
300
+
301
+ channel_ids = @slack_client.unique_channel_ids(channel_strings)
302
+ begin
303
+ # send a message to all channel_strings
304
+ with_multiple_channels(channel_ids) do |channel_id|
305
+ @slack_client.send_message(
306
+ channel_name: channel_id,
307
+ message: message,
308
+ attachments: attachments,
309
+ icon_url: icon_url
310
+ )
311
+ end
312
+ rescue StandardError => e
313
+ send_message_exceptions << e
314
+ end
315
+
316
+ user_ids = @slack_client.user_ids(user_strings).compact
317
+ begin
318
+ with_multiple_channels(user_ids) do |user_id|
319
+ @slack_client.send_message(
320
+ user_id: user_id,
321
+ message: message,
322
+ attachments: attachments,
323
+ icon_url: icon_url
324
+ )
325
+ end
326
+ rescue StandardError => e
327
+ send_message_exceptions << e
328
+ end
329
+
330
+ return unless !send_message_exceptions.empty? &&
331
+ send_message_exceptions.size == (channel_ids.size + user_ids.size)
332
+
333
+ error_message = send_message_exceptions.map do |exception|
334
+ puts exception.backtrace
335
+ exception.message
336
+ end.join("\n\n")
337
+ raise exception.class, error_message
338
+ end
339
+
340
+ # Sends an initial message and makes it a thread for the additional messages formatted as Slack API blocks
341
+ def send_message_in_thread(channel_strings, initial_message, formatted_reply_blocks, slack_ids)
342
+ send_message_exceptions = []
343
+
344
+ channel_ids = @slack_client.unique_channel_ids(channel_strings)
345
+ begin
346
+ # send a message to all channel_strings
347
+ with_multiple_channels(channel_ids) do |channel_id|
348
+ begin
349
+ @slack_client.invite_to_channel_if_needed(channel_id, slack_ids.uniq)
350
+ rescue StandardError => e
351
+ puts "Error inviting tagged users, #{e.message}"
352
+ end
353
+ @slack_client.send_messages_in_thread(channel_id, initial_message, formatted_reply_blocks)
354
+ end
355
+ rescue StandardError => e
356
+ send_message_exceptions << e
357
+ end
358
+ return unless !send_message_exceptions.empty? && send_message_exceptions.size == (channel_ids.size)
359
+
360
+ error_message = send_message_exceptions.map do |exception|
361
+ puts exception.backtrace
362
+ exception.message
363
+ end.join("\n\n")
364
+ raise exception.class, error_message
365
+ end
366
+
367
+ def update_or_send_message(channel_names, message, historical_lookup_count: 100)
368
+ with_multiple_channels(channel_names) do |channel_id|
369
+ @slack_client.update_or_send_message(
370
+ channel_id,
371
+ message,
372
+ new_message: message,
373
+ historical_lookup_count: historical_lookup_count
374
+ )
375
+ end
376
+ end
377
+
378
+ private
379
+
380
+ def fetch_user_ids(user_identifiers, error_if_not_found)
381
+ @slack_client.user_ids(user_identifiers, error_if_not_found: error_if_not_found)
382
+ end
383
+ end
@@ -0,0 +1,134 @@
1
+ require_relative '../services/merge_driver_service'
2
+ require_relative 'shared_constants'
3
+
4
+ # Encapsulates the branch configurations that drive automerging.
5
+ class AutomergeConfiguration
6
+ include SharedConstants
7
+
8
+ def initialize(
9
+ automerge_map,
10
+ merge_driver_map,
11
+ git_merge_error_message_cleaner = nil,
12
+ cycle_check_initiation_type_allow_list = [INITIATION_TYPE_MANUAL]
13
+ )
14
+ @automerge_map = automerge_map
15
+ @merge_driver_service = MergeDriverService.new(merge_driver_map)
16
+ @git_merge_error_message_cleaner = git_merge_error_message_cleaner
17
+ validate_automerge_map(cycle_check_initiation_type_allow_list)
18
+ end
19
+
20
+ attr_reader :merge_driver_service
21
+
22
+ def validate_automerge_map(cycle_check_initiation_type_allow_list)
23
+ @automerge_map.each do |key, value|
24
+ target_branches = value.map { |target_branch_element| target_branch_element[:branch] }
25
+
26
+ error_message = "Automerge map contains duplicate target branches for source branch '#{key}'"
27
+ raise error_message unless target_branches == target_branches.uniq
28
+
29
+ size_with_initiation = value.reject { |target_branch_element| target_branch_element[:initiation_type].nil? }.size
30
+ initiation_type_error_message = 'Not all automerge map entries have initiation types'
31
+ raise initiation_type_error_message unless value.size == size_with_initiation
32
+ end
33
+
34
+ detect_cycles(@automerge_map, cycle_check_initiation_type_allow_list)
35
+ end
36
+
37
+ # Detect branch cycles anywhere in the automerge map. For each top level
38
+ # branch traverse all their child paths. Only follow branch paths that
39
+ # have the same initiation_types
40
+ def detect_cycles(automerge_map, cycle_check_initiation_type_allow_list)
41
+ automerge_map.each do |key, value|
42
+ initiation_types_covered = Set.new
43
+ value.each do |branch_target|
44
+ initiation_type = branch_target[:initiation_type]
45
+ next if initiation_types_covered.include?(initiation_type) ||
46
+ cycle_check_initiation_type_allow_list.include?(initiation_type)
47
+
48
+ cycle_detected_message = "Automerge map cycle detected for branch '#{key}' with " \
49
+ "initiation_type '#{initiation_type}'"
50
+ raise cycle_detected_message if cycle_detection_dfs(automerge_map, initiation_type, key)
51
+
52
+ initiation_types_covered.add(initiation_type)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Perform a DFS on the automerge map starting from a specified branch.
58
+ # Only follow branch paths that have the specified initiation_type
59
+ def cycle_detection_dfs(automerge_map, initiation_type, starting_branch)
60
+ seen = Set.new
61
+ stack = [starting_branch]
62
+ until stack.empty?
63
+ branch = stack.pop
64
+ return true if seen.include?(branch)
65
+
66
+ downstream_branch_targets = automerge_map[branch] || []
67
+ downstream_branch_targets.each do |branch_target|
68
+ stack << branch_target[:branch] if branch_target[:initiation_type] == initiation_type
69
+ end
70
+ seen.add(branch) unless downstream_branch_targets.empty?
71
+ end
72
+ false
73
+ end
74
+
75
+ # Check to see if the source/target branch combination specifies static_version == true
76
+ # in the automerge map.
77
+ def should_enforce_static_version?(source_branch, target_branch)
78
+ elements = @automerge_map.fetch(source_branch, []).select do |target_branch_element|
79
+ target_branch_element[:branch] == target_branch && target_branch_element[:static_version] == true
80
+ end
81
+ !elements.empty?
82
+ end
83
+
84
+ # Retrieve the merge_options for the given source/target branch combination from
85
+ # the automerge map.
86
+ def merge_options(source_branch, target_branch)
87
+ elements = @automerge_map.fetch(source_branch, []).select do |target_branch_element|
88
+ target_branch_element[:branch] == target_branch
89
+ end
90
+ return '' if elements.empty?
91
+
92
+ elements[0].fetch(:merge_options, '')
93
+ end
94
+
95
+ def target_branches(branch_name, initiation_type)
96
+ target_branch_list = @automerge_map[branch_name]
97
+ return [] if target_branch_list.nil?
98
+
99
+ target_branch_list.select do |target_branch_element|
100
+ target_branch_element[:initiation_type] == initiation_type
101
+ end.map do |target_branch_element|
102
+ target_branch_element[:branch]
103
+ end
104
+ end
105
+
106
+ def clean_merge_error_message(error_message)
107
+ if @git_merge_error_message_cleaner.nil?
108
+ error_message
109
+ else
110
+ @git_merge_error_message_cleaner.clean_message(error_message)
111
+ end
112
+ end
113
+
114
+ # Replace slashes in branch names with underscores to makes automerge/ branches singly nested.
115
+ # Otherwise there is potential that creation of automerge branches would fail due to conflicting
116
+ # naming because branches are stored in git as file system paths and path segments can only
117
+ # be directories or files and not both. For example: if a branch named my_branch exists then
118
+ # attempting to create my_branch/fix will fail.
119
+ def generate_automerge_branch_name(source_branch, target_branch)
120
+ "#{AUTOMERGE_BRANCH_PREFIX}#{source_branch.tr('/', '_')}_to_#{target_branch.tr('/', '_')}"
121
+ end
122
+
123
+ def original_source_branch(target_branch, automerge_branch_name)
124
+ @automerge_map.each do |branch_key, target_branch_list|
125
+ includes_target_branch = target_branch_list.map do |target_branch_element|
126
+ target_branch_element[:branch]
127
+ end.include?(target_branch)
128
+ if includes_target_branch && generate_automerge_branch_name(branch_key, target_branch) == automerge_branch_name
129
+ return branch_key
130
+ end
131
+ end
132
+ nil
133
+ end
134
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'shell_helper'
2
+
3
+ # module for working with bundle commands
4
+ module Bundler
5
+ def self.bundle(arg_string)
6
+ ShellHelper.run("bundle #{arg_string}")
7
+ end
8
+
9
+ def self.install(arg_string)
10
+ bundle("install #{arg_string}")
11
+ end
12
+
13
+ def self.safe_install(arg_string)
14
+ install(arg_string)
15
+ rescue StandardError => e
16
+ e.message
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_support/core_ext/time'
2
+ require 'date'
3
+ require 'time'
4
+
5
+ # Module for help datetime related functions
6
+ module DateTimeHelper
7
+ # Finds a tuesday some number of weeks into the future and returns a Time
8
+ # on that tuesday at the given hour of day. If this function is called on
9
+ # a tuesday that current tuesday will not be counted.
10
+ def self.future_tuesday(number_of_weeks_in_future, hour_of_day = 17)
11
+ future_tuesday = Date.parse('Tuesday') + (number_of_weeks_in_future * 7)
12
+ Time.parse(future_tuesday.to_s) + (hour_of_day * 3600)
13
+ end
14
+
15
+ # returns a time on the optionally given date at the hour specified
16
+ # in the optionally specified time zone
17
+ def self.time_at_hour(hour, time_zone: nil, date: Date.today)
18
+ time = Time.utc(date.year, date.month, date.day, hour, 0, 0)
19
+ time = time.in_time_zone(time_zone) if time_zone
20
+ utc_offset = time.utc_offset
21
+ time - utc_offset
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'git'
2
+
3
+ # Module for finding and manipulating file content
4
+ module FileContent
5
+ def self.find_content_in_file(file_path, regex, git_ref = nil)
6
+ file_text = Git.file_content(file_path, git_ref)
7
+ file_text.scan(regex).flatten
8
+ end
9
+
10
+ def self.find_and_replace_content_in_file(file_path, regex, replacement_string)
11
+ file_text = Git.file_content(file_path)
12
+ new_file_text = file_text.gsub(regex, replacement_string)
13
+ File.open(File.join(Git.repo_root_path, file_path), 'w') { |file| file.puts new_file_text }
14
+ end
15
+ end