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,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
|