larrow-runner 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+