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
         
     |