buildbox 0.0.4 → 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.
- 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
|