sanemat/tachikoma

View on GitHub
lib/tachikoma/application.rb

Summary

Maintainability
D
1 day
Test Coverage
require 'safe_yaml'
require 'uri'
require 'tachikoma'
require 'octokit'
require 'fileutils'
require 'restore_bundled_with'

module Tachikoma
  # Main logic of Tachikoma
  class Application
    include FileUtils

    def self.run(strategy)
      new.run(strategy)
    end

    def run(strategy)
      load
      fetch
      send(strategy) if respond_to?(strategy)
      pull_request
    end

    def load
      @build_for = ENV['BUILD_FOR']
      @github_token = ENV[github_token_key(@build_for)]

      base_config_path = File.join(Tachikoma.original_data_path, 'default.yaml')
      base_config = YAML.safe_load_file(base_config_path) || {}
      user_config_path = File.join(Tachikoma.data_path, '__user_config__.yaml')
      user_config = YAML.safe_load_file(user_config_path) if File.exist?(user_config_path)
      user_config ||= {}
      each_config_path = File.join(Tachikoma.data_path, "#{@build_for}.yaml")
      each_config = YAML.safe_load_file(each_config_path) if File.exist?(each_config_path)
      unless each_config
        fail %(Something wrong, BUILD_FOR: #{@build_for}, your config_path: #{each_config_path})
      end

      @configure = base_config.merge(user_config).merge(each_config)

      @commiter_name = @configure['commiter_name']
      @commiter_email = @configure['commiter_email']
      @github_account = @configure['github_account']
      @url = @configure['url']
      @type = @configure['type']
      @base_remote_branch = @configure['base_remote_branch']
      @authorized_compare_url = authorized_compare_url_with_type(@url, @type, @github_token, @github_account)
      @authorized_base_url = authorized_base_url_with_type(@url, @type, @github_token, @github_account)
      @timestamp_format = @configure['timestamp_format']
      @readable_time = Time.now.utc.strftime(@timestamp_format)
      @parallel_option = bundler_parallel_option(Bundler::VERSION, @configure['bundler_parallel_number'])
      @depth_option = git_clone_depth_option(@configure['git_clone_depth'])
      @bundler_restore_bundled_with = @configure['bundler_restore_bundled_with']

      @target_head = target_repository_user(@type, @url, @github_account)
      @pull_request_url = repository_identity(@url)
      @pull_request_body = @configure['pull_request_body']
      @pull_request_base = @configure['pull_request_base']
      @pull_request_head = "#{@target_head}:tachikoma/update-#{@readable_time}"
      @pull_request_title = (@configure['pull_request_title'] || 'Exec tachikoma update %s') % @readable_time
    end

    def clean
      mkdir_p(Tachikoma.repos_path)
      rm_rf(Dir.glob(File.join(Tachikoma.repos_path, '*')))
    end

    def fetch
      clean
      sh(*([
        'git', 'clone',
        *@depth_option,
        @authorized_base_url,
        "#{Tachikoma.repos_path}/#{@build_for}"
      ].compact))
    end

    def bundler
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        Bundler.with_clean_env do
          sh(*['ruby', '-i', '-pe', '$_.gsub! /^ruby/, "#ruby"', 'Gemfile'])
          sh(*['git', 'config', 'user.name', @commiter_name])
          sh(*['git', 'config', 'user.email', @commiter_email])
          sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
          if File.exist?('Gemfile')
            @bundler_key_file = 'Gemfile'
            @bundler_lock_file = 'Gemfile.lock'
          elsif File.exist?('gems.rb')
            @bundler_key_file = 'gems.rb'
            @bundler_lock_file = 'gems.locked'
          else
            @bundler_key_file = 'Gemfile'
            @bundler_lock_file = 'Gemfile.lock'
          end
          sh(*([
            'bundle',
            '--gemfile', @bundler_key_file,
            '--no-deployment',
            '--without', 'nothing',
            '--path', 'vendor/bundle',
            @parallel_option
          ].compact))
          sh(*%w(bundle update))

          if @bundler_restore_bundled_with
            # restore_bundled_with
            lock_file_contents = File.read(@bundler_lock_file)
            lock_file = RestoreBundledWith::Lock.restore(
              lock_file_contents, @bundler_lock_file)
            File.write(@bundler_lock_file, lock_file.body)
          end

          sh(*['git', 'add', @bundler_lock_file])
          sh(*['git', 'commit', '-m', "Bundle update #{@readable_time}"]) do
            # ignore exitstatus
          end
          sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
        end
      end
    end

    def bundle
      warn '[DEPRECATION] `bundle` is deprecated. Please use `bundler` instead.'
      bundler
    end

    def carton
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*%w(carton install))
        sh(*%w(carton update))
        sh(*['git', 'add', 'carton.lock']) if File.exist?('carton.lock')
        sh(*['git', 'add', 'cpanfile.snapshot']) if File.exist?('cpanfile.snapshot')
        sh(*['git', 'commit', '-m', "Carton update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def none
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*['git', 'commit', '--allow-empty', '-m', "None update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def david
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*['david', 'update', '--warn404'])
        if File.exist?('npm-shrinkwrap.json')
          sh(*['rm', '-rf', 'node_modules/', 'npm-shrinkwrap.json'])
          sh(*['npm', 'install'])
          sh(*['npm', 'shrinkwrap'])
          sh(*['git', 'add', 'package.json'])
          sh(*['git', 'add', 'npm-shrinkwrap.json'])
        else
          sh(*['git', 'add', 'package.json'])
        end
        sh(*['git', 'commit', '-m', "David update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def composer
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        # FIXME: Use Octokit.api_endpoint for GitHub Enterprise
        sh(*['composer', 'config', 'github-oauth.github.com', @github_token])
        sh(*['composer', 'install', '--no-interaction'])
        sh(*['composer', 'update', '--no-interaction'])
        sh(*['git', 'add', 'composer.lock'])
        sh(*['git', 'commit', '-m', "Composer update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def carthage
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*%w(carthage bootstrap))
        sh(*%w(carthage update))
        sh(*['git', 'add', 'Cartfile.resolved'])
        sh(*['git', 'commit', '-m', "Carthage update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def cocoapods
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*%w(pod install))
        sh(*%w(pod update))
        sh(*['git', 'add', 'Podfile.lock'])
        sh(*['git', 'commit', '-m', "Cocoapods update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def ncu
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*%w(ncu -u))
        sh(*['git', 'add', 'package.json']) if File.exist?('package.json')
        sh(*['git', 'commit', '-m', "Ncu update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def yarn
      Dir.chdir("#{Tachikoma.repos_path}/#{@build_for}") do
        sh(*['git', 'config', 'user.name', @commiter_name])
        sh(*['git', 'config', 'user.email', @commiter_email])
        sh(*['git', 'checkout', '-b', "tachikoma/update-#{@readable_time}", @base_remote_branch])
        sh(*%w(yarn install))
        sh(*%w(yarn outdated))
        sh(*%w(yarn upgrade))
        sh(*['git', 'add', 'package.json'])
        sh(*['git', 'add', 'yarn.lock'])
        sh(*['git', 'commit', '-m', "Yarn update #{@readable_time}"]) do
          # ignore exitstatus
        end
        sh(*['git', 'push', @authorized_compare_url, "tachikoma/update-#{@readable_time}"])
      end
    end

    def pull_request
      @client = Octokit::Client.new access_token: @github_token
      @client.create_pull_request(
        @pull_request_url,
        @pull_request_base,
        @pull_request_head,
        @pull_request_title,
        @pull_request_body
      )
    rescue Octokit::UnprocessableEntity
    end

    # build_for = fenix-knight, github_token_key = TOKEN_FENIX_KNIGHT
    def github_token_key(build_for)
      "TOKEN_#{build_for}".gsub(/-/, '_').upcase
    end

    def authorized_compare_url_with_type(fetch_url, type, github_token, github_account)
      uri = URI.parse(fetch_url)
      case type
      when 'fork'
        %(#{uri.scheme}://#{github_token}:x-oauth-basic@#{uri.host}#{path_for_fork(uri.path, github_account)})
      when 'shared'
        "#{uri.scheme}://#{github_token}:x-oauth-basic@#{uri.host}#{uri.path}"
      else
        fail InvalidType, "Invalid type #{type}"
      end
    end

    def authorized_base_url_with_type(fetch_url, type, github_token, _github_account)
      uri = URI.parse(fetch_url)
      case type
      when 'fork', 'shared'
        "#{uri.scheme}://#{github_token}:x-oauth-basic@#{uri.host}#{uri.path}"
      else
        fail InvalidType, "Invalid type #{type}"
      end
    end

    def path_for_fork(path, github_account)
      path.sub(%r{^/[^/]+}) { '/' + github_account }
    end

    def target_repository_user(type, fetch_url, github_account)
      case type
      when 'fork'
        github_account
      when 'shared'
        URI.parse(fetch_url).path.split('/', 3)[1]
      else
        fail InvalidType, "Invalid type: #{type}"
      end
    end

    def repository_identity(url)
      project_name, user_name, _ = url.split('/').reverse
      project_name_identity = strip_extension(project_name)
      user_name + '/' + project_name_identity
    end

    def strip_extension(name)
      /\A(?<identity>.*?)(?:\.git)?\z/ =~ name
      identity
    end

    # TODO: refactor to returning command args array like git clone depth option
    def bundler_parallel_option(bundler_version, parallel_number)
      return if !bundler_parallel_available?(bundler_version) || parallel_number <= 1
      "--jobs=#{parallel_number}"
    end

    def bundler_parallel_available?(bundler_version)
      # bundler 1.4.0 gets parallel number option
      Gem::Version.create(bundler_version) >= Gem::Version.create('1.4.0')
    end

    def git_clone_depth_option(depth)
      return [nil] unless depth
      ['--depth', depth.to_s]
    end
  end
end