mongodb/mongoid

View on GitHub
.evergreen/make-github-actions

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby

# TODO: Support topology=sharded-cluster (requires better MongoDB Github Action)
# TODO: Support i18n-fallbacks test
# TODO: Support app-tests tests

# Note on topology: server:
# The GH actions use mongo-orchestration, which uses a "server" topology for
# the standalone one. See transform_node! below for the mapping of all
# topologies.

require 'fileutils'
require 'active_support'
require 'active_support/dependencies/autoload'
require 'active_support/core_ext'
require 'yaml'

class YamlConfig
  def data
    @data ||= YAML.load(File.read(path)).deep_symbolize_keys
  end
end

class EvergreenConfig < YamlConfig

  def matrix_axes
    data[:axes].each_with_object({}) do |axis, node|
      node[axis[:id].underscore] = axis[:values].pluck(:id)
    end
  end

  def axes
    @axes ||= data[:axes].each_with_object({}) do |axis, node|
      node[axis[:id].underscore.to_sym] = axis[:values].pluck(:id)
    end
  end

  def variants
    @variants ||= data[:buildvariants].each_with_object([]) do |var, nodes|
      name = var[:matrix_name]

      spec = var[:matrix_spec]
      tasks = var[:tasks]&.pluck(:name)
      raise("Invalid Evergreen buildvariant '#{name}'. Value for 'tasks' is empty.") unless tasks.present?
      raise("Unsupported Evergreen buildvariant '#{name}'. Cannot have more than one item in 'tasks'.") unless tasks.size == 1

      node = {
        # matrix_name:  name,
        # display_name: var[:display_name],
        mongodb:  spec[:'mongodb-version'],
        ruby:     spec[:jruby] || spec[:ruby],
        topology: spec[:topology],
        task:     tasks.first,

        # these will be later mapped to gemfile
        driver: spec[:driver],
        rails:  spec[:rails],
      }

      missing = node.map {|k, v| k if v.blank? }.compact
      missing -= %i[driver rails]

      if missing.present?
        puts "Skipping invalid Evergreen buildvariant '#{name}'. Keys missing: #{missing}"
        next
      end

      nodes << node
    end
  end

  protected

  def path
    File.join(File.dirname(__FILE__), 'config.yml')
  end
end

class GithubConfig < YamlConfig

  def write(data)
    FileUtils.mkdir_p(File.dirname(path))
    data = data.deep_stringify_keys
    File.open(path, 'w+') {|f| f.write((comment || '') + data.to_yaml) }
    @data = data
  end

  private

  def path
    File.join(File.dirname(__FILE__), '../.github/workflows/test.yml')
  end

  def comment
    <<~COMMENT
      # This file was auto-generated by .evergreen/make-github-actions
      # at #{Time.now.utc.rfc3339}
    COMMENT
  end
end

class Transmogrifier
  SPLATTABLE_FIELDS  = %i[mongodb topology ruby rails driver]
  COMPACTABLE_FIELDS = %i[mongodb topology ruby gemfile]

  attr_reader :eg_config,
              :gh_config

  def initialize
    @eg_config = EvergreenConfig.new
    @gh_config = GithubConfig.new
  end

  def transmogrify!
    gh_config.write(root_node)
    print_stats
  end

  delegate :variants,
           :axes,
           to: :eg_config

  def root_node
    {
      name: 'Run Mongoid Tests',
      on: %w[push pull_request],
      jobs: {
        build: {
          name: '${{matrix.ruby}} driver-${{matrix.driver}} mongodb-${{matrix.mongodb}} ${{matrix.topology}}',
          env: {
            CI: true,
            TESTOPTS: "-v"
          },
          'runs-on': 'ubuntu-latest',
          'continue-on-error': "${{matrix.experimental}}",
          strategy: {
            'fail-fast': false,
            matrix: { include: inclusions_node } },
          steps: steps_node
        }
      }
    }
  end

  def print_stats
    puts "#{inclusions_node.size} unique matrix builds written"
  end

  def inclusions_node
    @inclusions_node ||= begin
      inclusions = splat(variants).each {|node| transform_node!(node) }.reject {|node| unsupported?(node) }
      puts "#{inclusions.size} candidate matrix builds identified"
      compact_nodes(inclusions)
    end
  end

  def compact_nodes(nodes)
    nodes.each_with_object({}) do |node, uniq_nodes|
      key = COMPACTABLE_FIELDS.map {|k| "#{k}:#{node[k]}" }.join('|')
      uniq_nodes[key] ||= node
    end.values
  end

  def splat(array)
    # array
    SPLATTABLE_FIELDS.each do |field|
      array = array.each_with_object([]) do |node, new_array|
        case node[field]
        when Array
          node[field].each do |val|
            new_array << node.dup.tap {|n| n[field] = val }
          end
        when '*'
          axes[field].each do |val|
            new_array << node.dup.tap {|n| n[field] = val }
          end
        else
          new_array << node
        end
      end
    end
    array
  end

  def unsupported?(node)
    node[:topology] == 'sharded_cluster'
  end

  def transform_node!(node)
    node[:topology] = case node[:topology]
                        when 'replica-set'
                          'replica_set'
                        when 'sharded-cluster'
                          'sharded_cluster'
                        else
                          'server'
                      end
    extract_gemfile!(node)
    set_experimental!(node)
  end

  def set_experimental!(node)
    node[:experimental] = node[:gemfile].match?(/master|main|head/)
  end

  def extract_gemfile!(node)
    node[:gemfile] = get_gemfile(*node.values_at(:driver, :rails))
  end

  # Ported from run-tests.sh
  def get_gemfile(driver, rails)
    driver, rails = [driver, rails].map {|v| v&.to_s }
    if driver && driver != 'current'
      "gemfiles/driver_#{driver.underscore}.gemfile"
    elsif rails && rails != '6.1' # TODO: "6.1" should be renamed to "current" in Evergreen
      "gemfiles/rails-#{rails}.gemfile"
    else
      'Gemfile'
    end
  end

  def ruby_env
    {}
  end

  def steps_node
    [ step_checkout,
      step_mongodb,
      step_ruby,
      step_bundle,
      step_test ]
  end

  def step_checkout
    {
      name: "repo checkout",
      uses: "actions/checkout@v2",
      with: {
        submodules: "recursive"
      }
    }
  end

  def step_mongodb
    {
      id: "start-mongodb",
      name: "start mongodb",
      uses: "mongodb-labs/drivers-evergreen-tools@master",
      with: {
        'version': '${{matrix.mongodb}}',
        'topology': '${{matrix.topology}}'
      }
    }
  end

  def step_ruby
    {
      name: "load ruby",
      uses: "ruby/setup-ruby@v1",
      env: step_ruby_env,
      with: {
        'ruby-version': "${{matrix.ruby}}",
        bundler: 2
      }
    }
  end

  def step_bundle
    {
      name: 'bundle',
      run: 'bundle install --jobs 4 --retry 3',
      env: step_ruby_env
    }
  end

  def step_test
    {
      name: 'test',
      "timeout-minutes": 60,
      "continue-on-error": '${{matrix.experimental}}',
      run: 'bundle exec rake spec',
      env: step_ruby_env.merge({'MONGODB_URI': "${{ steps.start-mongodb.outputs.cluster-uri }}"})
    }
  end

  def step_ruby_env
    { 'BUNDLE_GEMFILE': '${{matrix.gemfile}}' }
  end
end

Transmogrifier.new.transmogrify!