larrow-runner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +25 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +80 -0
  8. data/Rakefile +2 -0
  9. data/bin/larrow +4 -0
  10. data/larrow-runner.gemspec +42 -0
  11. data/lib/larrow/runner/cli/build.rb +41 -0
  12. data/lib/larrow/runner/cli/main.rb +35 -0
  13. data/lib/larrow/runner/cli/tools.rb +34 -0
  14. data/lib/larrow/runner/cli.rb +11 -0
  15. data/lib/larrow/runner/errors.rb +1 -0
  16. data/lib/larrow/runner/helper.rb +0 -0
  17. data/lib/larrow/runner/logger.rb +69 -0
  18. data/lib/larrow/runner/manager.rb +114 -0
  19. data/lib/larrow/runner/manifest/adapter/blank.rb +8 -0
  20. data/lib/larrow/runner/manifest/adapter/larrow.rb +32 -0
  21. data/lib/larrow/runner/manifest/adapter/travis.rb +73 -0
  22. data/lib/larrow/runner/manifest/base_loader.rb +21 -0
  23. data/lib/larrow/runner/manifest/configuration.rb +126 -0
  24. data/lib/larrow/runner/manifest.rb +48 -0
  25. data/lib/larrow/runner/model/app.rb +62 -0
  26. data/lib/larrow/runner/model/node.rb +73 -0
  27. data/lib/larrow/runner/service/cloud.rb +54 -0
  28. data/lib/larrow/runner/service/executor.rb +56 -0
  29. data/lib/larrow/runner/service.rb +8 -0
  30. data/lib/larrow/runner/session.rb +64 -0
  31. data/lib/larrow/runner/vcs/base.rb +17 -0
  32. data/lib/larrow/runner/vcs/file_system.rb +58 -0
  33. data/lib/larrow/runner/vcs/github.rb +48 -0
  34. data/lib/larrow/runner/vcs.rb +20 -0
  35. data/lib/larrow/runner/version.rb +5 -0
  36. data/lib/larrow/runner.rb +34 -0
  37. data/spec/fixtures/travis_erlang.yml +8 -0
  38. data/spec/fixtures/travis_ruby.yml +9 -0
  39. data/spec/integration/build_cmds_spec.rb +18 -0
  40. data/spec/integration/test_cmds_spec.rb +13 -0
  41. data/spec/manifest/travis_spec.rb +42 -0
  42. data/spec/model/node_spec.rb +18 -0
  43. data/spec/service/executor_spec.rb +26 -0
  44. data/spec/spec_helper.rb +10 -0
  45. data/spec/vcs/github_spec.rb +33 -0
  46. metadata +340 -0
@@ -0,0 +1,126 @@
1
+ module Larrow::Runner::Manifest
2
+ # The top of manifest model which store Steps information
3
+ class Configuration
4
+ DEFINED_GROUPS = {
5
+ all:[
6
+ :init,
7
+ :source_sync, #inner step
8
+ :prepare,
9
+ :compile, :unit_test,
10
+ :before_install, #inner_step
11
+ :install, :functional_test,
12
+ :before_start, #inner_step
13
+ :start, :integration_test,
14
+ :after_start, :complete #inner_step
15
+ ],
16
+ custom: [
17
+ :init,
18
+ :prepare,
19
+ :compile, :unit_test,
20
+ :install, :functional_test,
21
+ :start, :integration_test,
22
+ ],
23
+ deploy: [
24
+ :init,:source_sync,:prepare,
25
+ :compile,:before_install,:install,
26
+ :before_start,:start,:after_start,
27
+ :complete
28
+ ],
29
+ image: [:init]
30
+ }
31
+
32
+ attr_accessor :steps, :image, :source_dir
33
+ def initialize
34
+ self.steps = {}
35
+ self.source_dir = '$HOME/source'
36
+ end
37
+
38
+ def put_to_step title, *scripts
39
+ steps[title] ||= CmdStep.new(nil, title)
40
+ steps[title].scripts += scripts.flatten
41
+ self
42
+ end
43
+
44
+ def insert_to_step title, *scripts
45
+ steps[title] ||= CmdStep.new(nil, title)
46
+ steps[title].scripts.unshift *scripts.flatten
47
+ self
48
+ end
49
+
50
+ def add_source_sync source_accessor
51
+ steps[:source_sync] = FunctionStep.new(:source_sync) do |node|
52
+ source_accessor.update_source node,source_dir
53
+ end
54
+ end
55
+
56
+ def steps_for type
57
+ groups = DEFINED_GROUPS[type]
58
+ # ignore init when image id is specified
59
+ groups = groups - [:init] if image
60
+ groups.each do |title|
61
+ yield steps[title] if steps[title]
62
+ end
63
+ end
64
+
65
+ def dump
66
+ data = DEFINED_GROUPS[:all].reduce({}) do |sum,title|
67
+ next sum if steps[title].nil?
68
+ scripts_data = steps[title].scripts.map(&:dump).compact
69
+ sum.update title.to_s => scripts_data
70
+ end
71
+ YAML.dump data
72
+ end
73
+ end
74
+
75
+ # Describe a set of scripts to accomplish a specific goal
76
+ class CmdStep
77
+ attr_accessor :scripts, :title
78
+ def initialize scripts, title
79
+ self.scripts = scripts || []
80
+ self.title = title
81
+ end
82
+
83
+ def run_on node
84
+ scripts.each do |script|
85
+ node.execute script.actual_command, base_dir: script.base_dir
86
+ end
87
+ end
88
+ end
89
+
90
+ # An abstract step which bind business logic with block
91
+ # This class designed for some typically service,eg:
92
+ # * local file folder sync
93
+ # * some service invoke
94
+ class FunctionStep
95
+ attr_accessor :block, :title
96
+ def initialize title, &block
97
+ self.title = title
98
+ self.block = block
99
+ end
100
+
101
+ def run_on node
102
+ block.call node
103
+ end
104
+ end
105
+
106
+ # store the real command line
107
+ # :cannt_fail used to declare `non-zero retcode of current script will be fail`
108
+ class Script
109
+ attr_accessor :cmd, :base_dir, :args, :cannt_fail
110
+ def initialize cmd, base_dir:nil, args:{}, cannt_fail: true
111
+ self.cmd = cmd
112
+ self.args = args
113
+ self.cannt_fail = cannt_fail
114
+ self.base_dir = base_dir
115
+ end
116
+
117
+ def actual_command
118
+ sprintf(cmd, args)
119
+ end
120
+
121
+ def dump
122
+ return nil if cmd.empty?
123
+ cmd
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,48 @@
1
+ require 'larrow/runner/manifest/base_loader'
2
+ require 'larrow/runner/manifest/configuration'
3
+
4
+ module Larrow
5
+ module Runner
6
+ # support multiple manifest style, such as travis, larrow, etc...
7
+ module Manifest
8
+ # Adapters is a set of class to adapt different manifest style.
9
+ # There isn't Adapter module, these classes are under Manifest module.
10
+ autoload :Travis, 'larrow/runner/manifest/adapter/travis'
11
+ autoload :Larrow, 'larrow/runner/manifest/adapter/larrow'
12
+ autoload :Blank, 'larrow/runner/manifest/adapter/blank'
13
+
14
+ extend self
15
+
16
+ def configuration source_accessor
17
+ [ Larrow, Travis, Blank ].each do |clazz|
18
+ configuration = clazz.new(source_accessor).load
19
+ return configuration if configuration
20
+ end
21
+ end
22
+
23
+ def add_base_scripts configuration,source_accessor
24
+ configuration.add_source_sync source_accessor
25
+ unless configuration.image
26
+ lines = <<-EOF
27
+ #{package_update}
28
+ #{bashrc_cleanup}
29
+ EOF
30
+ scripts = lines.split(/\n/).map{|s| Script.new s}
31
+ configuration.insert_to_step :init, scripts
32
+ end
33
+ end
34
+
35
+ def package_update
36
+ <<-EOF
37
+ apt-get update -qq
38
+ apt-get install git libssl-dev build-essential curl libncurses5-dev -y -qq
39
+ EOF
40
+ end
41
+
42
+ # remove PS1 check, for user to make ssh connection without tty
43
+ def bashrc_cleanup
44
+ "sed '/$PS1/ d' -i /root/.bashrc"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ module Larrow::Runner
2
+ module Model
3
+ class App
4
+
5
+ attr_accessor :vcs, :node, :configuration
6
+ def initialize vcs, attributes={}
7
+ self.vcs = vcs
8
+ self.configuration = vcs.configuration
9
+ self.assign attributes unless attributes.empty?
10
+ end
11
+
12
+ def assign arg
13
+ arg.each_pair do |k,v|
14
+ self.send "#{k}=".to_sym, v
15
+ end
16
+ end
17
+
18
+ def action group
19
+ configuration.steps_for(group) do |a_step|
20
+ RunLogger.title "[#{a_step.title}]"
21
+ begin_at = Time.new
22
+ a_step.run_on node
23
+ during = sprintf('%.2f',Time.new - begin_at)
24
+ RunLogger.level(1).detail "#{a_step.title} complete (#{during}s)"
25
+ end
26
+ end
27
+
28
+ def allocate
29
+ RunLogger.title 'allocate resource'
30
+ begin_at = Time.new
31
+ option = {image_id: configuration.image}
32
+ self.node = Node.new(*Cloud.create(option).first)
33
+ during = sprintf('%.2f', Time.new - begin_at)
34
+ RunLogger.level(1).detail "allocated(#{during}s)"
35
+ end
36
+
37
+ def build_image
38
+ action :image
39
+ node.stop
40
+ new_image = Cloud.create_image node.instance.id
41
+ RunLogger.level(1).detail "New Image Id: #{new_image.id}"
42
+ [
43
+ "To reduce the system setup, you might want to change larrow.yml.",
44
+ " You can replace init step with the follow contents:",
45
+ " image: #{new_image.id}"
46
+ ].each{|s| RunLogger.level(1).detail s}
47
+
48
+ new_image
49
+ end
50
+
51
+ def deploy
52
+ action :deploy
53
+ RunLogger.level(1).detail "application is deploy on: #{node.host}"
54
+ node
55
+ end
56
+
57
+ def dump
58
+ {nodes:[node.dump]}
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,73 @@
1
+ module Larrow::Runner
2
+ module Model
3
+ class Node
4
+ include Larrow::Runner::Service
5
+ include Larrow::Qingcloud
6
+ attr_accessor :instance, :eip
7
+ attr_accessor :user,:host
8
+ def initialize instance, eip, user='root'
9
+ self.instance = instance
10
+ self.eip = eip
11
+ self.host = eip.address
12
+ self.user = user
13
+ @executor = Executor.new host, user, nil, nil
14
+ end
15
+
16
+ def execute command, base_dir:nil
17
+ block = if block_given?
18
+ -> (data) { yield data }
19
+ else
20
+ -> (data) {
21
+ data.split(/\r?\n/).each do |msg|
22
+ RunLogger.level(1).info msg
23
+ end
24
+ }
25
+ end
26
+ @executor.execute command, base_dir: base_dir, &block
27
+ end
28
+
29
+ def stop
30
+ self.instance = instance.stop
31
+ end
32
+
33
+ def destroy
34
+ instance.destroy.force
35
+ eip.destroy.force
36
+ self
37
+ end
38
+
39
+ def dump
40
+ {
41
+ instance:{id: instance.id},
42
+ eip:{id:eip.id, address:eip.address}
43
+ }
44
+ end
45
+
46
+ def self.show resources, level=0
47
+ resources.map do |hash|
48
+ node = load_obj hash
49
+ RunLogger.level(level).info "instance: #{node.instance.id}"
50
+ RunLogger.level(level).info "eip:"
51
+ RunLogger.level(level+1).info "id: #{node.eip.id}"
52
+ RunLogger.level(level+1).info "address: #{node.eip.address}"
53
+ end
54
+ end
55
+
56
+ def self.cleanup resources
57
+ resources.map do |hash|
58
+ node = load_obj hash
59
+ future{node.destroy}
60
+ end.map do |instance|
61
+ RunLogger.detail "node cleaned: #{instance.address}"
62
+ end
63
+ end
64
+
65
+ def self.load_obj data
66
+ instance = Instance.new data[:instance][:id]
67
+ eip = Eip.new data[:eip][:id],address:data[:eip][:address]
68
+ new instance,eip
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,54 @@
1
+ require 'larrow/qingcloud'
2
+
3
+ module Larrow
4
+ module Runner
5
+ module Service
6
+ class Cloud
7
+ include Qingcloud
8
+ def initialize args={}
9
+ Qingcloud.remove_connection
10
+ access_id = args[:qy_access_key_id]
11
+ secret_key = args[:qy_secret_access_key]
12
+ zone_id = args[:zone_id]
13
+ @keypair_id = args[:keypair_id]
14
+ Qingcloud.establish_connection access_id,secret_key,zone_id
15
+ end
16
+
17
+ # return: Array< [ instance,eip ] >
18
+ # WARN: eips contains promise object, so it should be force
19
+ def create image_id:nil,count:1
20
+ RunLogger.level(1).detail "assign node"
21
+ instances = Instance.create(image_id: image_id||'trustysrvx64c',
22
+ count:count,
23
+ login_mode:'keypair',
24
+ keypair_id: @keypair_id
25
+ )
26
+
27
+ eips = Eip.create(count:count)
28
+
29
+ (0...count).map do |i|
30
+ RunLogger.level(1).detail "bind ip: #{eips[i].address}"
31
+ eips[i] = eips[i].associate instances[i].id
32
+ [ instances[i], eips[i] ]
33
+ end
34
+ end
35
+
36
+ # return image future
37
+ def create_image instance_id
38
+ Image.create instance_id
39
+ end
40
+
41
+ def image? image_id
42
+ Image.list(:self, ids: [image_id]).size == 1
43
+ end
44
+
45
+ def check_available
46
+ KeyPair.list
47
+ rescue
48
+ Qingcloud.remove_connection
49
+ raise $!
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ require 'net/ssh'
2
+ require 'net/scp'
3
+
4
+ module Larrow
5
+ module Runner
6
+ module Service
7
+ class Executor
8
+ attr_accessor :ip, :user, :port, :password
9
+ def initialize ip, user, port, password
10
+ self.ip = ip
11
+ self.user = user
12
+ self.port = port
13
+ self.password = password
14
+ @canceling = nil
15
+ @dlogger = RunLogger #::Logger.new "#{ip}_cmd.log"
16
+ end
17
+
18
+ def execute cmd, base_dir:nil
19
+ connection.open_channel do |ch|
20
+ RunLogger.level(1).detail "# #{cmd}"
21
+ cmd = "cd #{base_dir}; #{cmd}" unless base_dir.nil?
22
+ errmsg = ''
23
+ ch.exec cmd do |ch,success|
24
+ if RunOption.key? :debug
25
+ ch.on_data{ |c, data| yield data }
26
+ ch.on_extended_data{ |c, type, data| yield data }
27
+ else
28
+ ch.on_extended_data{ |c, type, data| errmsg << data }
29
+ end
30
+ ch.on_request('exit-status') do |c,data|
31
+ status = data.read_long
32
+ if status != 0
33
+ fail ExecutionError,{cmd:cmd,
34
+ errmsg: errmsg,
35
+ status: status}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ trap("INT") { @canceling = true }
41
+ connection.loop(0.1) do
42
+ not (@canceling || connection.channels.empty?)
43
+ end
44
+ end
45
+
46
+ def scp local_file_path, remote_file_path
47
+ raise 'not completed.'
48
+ end
49
+
50
+ def connection
51
+ @connection ||= Net::SSH.start(ip,user)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ module Larrow
2
+ module Runner
3
+ module Service
4
+ autoload :Cloud, 'larrow/runner/service/cloud'
5
+ autoload :Executor, 'larrow/runner/service/executor'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,64 @@
1
+ require 'yaml'
2
+ module Larrow
3
+ module Runner
4
+ module Session
5
+ extend self
6
+
7
+ FILE = "#{ENV['HOME']}/.larrow"
8
+ def login
9
+ return unless check_file
10
+ puts "The larrow config will be generated at #{FILE}."
11
+ data = nil
12
+ loop do
13
+ data = [:qy_access_key_id,
14
+ :qy_secret_access_key,
15
+ :zone_id,
16
+ :keypair_id].
17
+ reduce({}){|s,key| s.update key => value_for(key)}
18
+
19
+ cloud = Service::Cloud.new data
20
+ begin
21
+ cloud.check_available
22
+ RunLogger.info "login success! write to ~/.larrow"
23
+ break
24
+ rescue Exception => e
25
+ RunLogger.info "login fail: #{e.message}"
26
+ return unless ask "try again"
27
+ end
28
+ end
29
+ content={'qingcloud' => data}
30
+ File.write FILE, YAML.dump(content)
31
+ end
32
+
33
+ def load_cloud
34
+ args = begin
35
+ YAML.
36
+ load(File.read FILE).
37
+ with_indifferent_access[:qingcloud]
38
+ rescue
39
+ nil
40
+ end
41
+ Service::Cloud.new args if args
42
+ end
43
+
44
+ def value_for name
45
+ print sprintf("%25s: ", name)
46
+ v = $stdin.gets.strip
47
+ v.empty? ? nil : v
48
+ end
49
+
50
+ def check_file
51
+ return true unless File.exist?(FILE)
52
+ puts "#{FILE} does exist: "
53
+ puts File.read(FILE)
54
+ RunOption[:force] || ask("overwrite #{FILE}")
55
+ end
56
+
57
+ def ask title
58
+ print "#{title} ? (yes/[no]) "
59
+ v = $stdin.gets.strip
60
+ ['yes','y','Y','Yes','YES'].include? v
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,17 @@
1
+ module Larrow::Runner::Vcs
2
+ class Base
3
+ attr_accessor :larrow_file
4
+ include Larrow::Runner
5
+ def configuration merge=true
6
+ configuration = Manifest.configuration(self)
7
+ if merge
8
+ Manifest.add_base_scripts configuration,self
9
+ end
10
+ configuration
11
+ end
12
+
13
+ def formatted_url
14
+ raise 'not implement yet'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ module Larrow::Runner
2
+ module Vcs
3
+ class FileSystem < Base
4
+ # path: absulute path of LarrowFile
5
+ attr_accessor :project_folder
6
+ def initialize path
7
+ if File.file? path
8
+ path = File.absolute_path path
9
+ self.larrow_file = File.basename path
10
+ self.project_folder = File.dirname path
11
+ else # directory
12
+ self.project_folder = File.absolute_path path
13
+ end
14
+ end
15
+
16
+ def formatted_url
17
+ self.project_folder
18
+ end
19
+
20
+ def get filename
21
+ file_path = "#{project_folder}/#{filename}"
22
+ return nil unless File.exist? file_path
23
+
24
+ File.read(file_path)
25
+ end
26
+
27
+ def update_source node, target_dir
28
+ command = rsync_command node.user, node.host,target_dir
29
+ invoke command
30
+ invoke "ssh-keygen -R #{node.host} 2>&1"
31
+ end
32
+
33
+ def rsync_command user, host, target_dir
34
+ ssh_path = '%s@%s:%s' % [user, host, target_dir]
35
+
36
+ excludes = (get('.gitignore')||''). # rsync exclude according .gitignore
37
+ split(/[\r\n]/). #
38
+ select{|s| s =~ /^[^#]/}. # not commented
39
+ compact. # not blank
40
+ unshift('.git'). # .git itself is ignored
41
+ map{|s| "--exclude '#{s}'" } # build rsync exclude arguments
42
+
43
+ ssh_options = "-e 'ssh -o StrictHostKeyChecking=no'"
44
+
45
+ rsync_options = "-az #{ssh_options} #{excludes.join ' '}"
46
+ rsync_options += ' -v' if RunOption.key? :debug
47
+
48
+ "rsync #{rsync_options} #{project_folder}/ '#{ssh_path}' 2>&1"
49
+ end
50
+ def invoke command
51
+ `#{command}`.split(/\r?\n/).each do |msg|
52
+ RunLogger.level(1).info msg
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ require 'faraday'
2
+
3
+ module Larrow::Runner::Vcs
4
+ class Github < Base
5
+ URL_TEMPLATE='https://raw.githubusercontent.com/%s/%s/%s/%s'
6
+ attr_accessor :organize, :name, :branch
7
+ # url sample:
8
+ # git@github.com:fsword/larrow-qingcloud.git
9
+ # https://github.com/fsword/larrow-qingcloud.git
10
+ def initialize url
11
+ self.branch = 'master'
12
+ case url
13
+ when /git@github\.com:(.+)\/(.+)\.git/
14
+ self.organize = $1
15
+ self.name = $2
16
+ when /http.:\/\/github.com\/(.+)\/(.+)\.git/
17
+ self.organize = $1
18
+ self.name = $2
19
+ end
20
+ end
21
+
22
+ def formatted_url
23
+ 'git@github.com:%s/%s.git' % [organize, name]
24
+ end
25
+
26
+ def get filename
27
+ url = URL_TEMPLATE % [organize, name, branch, filename]
28
+ resp = Faraday.get(url)
29
+ case resp.status
30
+ when 200
31
+ resp.body
32
+ when 404
33
+ nil
34
+ else
35
+ raise resp.body
36
+ end
37
+ end
38
+
39
+ def update_source node, target_dir
40
+ template = ["git clone ",
41
+ "--depth 1",
42
+ "http://github.com/%s/%s.git",
43
+ "-b %s %s"].join(' ')
44
+ cmd = template % [organize, name, branch, target_dir]
45
+ node.execute cmd
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ require 'larrow/runner/vcs/base'
2
+ module Larrow
3
+ module Runner
4
+ # Access source code from Version Control System
5
+ # eg: Subversion, Github, LocalStore
6
+ module Vcs
7
+ autoload :Github, 'larrow/runner/vcs/github'
8
+ autoload :FileSystem,'larrow/runner/vcs/file_system'
9
+ def self.detect url
10
+ case url
11
+ when /github\.com.+\.git$/
12
+ Github.new(url)
13
+ else # local file/folder
14
+ fail "cannot recognized: #{url}" unless File.exist? url
15
+ FileSystem.new url
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Larrow
2
+ module Runner
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_support/deprecation'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ require "larrow/runner/version"
5
+ require 'larrow/runner/logger'
6
+ require 'larrow/runner/service'
7
+ require 'larrow/runner/session'
8
+
9
+ require 'larrow/runner/errors'
10
+
11
+ module Larrow
12
+ module Runner
13
+ # default runtime logger
14
+ RunLogger = if ENV['RUN_AS']
15
+ Logger.new "#{ENV['RUN_AS']}.log"
16
+ else
17
+ Logger.new $stdout
18
+ end
19
+ # global options
20
+ RunOption = {}.with_indifferent_access
21
+ # cloud wrapper
22
+ Cloud = Session.load_cloud
23
+ end
24
+ end
25
+
26
+ require 'larrow/runner/vcs'
27
+ require 'larrow/runner/manifest'
28
+ require 'larrow/runner/helper'
29
+
30
+ require 'larrow/runner/manager'
31
+ require 'larrow/runner/cli'
32
+ require 'larrow/runner/model/app'
33
+ require 'larrow/runner/model/node'
34
+
@@ -0,0 +1,8 @@
1
+ language: erlang
2
+ before_script:
3
+ - erl -verion
4
+ script:
5
+ - ./rebar compile ct
6
+ otp_release:
7
+ - R16B
8
+