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