module Circleci::Bundle::Update::Pr

Constants

BRANCH_PREFIX
TITLE_PREFIX
VERSION

Public Class Methods

create_if_needed(git_username: nil, git_email: nil, git_branches: %w[master main], assignees: nil, reviewers: nil, labels: nil, allow_dup_pr: false) click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 10
def self.create_if_needed(git_username: nil, git_email: nil, git_branches: %w[master main],
                          assignees: nil, reviewers: nil, labels: nil, allow_dup_pr: false)
  raise_if_env_unvalid!

  if skip?(allow_dup_pr)
    puts 'Skip because it has already existed.'
    return
  end

  unless target_branch?(running_branch: ENV.fetch('CIRCLE_BRANCH', nil), target_branches: git_branches)
    puts "Skip because CIRCLE_BRANCH[#{ENV.fetch('CIRCLE_BRANCH', nil)}] is not included in target branches[#{git_branches.join(',')}]."
    return
  end

  unless need_to_commit?
    puts 'No changes due to bundle update'
    return
  end

  git_username ||= client.user.login
  git_email ||= "#{git_username}@users.noreply.#{github_host}"

  branch = create_branch(git_username, git_email)
  pull_request = create_pull_request(branch)
  add_labels(pull_request[:number], labels) if labels
  update_pull_request_body(pull_request[:number])
  add_assignees(pull_request[:number], assignees) if assignees
  request_review(pull_request[:number], reviewers) if reviewers
end

Private Class Methods

add_assignees(pr_number, assignees) click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 189
def self.add_assignees(pr_number, assignees)
  client.add_assignees(repo_full_name, pr_number, assignees)
end
add_labels(pr_number, labels) click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 158
def self.add_labels(pr_number, labels)
  client.add_labels_to_an_issue(repo_full_name, pr_number, labels)
end
client() click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 199
def self.client
  if enterprise?
    Octokit::Client.new(access_token: ENV.fetch('ENTERPRISE_OCTOKIT_ACCESS_TOKEN', nil),
                        api_endpoint: ENV.fetch('ENTERPRISE_OCTOKIT_API_ENDPOINT', nil))
  else
    Octokit::Client.new(access_token: ENV.fetch('GITHUB_ACCESS_TOKEN', nil))
  end
end
create_branch(git_username, git_email) click to toggle source

Create remote branch for bundle update PR

@return [String] remote branch name. e.g. bundle-update-20180929154455

# File lib/circleci/bundle/update/pr.rb, line 106
def self.create_branch(git_username, git_email)
  branch = "#{BRANCH_PREFIX}#{now.strftime('%Y%m%d%H%M%S')}"

  current_ref = client.ref(repo_full_name, "heads/#{ENV.fetch('CIRCLE_BRANCH', nil)}")
  branch_ref = client.create_ref(repo_full_name, "heads/#{branch}", current_ref.object.sha)

  branch_commit = client.commit(repo_full_name, branch_ref.object.sha)

  lockfile = File.read('Gemfile.lock')
  lockfile_blob_sha = client.create_blob(repo_full_name, lockfile)
  tree = client.create_tree(
    repo_full_name,
    [
      {
        path: lockfile_path,
        mode: '100644',
        type: 'blob',
        sha: lockfile_blob_sha
      }
    ],
    base_tree: branch_commit.commit.tree.sha
  )

  commit = client.create_commit(
    repo_full_name,
    '$ bundle update && bundle update --ruby',
    tree.sha,
    branch_ref.object.sha,
    author: {
      name: git_username,
      email: git_email
    }
  )

  client.update_ref(repo_full_name, "heads/#{branch}", commit.sha)

  puts "#{branch} is created"

  branch
end
create_pull_request(branch) click to toggle source

Create bundle update PR

@param branch [String] branch name @return [Sawyer::Resource] The newly created pull request

# File lib/circleci/bundle/update/pr.rb, line 152
def self.create_pull_request(branch)
  title = "#{TITLE_PREFIX}#{now.strftime('%Y-%m-%d %H:%M:%S %Z')}"
  client.create_pull_request(repo_full_name, ENV.fetch('CIRCLE_BRANCH', nil), branch, title)
end
enterprise?() click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 209
def self.enterprise?
  !!ENV['ENTERPRISE_OCTOKIT_ACCESS_TOKEN']
end
exists_bundle_update_pr?() click to toggle source

Has ‘bundle update PR’ already existed?

@return [Boolean]

# File lib/circleci/bundle/update/pr.rb, line 70
def self.exists_bundle_update_pr?
  client.pull_requests(repo_full_name).find do |pr|
    pr.title =~ /\A#{TITLE_PREFIX}/ && pr.head.ref =~ /\A#{BRANCH_PREFIX}\d+/
  end != nil
end
github_host() click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 222
def self.github_host
  # A format like https://github.com/masutaka/circleci-bundle-update-pr.git
  return Regexp.last_match(1) if ENV.fetch('CIRCLE_REPOSITORY_URL', nil) =~ %r{https://(.+?)/}
  # A format like git@github.com:masutaka/compare_linker.git
  return Regexp.last_match(1) if ENV.fetch('CIRCLE_REPOSITORY_URL', nil) =~ /([^@]+?):/

  'github.com'
end
lockfile_path() click to toggle source

Get Gemfile.lock path relative to workdir

@return [String]

# File lib/circleci/bundle/update/pr.rb, line 243
def self.lockfile_path
  workdir_env = ENV.fetch('CIRCLE_WORKING_DIRECTORY', nil)
  return 'Gemfile.lock' unless workdir_env

  workdir = Pathname.new(workdir_env).expand_path
  gemfile_lock = Pathname.new(File.expand_path('Gemfile.lock'))
  gemfile_lock.relative_path_from(workdir).to_s
end
need_to_commit?() click to toggle source

Does it need to commit due to bundle update?

@return [Boolean]

# File lib/circleci/bundle/update/pr.rb, line 90
def self.need_to_commit?
  old_lockfile = File.read('Gemfile.lock')

  unless system('bundle update && bundle update --ruby')
    raise 'Unable to execute `bundle update && bundle update --ruby`'
  end

  new_lockfile = File.read('Gemfile.lock')

  old_lockfile != new_lockfile
end
now() click to toggle source

Get unified current time

@return [Time]

# File lib/circleci/bundle/update/pr.rb, line 235
def self.now
  @now ||= Time.now
end
raise_if_env_unvalid!() click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 43
def self.raise_if_env_unvalid!
  raise "$CIRCLE_PROJECT_USERNAME isn't set" unless ENV['CIRCLE_PROJECT_USERNAME']
  raise "$CIRCLE_PROJECT_REPONAME isn't set" unless ENV['CIRCLE_PROJECT_REPONAME']
  raise "$GITHUB_ACCESS_TOKEN isn't set" unless ENV['GITHUB_ACCESS_TOKEN']
  if ENV.fetch('ENTERPRISE_OCTOKIT_ACCESS_TOKEN', nil) && !ENV['ENTERPRISE_OCTOKIT_API_ENDPOINT']
    raise "$ENTERPRISE_OCTOKIT_API_ENDPOINT isn't set"
  end
  if !ENV['ENTERPRISE_OCTOKIT_ACCESS_TOKEN'] && ENV.fetch('ENTERPRISE_OCTOKIT_API_ENDPOINT', nil)
    raise "$ENTERPRISE_OCTOKIT_ACCESS_TOKEN isn't set"
  end
end
repo_full_name() click to toggle source

Get repository full name

@return [String] e.g. ‘masutaka/circleci-bundle-update-pr’

# File lib/circleci/bundle/update/pr.rb, line 217
def self.repo_full_name
  @repo_full_name ||= "#{ENV.fetch('CIRCLE_PROJECT_USERNAME', nil)}/#{ENV.fetch('CIRCLE_PROJECT_REPONAME', nil)}"
end
request_review(pr_number, reviewers) click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 194
def self.request_review(pr_number, reviewers)
  client.request_pull_request_review(repo_full_name, pr_number, reviewers)
end
skip?(allow_dup_pr) click to toggle source

Should skip to make PR?

@param allow_dup_pr [Boolean] @return [Boolean]

# File lib/circleci/bundle/update/pr.rb, line 60
def self.skip?(allow_dup_pr)
  return false if allow_dup_pr

  exists_bundle_update_pr?
end
target_branch?(running_branch:, target_branches:) click to toggle source

Is running branch included in target_branches?

@param running_branch [String] @param target_branches [Array<String>] @return [Boolean]

# File lib/circleci/bundle/update/pr.rb, line 82
def self.target_branch?(running_branch:, target_branches:)
  target_branches.include?(running_branch)
end
update_pull_request_body(pr_number) click to toggle source
# File lib/circleci/bundle/update/pr.rb, line 163
        def self.update_pull_request_body(pr_number)
          ENV['OCTOKIT_ACCESS_TOKEN'] = ENV.fetch('GITHUB_ACCESS_TOKEN', nil)
          compare_linker = CompareLinker.new(repo_full_name, pr_number)
          compare_linker.formatter = CompareLinker::Formatter::Markdown.new

          body = <<-PR_BODY
**Updated RubyGems:**

#{compare_linker.make_compare_links.to_a.join("\n")}

Powered by [circleci-bundle-update-pr](https://rubygems.org/gems/circleci-bundle-update-pr)
          PR_BODY

          if Note.exist?
            body << <<-PR_BODY

---

#{Note.read}
            PR_BODY
          end

          client.update_pull_request(repo_full_name, pr_number, body: body)
        end