.evergreen/make-github-actions
#!/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!