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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7109f4282fc5a8de77dde35d046bd9897b6ebbb1b9dd6db5fce801c9c28777fe
4
+ data.tar.gz: a7f2358fd522aa99a59e8888af8d15b8477f69e61e1e1f5468a453828f3654f9
5
+ SHA512:
6
+ metadata.gz: cf0d61a48eb75c84e776792a762c5bd1b8f3e4c7049a858cb7b82e007d2f1e1d9a08a6377af507d09dd39a6e08892b1d10c3dd4f67d9d859924d0a9aa402b9bd
7
+ data.tar.gz: f586cd1dd604c05fc56f01dd6583ce8ca39db68e2bb92506d9dfca7cecac386d4dac1ae0182cf36465821c344489d83f7f769b4a4fdcf378a4aa9b6e829414f0
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+
4
+ require_relative '../util/network'
5
+
6
+ # App Center api client
7
+ # See https://openapi.appcenter.ms for API reference.
8
+ class AppCenter
9
+ # @param api_key [String] App Center API key
10
+ def initialize(api_key, base_url = 'https://api.appcenter.ms/v0.1', is_dry_run: false)
11
+ @api_key = api_key
12
+ @base_url = base_url
13
+ @is_dry_run = is_dry_run
14
+ end
15
+
16
+ # User must already be a member of the organization to be added to a team.
17
+ def assign_to_team(org_name, team_name, email_address)
18
+ if @is_dry_run
19
+ puts "Would have assigned #{email_address} to App Center team '#{team_name}' in org '#{org_name}'"
20
+ return
21
+ end
22
+ response = Network.post(
23
+ "#{@base_url}/orgs/#{org_name}/teams/#{team_name}/users",
24
+ {
25
+ 'Content-Type' => 'application/json',
26
+ 'X-API-Token' => @api_key
27
+ },
28
+ { 'user_email' => email_address }.to_json
29
+ )
30
+
31
+ # raise an error unless the response is a 201 meaning the user was added to the team
32
+ # or a 409 meaning that the user is already a member of the team.
33
+ raise "#{response.code}: #{response.body}" unless %w[201 409].include?(response.code)
34
+ end
35
+
36
+ def get_release_information(owner_name, app_name, version: nil, version_code: nil)
37
+ # first get the specifc build that matches the version code criteria if supplied
38
+ # secondly get the most recent release that matches the version criteria if supplied
39
+ # otherwise just use the most recent release.
40
+ releases_json_response = Network.json_response(
41
+ "#{@base_url}/apps/#{owner_name}/#{app_name}/releases",
42
+ 'X-API-Token' => @api_key
43
+ )
44
+
45
+ release_json = if version_code
46
+ releases_json_response.find { |item| item['version'] == version_code }
47
+ elsif version.nil?
48
+ releases_json_response.first
49
+ else
50
+ releases_json_response.select { |item| item['short_version'] == version }
51
+ .max_by { |item| item['version'] }
52
+ end
53
+
54
+ error_message = "No release for owner_name: #{owner_name}, app_name: #{app_name}, " \
55
+ "version: #{version} and version code: #{version_code}"
56
+ raise error_message if release_json.nil?
57
+
58
+ # next, use the release id to query for specific information about that release
59
+ Network.json_response(
60
+ "#{@base_url}/apps/#{owner_name}/#{app_name}/releases/#{release_json['id']}",
61
+ 'X-API-Token' => @api_key
62
+ )
63
+ end
64
+
65
+ # Download the the most recent application file matching the given owner_name, app_name, and optional (short) version
66
+ # Return the full version of the downloaded app. That means a 4 part version for iOS or the version code for android.
67
+ def download_latest_build(owner_name, app_name, output_path, version: nil)
68
+ specific_release_json = get_release_information(owner_name, app_name, version: version)
69
+
70
+ Network.download_file(specific_release_json['download_url'], output_path)
71
+
72
+ # return the full version of the downloaded app
73
+ specific_release_json['version']
74
+ end
75
+
76
+ def download_specific_build(owner_name, app_name, output_path, version_code)
77
+ specific_release_json = get_release_information(owner_name, app_name, version_code: version_code)
78
+
79
+ Network.download_file(specific_release_json['download_url'], output_path)
80
+
81
+ # return the version name of the downloaded app
82
+ specific_release_json['short_version']
83
+ end
84
+
85
+ def send_invitation(org_name, email_address)
86
+ if @is_dry_run
87
+ puts "Would have sent App Center invitation to #{email_address} for org '#{org_name}'"
88
+ return
89
+ end
90
+ response = Network.post(
91
+ "#{@base_url}/orgs/#{org_name}/invitations",
92
+ {
93
+ 'Content-Type' => 'application/json',
94
+ 'X-API-Token' => @api_key
95
+ },
96
+ { 'user_email' => email_address, 'role' => 'member' }.to_json
97
+ )
98
+
99
+ # raise an error unless the response is a 204 meaning the invitation was sent or
100
+ # a 409 meaning that the user has already been invited or is already part of the
101
+ # organization.
102
+ raise "#{response.code}: #{response.body}" unless %w[204 409].include?(response.code)
103
+ end
104
+ end
@@ -0,0 +1,94 @@
1
+ require 'bugsnag/api'
2
+
3
+ # Service for connecting to bugsnag API
4
+ class BugsnagClient
5
+ def initialize(platform, auth_token)
6
+ @platform = platform
7
+ @client = Bugsnag::Api::Client.new(auth_token: auth_token)
8
+ end
9
+
10
+ def my_organization
11
+ @client.organizations[0]
12
+ end
13
+
14
+ def my_projects
15
+ @client.projects(my_organization.id, { q: @platform }) ## Bugsnag projects
16
+ end
17
+
18
+ def my_project
19
+ my_projects.select { |p| p.name == @platform }[0] ## We only have one bugsnag project
20
+ end
21
+
22
+ def top_errors(
23
+ app_version,
24
+ events_since: '1d',
25
+ error_status: 'open',
26
+ release_stage: 'production',
27
+ unhandled: 'true',
28
+ sort_by: 'events',
29
+ count: '5'
30
+ )
31
+ error_options = {
32
+ 'filters[event.since]': events_since,
33
+ 'filters[error.status]': error_status,
34
+ 'filters[app.release_stage]': release_stage,
35
+ 'filters[release.seen_in]': app_version,
36
+ 'filters[event.unhandled]': unhandled,
37
+ sort: sort_by,
38
+ per_page: count,
39
+ base: Time.now.utc.iso8601
40
+ }
41
+ @client.errors(my_project.id, nil, error_options)
42
+ end
43
+
44
+ def error_details(error_id)
45
+ @client.errors(my_project.id, error_id)
46
+ end
47
+
48
+ def release_groups(
49
+ current_version,
50
+ release_stage: 'production',
51
+ count: 5,
52
+ minimum_total_sessions: 50
53
+ )
54
+ options = {
55
+ release_stage_name: release_stage,
56
+ top_only: false,
57
+ visible_only: true,
58
+ per_page: count + 5 # grab a few extra to account for history and unreleased
59
+ }
60
+ result = @client.get("/projects/#{my_project.id}/release_groups", options).map do |release_group|
61
+ {
62
+ version: Gem::Version.new(release_group.app_version),
63
+ recent_sessions: release_group.sessions_count_in_last_24h,
64
+ session_errors: release_group.unhandled_sessions_count,
65
+ session_total: release_group.total_sessions_count,
66
+ user_errors: release_group.accumulative_daily_users_with_unhandled,
67
+ user_total: release_group.accumulative_daily_users_seen
68
+ }
69
+ end.each do |release_group|
70
+ release_group[:session_failure_rate] = release_group[:session_errors] / release_group[:session_total].to_f
71
+ release_group[:user_failure_rate] = release_group[:user_errors] / release_group[:user_total].to_f
72
+ end.each do |release_group|
73
+ release_group[:session_stability] = (1 - release_group[:session_failure_rate]) * 100
74
+ release_group[:user_stability] = (1 - release_group[:user_failure_rate]) * 100
75
+ end.sort_by do |release_group|
76
+ release_group[:version]
77
+ end
78
+
79
+ selected_releases = result.select do |release_group|
80
+ release_group[:version] <= Gem::Version.new(current_version) &&
81
+ release_group[:session_total] >= minimum_total_sessions # filter test releases
82
+ end
83
+
84
+ total_recent_sessions = selected_releases.sum { |release_group| release_group[:recent_sessions] }
85
+
86
+ selected_releases.each_with_index do |current, index|
87
+ current[:adoption_rate] = (current[:recent_sessions] / total_recent_sessions.to_f) * 100
88
+ previous = selected_releases[index - 1] unless index.zero?
89
+ current[:session_stability_change] =
90
+ previous ? current[:session_stability] - previous[:session_stability] : nil
91
+ current[:user_stability_change] = previous ? current[:user_stability] - previous[:user_stability] : nil
92
+ end.reverse.take(count)
93
+ end
94
+ end
@@ -0,0 +1,371 @@
1
+ require 'octokit'
2
+ require 'set'
3
+
4
+ # This class is a wrapper around the github api gem octokit
5
+ class GitHub
6
+ PULL_REQUEST_STATE_OPEN = 'open'.freeze
7
+
8
+ # the api_base_server_url for github.com is https://api.github.com
9
+ def initialize(repo, api_key, api_server_base_url: 'https://git.autodesk.com/api/v3/', is_dry_run: false)
10
+ @repo = repo
11
+ @client = Octokit::Client.new(access_token: api_key, api_endpoint: api_server_base_url)
12
+ @client.auto_paginate = true
13
+ @is_dry_run = is_dry_run
14
+ @username = nil
15
+ @user_id_cache = {}
16
+ @org_name = @repo.split('/')[0]
17
+ end
18
+
19
+ attr_accessor :client
20
+
21
+ # Get the username of the account associated with the api_key supplied to this client.
22
+ def username
23
+ @username = @client.user[:login] if @username.nil?
24
+ @username
25
+ end
26
+
27
+ def pull_request_state_open
28
+ PULL_REQUEST_STATE_OPEN
29
+ end
30
+
31
+ def repository?
32
+ @client.repository?(@repo)
33
+ end
34
+
35
+ def search_users(query)
36
+ @client.search_users(query)
37
+ end
38
+
39
+ # Find a user ID for a given name. Return the value immediately if already cached.
40
+ # Optionally enforce that the user is part of the organization that the specified repo
41
+ # is in. This helps disambiguate users on github.com
42
+ def find_user_id(name, enforce_org_membership: true)
43
+ return @user_id_cache[name] if @user_id_cache[name]
44
+
45
+ logins = search_users("fullname:#{name} type:users")[:items].map { |user_item| user_item[:login] }
46
+
47
+ org_member_logins = logins.select { |user_id| !enforce_org_membership || organization_member?(@org_name, user_id) }
48
+ raise "More than one github user id found for name '#{name}': #{org_member_logins}" if org_member_logins.size > 1
49
+
50
+ if org_member_logins.size == 1
51
+ @user_id_cache[name] = org_member_logins[0]
52
+ return @user_id_cache[name]
53
+ end
54
+ nil
55
+ end
56
+
57
+ # Fetch user data based on user login
58
+ def fetch_user(user_login)
59
+ @client.user(user_login)
60
+ end
61
+
62
+ def organization_member?(org_name, user_id)
63
+ @client.organization_member?(org_name, user_id)
64
+ end
65
+
66
+ # Branches #
67
+
68
+ def delete_branch(branch_name)
69
+ if @is_dry_run
70
+ puts "Would have deleted branch #{branch_name} from GitHub."
71
+ true
72
+ else
73
+ @client.delete_branch(@repo, branch_name)
74
+ end
75
+ end
76
+
77
+ def branch_protected?(branch_name)
78
+ @client.branch(@repo, branch_name).protected
79
+ end
80
+
81
+ # Releases #
82
+
83
+ # Creates a new release on github. If a release already exists with the same name delete that
84
+ # release before creating it again.
85
+ def create_or_update_draft_release(release_name, release_notes, git_tag_name, upload_asset_file_paths, git_ref)
86
+ if @is_dry_run
87
+ puts "Would have created github release with release name: #{release_name}, release notes: #{release_notes}, " \
88
+ "git tag name: #{git_tag_name}, asset_file_paths: #{upload_asset_file_paths}, and git_ref: #{git_ref}"
89
+ return
90
+ end
91
+ release = @client.list_releases(@repo).select { |item| item.name == release_name }.first
92
+
93
+ if release&.draft == false
94
+ puts "Release #{release_name} is not a draft. Do nothing"
95
+ return
96
+ end
97
+
98
+ @client.delete_release(release.url) if release
99
+
100
+ # Create Release
101
+ new_release = @client.create_release(
102
+ @repo,
103
+ git_tag_name,
104
+ target_commitish: git_ref,
105
+ name: release_name,
106
+ body: release_notes,
107
+ draft: true,
108
+ prerelease: false
109
+ )
110
+
111
+ # Upload assests
112
+ upload_asset_file_paths.each do |file_path|
113
+ absolute_path = File.absolute_path(file_path)
114
+ raise "File does not exist at path #{file_path}" unless File.exist?(absolute_path)
115
+
116
+ @client.upload_asset(new_release['url'], absolute_path)
117
+ end
118
+ end
119
+
120
+ # Publish a preexisting draft release on Github
121
+ def publish_release(release_name)
122
+ release = @client.list_releases(@repo).select { |item| item.name == release_name }.first
123
+ raise "No release found with release_name = #{release_name}" unless release
124
+
125
+ if @is_dry_run
126
+ puts "Would have released release #{release_name} on GitHub."
127
+ else
128
+ result = @client.update_release(release.url, draft: false)
129
+ puts 'Release is still a draft' if result[:draft] == true
130
+ nil
131
+ end
132
+ end
133
+
134
+ # Pull Request #
135
+ def create_pull_request(source_branch, target_branch, title, description = nil)
136
+ if @is_dry_run
137
+ puts "Would have created a pull request between branch #{source_branch} and branch #{target_branch} " \
138
+ "with title: #{title} and description: #{description}"
139
+ return DummyPR.new
140
+ end
141
+ begin
142
+ @client.create_pull_request(@repo, target_branch, source_branch, title, description)
143
+ rescue Octokit::UnprocessableEntity => e
144
+ if e.message.include? 'pull request already exists'
145
+ pull_request_by_source_and_target_branches(source_branch, target_branch, PULL_REQUEST_STATE_OPEN)
146
+ end
147
+ end
148
+ end
149
+
150
+ def approve_pull_request(pr_number)
151
+ if @is_dry_run
152
+ puts "Would have approved PR ##{pr_number}"
153
+ else
154
+ options = { event: 'APPROVE' }
155
+ @client.create_pull_request_review(@repo, pr_number, options)
156
+ nil
157
+ end
158
+ end
159
+
160
+ def pull_request_approved?(pr_number)
161
+ list_of_approvals = @client.pull_request_reviews(@repo, pr_number).select { |item| item[:state] == 'APPROVED' }
162
+ !list_of_approvals.empty?
163
+ end
164
+
165
+ # Given a list of globular patterns that match filepaths return a list
166
+ # of filepaths that don't match the provided glob patterns
167
+ def get_nonmatching_changes(pr_number, matching_glob_list)
168
+ changes = @client.pull_request_files(@repo, pr_number).map(&:filename)
169
+ matching_glob_list.each do |matching_glob|
170
+ changes.reject! { |filepath| File.fnmatch(matching_glob, filepath, File::FNM_DOTMATCH) }
171
+ end
172
+ changes
173
+ end
174
+
175
+ def pull_request(pr_number)
176
+ @client.pull_request(@repo, pr_number)
177
+ end
178
+
179
+ # Finds the first pull request that matches the source and target branch matching criteria.
180
+ # Wildcard characters '*' can be used as matching criteria.
181
+ def pull_request_by_source_and_target_branches(source_branch, target_branch, state)
182
+ @client.pull_requests(@repo, state: state).find do |item|
183
+ source_branch_matches = [item.head.ref, '*'].include?(source_branch)
184
+ target_branch_matches = [item.base.ref, '*'].include?(target_branch)
185
+ source_branch_matches && target_branch_matches
186
+ end
187
+ end
188
+
189
+ def add_comment_to_pr(pr_number, comment)
190
+ if @is_dry_run
191
+ puts "Would have added comment '#{comment}' to PR ##{pr_number}"
192
+ else
193
+ @client.add_comment(@repo, pr_number, comment)
194
+ nil
195
+ end
196
+ end
197
+
198
+ def assign_pr(pr_number, username_list, replace: false)
199
+ clean_username_list = username_list.compact.uniq
200
+ if @is_dry_run
201
+ puts "Would have assigned users: #{clean_username_list.join(', ')} to PR ##{pr_number}. Replace: #{replace}"
202
+ elsif replace
203
+ @client.update_issue(@repo, pr_number, assignees: clean_username_list)
204
+ else
205
+ pull_request = pull_request(pr_number)
206
+ assignees = pull_request['assignees'].map { |item| item['login'] }
207
+ all_assignees = assignees + clean_username_list
208
+ @client.update_issue(@repo, pr_number, assignees: all_assignees)
209
+ end
210
+ end
211
+
212
+ def merge_pull_request(pr_number, commit_title, commit_message, sha, merge_method)
213
+ options = {
214
+ 'commit_title' => commit_title,
215
+ 'sha' => sha,
216
+ 'merge_method' => merge_method
217
+ }
218
+ if @is_dry_run
219
+ puts "Would have merged pull request ##{pr_number} with options: #{options} " \
220
+ ", commit message: #{commit_message}, and merge method: #{merge_method}"
221
+ else
222
+ @client.merge_pull_request(@repo, pr_number, commit_message || '', options)
223
+ nil
224
+ end
225
+ end
226
+
227
+ def pull_request_is_merged?(pr_number)
228
+ @client.pull_merged?(@repo, pr_number)
229
+ end
230
+
231
+ def pull_request_commits(pr_number)
232
+ @client.pull_request_commits(@repo, pr_number)
233
+ end
234
+
235
+ # Pull Request Labels #
236
+
237
+ # Adds git labels to the provided PR
238
+ #
239
+ # @param pr_number [Integer] The number of the PR to add the labels to
240
+ # @param labels [[String]] An array of Strings
241
+ # @return [nil]
242
+ def add_labels_to_pr(pr_number:, labels:)
243
+ if @is_dry_run
244
+ puts "Would have added labels: #{labels.join(', ')} to PR ##{pr_number}"
245
+ else
246
+ @client.add_labels_to_an_issue(@repo, pr_number, labels)
247
+ nil
248
+ end
249
+ end
250
+
251
+ def remove_label_from_pr(pr_number, label)
252
+ if @is_dry_run
253
+ puts "Would have removed label: #{label} from PR ##{pr_number}"
254
+ else
255
+ @client.remove_label(@repo, pr_number, label)
256
+ nil
257
+ end
258
+ end
259
+
260
+ # Checks if the provided PR has given labels
261
+ #
262
+ # @param pr_number [Integer] The number of the PR
263
+ # @param labels [[String]] An array of labels to check
264
+ # @return [TrueClass, FalseClass]
265
+ def pull_request_has_labels?(pr_number:, labels:)
266
+ pr_labels = @client.labels_for_issue(@repo, pr_number).select { |item| labels.include?(item[:name]) }
267
+ pr_labels.length == labels.length
268
+ end
269
+
270
+ def pull_request_is_open?(pr_number)
271
+ @client.pull_request(@repo, pr_number)[:state] == PULL_REQUEST_STATE_OPEN
272
+ end
273
+
274
+ # Milestones #
275
+
276
+ def milestone(milestone_name, options = {})
277
+ @client.list_milestones(@repo, options).select { |item| item[:title] == milestone_name }.first
278
+ end
279
+
280
+ # Status Checks #
281
+
282
+ # Get most recent statuses for each context.
283
+ # See https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
284
+ def status_checks_for_ref(git_ref)
285
+ @client.combined_status(@repo, git_ref)
286
+ end
287
+
288
+ # Get the a set of context names for the required status checks for the given branch.
289
+ # This API requires that the account associated with the API key being used have
290
+ # admin access to the repository.
291
+ def required_status_checks_for_branch(branch_name)
292
+ branch_protection_info = @client.branch_protection(@repo, branch_name)
293
+ return Set.new if branch_protection_info.nil? || branch_protection_info.required_status_checks.empty?
294
+
295
+ Set.new(branch_protection_info.required_status_checks.contexts)
296
+ rescue Octokit::NotFound => e
297
+ raise "Branch protection not found for branch_name = #{branch_name}" if e.message.include? 'Branch not found'
298
+ if e.message.include? 'Not Found'
299
+ raise 'Branch protection resource not found. Check that the API key has the appropriate permissions.'
300
+ end
301
+
302
+ raise e.message
303
+ end
304
+
305
+ # Add a new status to a commit sha. Possible state options are: "error", "failure",
306
+ # "pending", "success". Context is the name of the status check
307
+ def add_status(git_commit_sha, state, context, description)
308
+ valid_states = %w[error failure pending success]
309
+ raise "Github status state must be one of #{valid_states.join(', ')}" unless Set.new(valid_states).include?(state)
310
+
311
+ options = {
312
+ context: context,
313
+ description: description
314
+ }
315
+ if @is_dry_run
316
+ puts "Would have created status for git sha: #{git_commit_sha} with state: #{state} and options: #{options}"
317
+ else
318
+ @client.create_status(@repo, git_commit_sha, state, options)
319
+ nil
320
+ end
321
+ end
322
+
323
+ # Return a set of status check context names which have not successfully completed
324
+ def unsatisfied_required_status_checks(branch_name, git_ref)
325
+ current_statuses = status_checks_for_ref(git_ref)['statuses']
326
+
327
+ successful_statuses_set = Set.new
328
+ current_statuses.select { |status| status.state == 'success' }.each do |current_status|
329
+ successful_statuses_set.add(current_status.context)
330
+ end
331
+
332
+ required_status_check_set = required_status_checks_for_branch(branch_name)
333
+ required_status_check_set.subtract(successful_statuses_set)
334
+
335
+ required_status_check_set
336
+ end
337
+
338
+ # Contents #
339
+
340
+ def content(path, git_ref)
341
+ @client.contents(@repo, path: path, ref: git_ref)
342
+ end
343
+
344
+ def content_size(path, git_ref)
345
+ content(path, git_ref)[:size].to_i
346
+ end
347
+ end
348
+
349
+ # Class for impersonating a pull request response when is_dry_run is true
350
+ class DummyPR < Object
351
+ def number
352
+ '9999999'
353
+ end
354
+
355
+ def html_url
356
+ 'https://github.com/fake_pr_link'
357
+ end
358
+
359
+ def [](key)
360
+ case key
361
+ when :number
362
+ '9999999'
363
+ when :html_url
364
+ 'https://github.com/fake_pr_link'
365
+ when :base
366
+ { ref: 'TARGET_BRANCH' }
367
+ else
368
+ raise "Unsupported key #{key} for DummyPR"
369
+ end
370
+ end
371
+ end