buildbox 0.0.4 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.ruby-version +1 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +49 -0
- data/README.md +3 -3
- data/Rakefile +4 -0
- data/bin/buildbox +2 -94
- data/buildbox-ruby.gemspec +5 -4
- data/lib/buildbox.rb +20 -33
- data/lib/buildbox/api.rb +32 -87
- data/lib/buildbox/build.rb +10 -64
- data/lib/buildbox/cli.rb +92 -0
- data/lib/buildbox/command.rb +33 -39
- data/lib/buildbox/configuration.rb +18 -42
- data/lib/buildbox/environment.rb +17 -0
- data/lib/buildbox/monitor.rb +25 -0
- data/lib/buildbox/runner.rb +59 -0
- data/lib/buildbox/version.rb +1 -1
- data/lib/buildbox/worker.rb +25 -9
- data/spec/buildbox/buildbox/command_spec.rb +46 -33
- data/spec/integration/running_a_build_spec.rb +140 -23
- data/spec/support/silence_logger.rb +6 -1
- data/spec/support/webmock.rb +1 -0
- metadata +47 -40
- data/.buildbox.toml +0 -2
- data/lib/buildbox/auth.rb +0 -37
- data/lib/buildbox/client.rb +0 -86
- data/lib/buildbox/observer.rb +0 -59
- data/lib/buildbox/pid_file.rb +0 -25
- data/lib/buildbox/response.rb +0 -49
- data/lib/buildbox/result.rb +0 -36
- data/spec/buildbox/buildbox/build_spec.rb +0 -66
- data/spec/buildbox/buildbox/configuration_spec.rb +0 -9
- data/spec/buildbox/buildbox_spec.rb +0 -4
- data/spec/support/dotenv.rb +0 -3
data/lib/buildbox/cli.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Buildbox
|
4
|
+
class CLI
|
5
|
+
attr_reader :argv
|
6
|
+
|
7
|
+
def initialize(argv)
|
8
|
+
@argv = argv
|
9
|
+
@commands = {}
|
10
|
+
@options = {}
|
11
|
+
|
12
|
+
@commands['worker:start'] = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: buildbox worker:start"
|
14
|
+
|
15
|
+
opts.on("--help", "You're looking at it.") do
|
16
|
+
puts @commands['worker:start']
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@commands['worker:setup'] = OptionParser.new do |opts|
|
22
|
+
opts.banner = "Usage: buildbox worker:setup [token]"
|
23
|
+
|
24
|
+
opts.on("--help", "You're looking at it.") do
|
25
|
+
puts @commands['worker:setup']
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@commands['version'] = OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: buildbox version"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse
|
36
|
+
global.order!
|
37
|
+
|
38
|
+
command = @argv.shift
|
39
|
+
|
40
|
+
if command
|
41
|
+
if @commands.has_key?(command)
|
42
|
+
@commands[command].parse!
|
43
|
+
else
|
44
|
+
puts "`#{command}` is an unknown command"
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
|
48
|
+
if command == "version"
|
49
|
+
puts Buildbox::VERSION
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
|
53
|
+
if command == "worker:start"
|
54
|
+
Buildbox::Worker.new(Buildbox.config.worker_access_token).start
|
55
|
+
elsif command == "worker:setup"
|
56
|
+
if @argv.length == 0
|
57
|
+
puts "No token provided"
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
|
61
|
+
access_token = @argv.first
|
62
|
+
Buildbox.config.update(:worker_access_token => access_token)
|
63
|
+
|
64
|
+
puts "Successfully added worker access token"
|
65
|
+
puts "You can now start the worker with `buildbox worker:start`"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
puts global.help
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def global
|
75
|
+
@global ||= OptionParser.new do |opts|
|
76
|
+
opts.version = Buildbox::VERSION
|
77
|
+
opts.banner = 'Usage: buildbox COMMAND [command-specific-actions]'
|
78
|
+
|
79
|
+
opts.separator help
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def help
|
84
|
+
<<HELP
|
85
|
+
|
86
|
+
worker # worker management (setup, server)
|
87
|
+
version # display version
|
88
|
+
|
89
|
+
HELP
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/buildbox/command.rb
CHANGED
@@ -1,46 +1,40 @@
|
|
1
|
+
require 'pty'
|
2
|
+
|
1
3
|
module Buildbox
|
2
4
|
class Command
|
3
|
-
|
4
|
-
|
5
|
-
class Error < StandardError; end
|
5
|
+
class Result < Struct.new(:output, :exit_status)
|
6
|
+
end
|
6
7
|
|
7
|
-
def
|
8
|
-
|
9
|
-
@read_interval = read_interval || 5
|
8
|
+
def self.run(command, options = {}, &block)
|
9
|
+
new(command, options).run(&block)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
|
12
|
+
def initialize(command, options = {})
|
13
|
+
@command = command
|
14
|
+
@directory = options[:directory] || "."
|
15
|
+
@read_interval = options[:read_interval] || 5
|
16
|
+
end
|
14
17
|
|
18
|
+
def run(&block)
|
19
|
+
output = ""
|
15
20
|
read_io, write_io, pid = nil
|
16
|
-
result = Buildbox::Result.new(command)
|
17
|
-
|
18
|
-
# hack: this is so the observer class can raise a started event.
|
19
|
-
# instead of having a block passed to this command, we should implement
|
20
|
-
# a proper command observer
|
21
|
-
yield result
|
22
|
-
|
23
|
-
begin
|
24
|
-
dir = File.expand_path(@path)
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
rescue Errno::ENOENT => e
|
29
|
-
return Buildbox::Result.new(false, e.message)
|
30
|
-
end
|
22
|
+
# spawn the process in a pseudo terminal so colors out outputted
|
23
|
+
read_io, write_io, pid = PTY.spawn("cd #{expanded_directory} && #{@command}")
|
31
24
|
|
25
|
+
# we don't need to write to the spawned io
|
32
26
|
write_io.close
|
33
27
|
|
34
28
|
loop do
|
35
|
-
fds, = IO.select([read_io], nil, nil,
|
29
|
+
fds, = IO.select([read_io], nil, nil, read_interval)
|
36
30
|
if fds
|
37
31
|
# should have some data to read
|
38
32
|
begin
|
39
|
-
chunk
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
33
|
+
chunk = read_io.read_nonblock(10240)
|
34
|
+
cleaned_chunk = UTF8.clean(chunk)
|
35
|
+
|
36
|
+
output << chunk
|
37
|
+
yield cleaned_chunk if block_given?
|
44
38
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
45
39
|
# do select again
|
46
40
|
rescue EOFError, Errno::EIO # EOFError from OSX, EIO is raised by ubuntu
|
@@ -50,24 +44,24 @@ module Buildbox
|
|
50
44
|
# if fds are empty, timeout expired - run another iteration
|
51
45
|
end
|
52
46
|
|
47
|
+
# we're done reading, yay!
|
53
48
|
read_io.close
|
54
|
-
Process.waitpid(pid)
|
55
49
|
|
56
|
-
#
|
57
|
-
|
58
|
-
result.exit_status = $?.exitstatus
|
50
|
+
# just wait until its finally finished closing
|
51
|
+
Process.waitpid(pid)
|
59
52
|
|
60
|
-
result
|
53
|
+
# the final result!
|
54
|
+
Result.new(output.chomp, $?.exitstatus)
|
61
55
|
end
|
62
56
|
|
63
|
-
|
64
|
-
result = run(command)
|
57
|
+
private
|
65
58
|
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
def expanded_directory
|
60
|
+
File.expand_path(@directory)
|
61
|
+
end
|
69
62
|
|
70
|
-
|
63
|
+
def read_interval
|
64
|
+
@read_interval
|
71
65
|
end
|
72
66
|
end
|
73
67
|
end
|
@@ -1,61 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
new(*args).tap &:reload
|
5
|
-
end
|
6
|
-
|
7
|
-
require 'json'
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hashie/dash'
|
3
|
+
require 'json'
|
8
4
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
attr_accessor :api_version
|
14
|
-
|
15
|
-
def initialize
|
16
|
-
@use_ssl = true
|
17
|
-
@endpoint = 'api.buildbox.io'
|
18
|
-
@api_version = 1
|
19
|
-
end
|
5
|
+
module Buildbox
|
6
|
+
class Configuration < Hashie::Dash
|
7
|
+
property :worker_access_token, :default => nil
|
8
|
+
property :api_endpoint, :default => "http://api.buildbox.dev/v1"
|
20
9
|
|
21
|
-
def update(
|
22
|
-
|
10
|
+
def update(attributes)
|
11
|
+
attributes.each_pair { |key, value| self[key] = value }
|
23
12
|
save
|
24
13
|
end
|
25
14
|
|
26
15
|
def save
|
27
|
-
File.open(path, 'w+')
|
28
|
-
file.write(to_json)
|
29
|
-
end
|
30
|
-
Buildbox.logger.debug "Configuration saved to `#{path}`"
|
16
|
+
File.open(path, 'w+') { |file| file.write(pretty_json) }
|
31
17
|
end
|
32
18
|
|
33
19
|
def reload
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
json.each_pair do |key, value|
|
41
|
-
self.public_send("#{key}=", value)
|
20
|
+
if path.exist?
|
21
|
+
read_and_load
|
22
|
+
else
|
23
|
+
save && read_and_load
|
42
24
|
end
|
43
25
|
end
|
44
26
|
|
45
27
|
private
|
46
28
|
|
47
|
-
def
|
48
|
-
JSON.pretty_generate(
|
49
|
-
:use_ssl => use_ssl,
|
50
|
-
:api_version => api_version,
|
51
|
-
:api_key => api_key,
|
52
|
-
:worker_uuid => worker_uuid)
|
29
|
+
def pretty_json
|
30
|
+
JSON.pretty_generate(self)
|
53
31
|
end
|
54
32
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
JSON.parse(path.read)
|
33
|
+
def read_and_load
|
34
|
+
merge! JSON.parse(path.read)
|
59
35
|
end
|
60
36
|
|
61
37
|
def path
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Buildbox
|
2
|
+
class Environment
|
3
|
+
def initialize(environment)
|
4
|
+
@environment = environment
|
5
|
+
end
|
6
|
+
|
7
|
+
def any?
|
8
|
+
@environment && @environment.keys.length > 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@environment.to_a.map do |key, value|
|
13
|
+
%{#{key}=#{value.inspect}}
|
14
|
+
end.join(" ")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'celluloid'
|
3
|
+
|
4
|
+
module Buildbox
|
5
|
+
class Monitor
|
6
|
+
include Celluloid
|
7
|
+
|
8
|
+
def initialize(build, api)
|
9
|
+
@build = build
|
10
|
+
@api = api
|
11
|
+
end
|
12
|
+
|
13
|
+
def monitor
|
14
|
+
loop do
|
15
|
+
@api.update_build(@build) if @build.started?
|
16
|
+
|
17
|
+
if @build.finished?
|
18
|
+
break
|
19
|
+
else
|
20
|
+
sleep 1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'celluloid'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Buildbox
|
7
|
+
class Runner
|
8
|
+
include Celluloid
|
9
|
+
include Celluloid::Logger
|
10
|
+
|
11
|
+
attr_reader :build
|
12
|
+
|
13
|
+
def initialize(build)
|
14
|
+
@build = build
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
info "Starting to build #{namespace}/#{@build.number} starting..."
|
19
|
+
info "Running command: #{command}"
|
20
|
+
|
21
|
+
FileUtils.mkdir_p(directory_path)
|
22
|
+
File.open(script_path, 'w+') { |file| file.write(@build.script) }
|
23
|
+
|
24
|
+
build.output = ""
|
25
|
+
result = Command.run(command, :directory => directory_path) do |chunk|
|
26
|
+
build.output << chunk
|
27
|
+
end
|
28
|
+
|
29
|
+
build.output = result.output
|
30
|
+
build.exit_status = result.exit_status
|
31
|
+
|
32
|
+
info "#{namespace}/#{@build.number} finished"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def command
|
38
|
+
export = environment.any? ? "export #{environment};" : ""
|
39
|
+
|
40
|
+
"#{export} chmod +x #{script_path} && #{script_path}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def directory_path
|
44
|
+
@directory_path ||= Buildbox.root_path.join(namespace)
|
45
|
+
end
|
46
|
+
|
47
|
+
def script_path
|
48
|
+
@script_path ||= Tempfile.new("buildbox-#{namespace.gsub(/\//, '-')}-#{@build.number}").path
|
49
|
+
end
|
50
|
+
|
51
|
+
def namespace
|
52
|
+
"#{@build.project.team.name}/#{@build.project.name}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def environment
|
56
|
+
@environment ||= Environment.new(@build.env)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/buildbox/version.rb
CHANGED
data/lib/buildbox/worker.rb
CHANGED
@@ -1,21 +1,37 @@
|
|
1
1
|
module Buildbox
|
2
2
|
class Worker
|
3
|
-
def
|
4
|
-
|
5
|
-
|
3
|
+
def initialize(access_token)
|
4
|
+
@access_token = access_token
|
5
|
+
end
|
6
|
+
|
7
|
+
def start
|
8
|
+
loop do
|
9
|
+
projects.each do |project|
|
10
|
+
running_builds = api.scheduled_builds(project).map do |build|
|
11
|
+
Monitor.new(build, api).async.monitor
|
12
|
+
Runner.new(build).future(:start)
|
13
|
+
end
|
14
|
+
|
15
|
+
# wait for all the running builds to finish
|
16
|
+
running_builds.map(&:value)
|
17
|
+
|
18
|
+
sleep 5
|
19
|
+
end
|
6
20
|
end
|
7
21
|
end
|
8
22
|
|
9
23
|
private
|
10
24
|
|
11
|
-
def
|
12
|
-
api.
|
13
|
-
build.start Buildbox::Observer.new(api, build.uuid)
|
14
|
-
api.update_build_state_async(build.uuid, 'finished')
|
25
|
+
def api
|
26
|
+
@api ||= Buildbox::API.new
|
15
27
|
end
|
16
28
|
|
17
|
-
def
|
18
|
-
|
29
|
+
def projects
|
30
|
+
api.worker(:access_token => @access_token, :hostname => hostname).projects
|
31
|
+
end
|
32
|
+
|
33
|
+
def hostname
|
34
|
+
`hostname`.chomp
|
19
35
|
end
|
20
36
|
end
|
21
37
|
end
|
@@ -1,34 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe Buildbox::Command do
|
4
|
-
let(:command) { Buildbox::Command.new }
|
5
|
-
|
6
6
|
describe "#run" do
|
7
7
|
it "successfully runs and returns the output from a simple comment" do
|
8
|
-
result =
|
8
|
+
result = Buildbox::Command.run('echo hello world')
|
9
9
|
|
10
|
-
result.should
|
11
|
-
result.output.should ==
|
10
|
+
result.exit_status.should == 0
|
11
|
+
result.output.should == "hello world"
|
12
12
|
end
|
13
13
|
|
14
14
|
it "redirects stdout to stderr" do
|
15
|
-
result =
|
15
|
+
result = Buildbox::Command.run('echo hello world 1>&2')
|
16
16
|
|
17
|
-
result.should
|
18
|
-
result.output.should ==
|
17
|
+
result.exit_status.should == 0
|
18
|
+
result.output.should == "hello world"
|
19
19
|
end
|
20
20
|
|
21
21
|
it "handles commands that fail and returns the correct status" do
|
22
|
-
result =
|
22
|
+
result = Buildbox::Command.run('(exit 1)')
|
23
23
|
|
24
|
-
result.should_not
|
24
|
+
result.exit_status.should_not == 0
|
25
25
|
result.output.should == ''
|
26
26
|
end
|
27
27
|
|
28
28
|
it "handles running malformed commands" do
|
29
|
-
result =
|
29
|
+
result = Buildbox::Command.run('if (')
|
30
30
|
|
31
|
-
result.should_not
|
31
|
+
result.exit_status.should_not == 0
|
32
32
|
# bash 3.2.48 prints "syntax error" in lowercase.
|
33
33
|
# freebsd 9.1 /bin/sh prints "Syntax error" with capital S.
|
34
34
|
# zsh 5.0.2 prints "parse error" which we do not handle.
|
@@ -39,36 +39,40 @@ describe Buildbox::Command do
|
|
39
39
|
|
40
40
|
it "can collect output in chunks" do
|
41
41
|
chunked_output = ''
|
42
|
-
result =
|
43
|
-
|
42
|
+
result = Buildbox::Command.run('echo hello world') do |chunk|
|
43
|
+
unless chunk.nil?
|
44
|
+
chunked_output += chunk
|
45
|
+
end
|
44
46
|
end
|
45
47
|
|
46
|
-
result.should
|
47
|
-
result.output.should ==
|
48
|
+
result.exit_status.should == 0
|
49
|
+
result.output.should == "hello world"
|
48
50
|
chunked_output.should == "hello world\r\n"
|
49
51
|
end
|
50
52
|
|
51
53
|
it "can collect chunks at paticular intervals" do
|
52
|
-
command = Buildbox::Command.new(nil, 0.1)
|
54
|
+
command = Buildbox::Command.new(nil, :read_interval => 0.1)
|
53
55
|
|
54
56
|
chunked_output = ''
|
55
|
-
result =
|
56
|
-
|
57
|
+
result = Buildbox::Command.run('sleep 0.5; echo hello world') do |chunk|
|
58
|
+
unless chunk.nil?
|
59
|
+
chunked_output += chunk
|
60
|
+
end
|
57
61
|
end
|
58
62
|
|
59
|
-
result.should
|
60
|
-
result.output.should ==
|
63
|
+
result.exit_status.should == 0
|
64
|
+
result.output.should == "hello world"
|
61
65
|
chunked_output.should == "hello world\r\n"
|
62
66
|
end
|
63
67
|
|
64
|
-
it 'passes a result object to the block'
|
65
|
-
|
66
68
|
it "can collect chunks from within a thread" do
|
67
69
|
chunked_output = ''
|
68
70
|
result = nil
|
69
71
|
worker_thread = Thread.new do
|
70
|
-
result =
|
71
|
-
|
72
|
+
result = Buildbox::Command.run('echo before sleep; sleep 1; echo after sleep') do |chunk|
|
73
|
+
unless chunk.nil?
|
74
|
+
chunked_output += chunk
|
75
|
+
end
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
@@ -80,7 +84,7 @@ describe Buildbox::Command do
|
|
80
84
|
worker_thread.join
|
81
85
|
|
82
86
|
result.should_not be_nil
|
83
|
-
result.should
|
87
|
+
result.exit_status.should == 0
|
84
88
|
result.output.should == "before sleep\r\nafter sleep"
|
85
89
|
chunked_output.should == "before sleep\r\nafter sleep\r\n"
|
86
90
|
end
|
@@ -89,15 +93,15 @@ describe Buildbox::Command do
|
|
89
93
|
result = nil
|
90
94
|
second_result = nil
|
91
95
|
thread = Thread.new do
|
92
|
-
result =
|
93
|
-
second_result =
|
96
|
+
result = Buildbox::Command.run('sillycommandlololol')
|
97
|
+
second_result = Buildbox::Command.run('export FOO=bar; doesntexist.rb')
|
94
98
|
end
|
95
99
|
thread.join
|
96
100
|
|
97
|
-
result.should_not
|
101
|
+
result.exit_status.should_not == 0
|
98
102
|
result.output.should =~ /sillycommandlololol.+not found/
|
99
103
|
|
100
|
-
second_result.should_not
|
104
|
+
second_result.exit_status.should_not == 0
|
101
105
|
# osx: `sh: doesntexist.rb: command not found`
|
102
106
|
# ubuntu: `sh: 1: doesntexist.rb: not found`
|
103
107
|
second_result.output.should =~ /doesntexist.rb:.+not found/
|
@@ -105,13 +109,22 @@ describe Buildbox::Command do
|
|
105
109
|
|
106
110
|
it "captures color'd output" do
|
107
111
|
chunked_output = ''
|
108
|
-
result =
|
109
|
-
chunked_output += chunk
|
112
|
+
result = Buildbox::Command.run("rspec #{FIXTURES_PATH.join('rspec', 'test_spec.rb')} --color") do |chunk|
|
113
|
+
chunked_output += chunk unless chunk.nil?
|
110
114
|
end
|
111
115
|
|
112
|
-
result.should
|
116
|
+
result.exit_status.should == 0
|
113
117
|
result.output.should include("32m")
|
114
118
|
chunked_output.should include("32m")
|
115
119
|
end
|
120
|
+
|
121
|
+
it "supports utf8 characters" do
|
122
|
+
result = Buildbox::Command.run('echo "hello"; echo "\xE2\x98\xA0"')
|
123
|
+
|
124
|
+
result.exit_status.should == 0
|
125
|
+
# just trying to interact with the string that has utf8 in it to make sure that it
|
126
|
+
# doesn't blow up like it doesn on osx. this is hacky - need a better test.
|
127
|
+
added = result.output + "hello"
|
128
|
+
end
|
116
129
|
end
|
117
130
|
end
|