Last active
December 7, 2019 13:10
-
-
Save yyuu/995a11644bf4ec7d61f6652a5c9dfebe to your computer and use it in GitHub Desktop.
A simple script to convert Circle CI configurations from circle.yml (1.0) to .circleci/config.yml (2.0)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require "fileutils" | |
require "shellwords" | |
require "yaml" | |
CIRCLE1_DEFAULT_CACHE_DIRECTORIES = [ | |
"vendor/bundle", | |
"~/.m2", | |
"~/.bundle", | |
"~/.go_workspace", | |
] | |
circle1 = YAML.load(ARGF.read) | |
circle2 = { | |
"version" => 2, | |
} | |
circle2["jobs"] = {"build" => {}} | |
if circle1["machine"] | |
circle2["jobs"]["build"]["docker"] = %w(ruby).flat_map { |language| | |
Array(circle1["machine"][language]["version"]).map { |version| | |
{"image" => "circleci/#{language}:#{version}", "environment" => Hash(circle1["machine"]["environment"])} | |
} | |
} | |
end | |
def generate_cache_key(prefix, dir) | |
# Tip: Caches are immutable so it is useful to start all your cache keys with a version prefix, for example v1-.... This enables you to regenerate all of your caches by incrementing the version in this prefix. | |
# https://circleci.com/docs/2.0/caching/ | |
if File.file?(File.join(dir, "Gemfile.lock")) | |
%|#{prefix}{{ checksum "Gemfile.lock" }}| | |
elsif File.file?(File.join(dir, "pom.xml")) | |
%|#{prefix}{{ checksum "pom.xml" }}| | |
elsif File.file?(File.join(dir, "build.gradle")) | |
%|#{prefix}{{ checksum "build.gradle" }}| | |
elsif File.file?(File.join(dir, "build.sbt")) | |
%|#{prefix}{{ checksum "build.sbt" }}| | |
else | |
%|#{prefix}{{ .Branch }}| | |
end | |
end | |
def convert_runnable_params(params) | |
Hash[params.map { |key, val| | |
case key | |
when "timeout" | |
["no_output_timeout", val] | |
else | |
[key, val] | |
end | |
}] | |
end | |
def convert_runnable(runnable) | |
# command may have some extra parameters (e.g. timeout) | |
if Hash === runnable | |
runnable.map { |key, val| | |
if Hash === val | |
{"run" => convert_runnable_params(val).merge("command" => key.strip)} | |
else | |
{"run" => {"command" => key.strip}} | |
end | |
} | |
else | |
[{"run" => {"command" => runnable}}] | |
end | |
end | |
# FIXME: need sophisticated way to detect the use of awscli | |
needs_awscli = /aws\s+/ =~ YAML.dump(circle1) | |
needs_docker = Array(Hash(circle1["machine"])["services"]).index("docker") | |
circle2["jobs"]["build"]["steps"] = [] | |
if needs_awscli | |
circle2["jobs"]["build"]["steps"] << { | |
"run" => {"command" => [ | |
%|set -ex -o pipefail|, | |
%|sudo DEBIAN_FRONTEND="noninteractive" apt-get update|, | |
%|sudo DEBIAN_FRONTEND="noninteractive" apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -q -y install python-pip python-yaml|, | |
%|sudo pip install awscli==1.11.151|, | |
].join("\n")}, | |
} | |
end | |
if needs_docker | |
circle2["jobs"]["build"]["steps"] << "setup_remote_docker" | |
end | |
circle2["jobs"]["build"]["steps"] += %w(checkout dependencies database compile test).flat_map { |circle1_step| | |
steps = [] | |
original_steps = Hash(circle1[circle1_step]) | |
case circle1_step | |
when "dependencies" | |
steps << { | |
"restore_cache" => { | |
"key" => generate_cache_key("v1-dependency-cache-", ".") | |
} | |
} | |
end | |
original_steps_pre = Array(original_steps["pre"]) | |
steps += original_steps_pre.flat_map { |runnable| | |
convert_runnable(runnable) | |
} | |
original_steps_override = Array(original_steps["override"]) | |
if original_steps_override.empty? | |
# simulate circle1's default behavior | |
case circle1_step | |
when "checkout" | |
steps << "checkout" | |
when "dependencies" | |
if File.file?("Gemfile") | |
steps << {"run" => {"command" => "bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3"}} | |
elsif File.file?("pom.xml") | |
steps << {"run" => {"command" => "mvn dependency:resolve"}} | |
elsif 0 < Dir.glob("*.gradle").length | |
steps << {"run" => {"command" => "gradle dependencies"}} | |
elsif 0 < Dir.glob("*.sbt").length | |
# nop | |
else | |
steps << {"run" => {"command" => "echo FIXME: please add some command for dependency resolution"}} | |
end | |
when "compile" | |
if File.file?("Gemfile") | |
# nop | |
elsif File.file?("pom.xml") | |
steps << {"run" => {"command" => "mvn test:compile"}} | |
elsif 0 < Dir.glob("*.gradle").length | |
steps << {"run" => {"command" => "gradle compile"}} | |
elsif 0 < Dir.glob("*.sbt").length | |
steps << {"run" => {"command" => "sbt test:compile"}} | |
else | |
steps << {"run" => {"command" => "echo FIXME: please add some command for compilation"}} | |
end | |
when "test" | |
if File.file?("Gemfile") | |
steps << {"run" => {"command" => "bundle exec rake test"}} | |
elsif File.file?("pom.xml") | |
steps << {"run" => {"command" => "mvn integration-test"}} | |
elsif 0 < Dir.glob("*.gradle").length | |
steps << {"run" => {"command" => "gradle test"}} | |
elsif 0 < Dir.glob("*.sbt").length | |
steps << {"run" => {"command" => "sbt test:test"}} | |
else | |
steps << {"run" => {"command" => "echo FIXME: please add some command for testing"}} | |
end | |
end | |
else | |
steps += original_steps_override.flat_map { |runnable| | |
convert_runnable(runnable) | |
} | |
end | |
case circle1_step | |
when "dependencies" | |
if circle1[circle1_step].key?("cache_directories") | |
steps << { | |
"save_cache" => { | |
"key" => generate_cache_key("v1-dependency-cache-", "."), | |
"paths" => CIRCLE1_DEFAULT_CACHE_DIRECTORIES + Array(circle1[circle1_step]["cache_directories"]), | |
} | |
} | |
else | |
steps << { | |
"save_cache" => { | |
"key" => generate_cache_key("v1-dependency-cache-", "."), | |
"paths" => CIRCLE1_DEFAULT_CACHE_DIRECTORIES, | |
} | |
} | |
end | |
end | |
original_steps_post = Array(original_steps["post"]) | |
steps += original_steps_post.flat_map { |runnable| | |
convert_runnable(runnable) | |
} | |
steps | |
} | |
if circle1.key?("deployment") | |
default_deployments = Array(circle1["deployment"]).select { |name, params| | |
Array(params["branch"]).index("/.*/") | |
} | |
deploy = [] | |
deploy << %|if false; then| | |
deploy << %| true| | |
deploy += (Array(circle1["deployment"]) - default_deployments).flat_map { |name, params| | |
Array(params["branch"]).flat_map { |branch| | |
case branch | |
when %r|\A/(.*)/\z| | |
[%|elif [[ "${CIRCLE_BRANCH}" =~ #{$1} ]]; then|] | |
else | |
[%|elif [[ "${CIRCLE_BRANCH}" == #{Shellwords.shellescape(branch)} ]]; then|] | |
end + Array(params["commands"]).map { |command| command.strip } | |
} | |
} | |
if default_deployments.empty? | |
deploy << %|else| | |
deploy << %| echo "Not target branch so not deploying"| | |
else | |
default_deployment_name, default_deployment_params = default_deployments.first | |
deploy << %|else| | |
deploy += Array(default_deployment_params["commands"]).map { |command| command.strip } | |
end | |
deploy << %|fi| | |
circle2["jobs"]["build"]["steps"] << { | |
"deploy" => { | |
"name" => Array(Array(circle1["deployment"]).first).first || "deploy", | |
"command" => deploy.join("\n"), | |
} | |
} | |
end | |
STDOUT.puts(YAML.dump(circle2)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment