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.
@@ -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
@@ -1,46 +1,40 @@
1
+ require 'pty'
2
+
1
3
  module Buildbox
2
4
  class Command
3
- require 'pty'
4
-
5
- class Error < StandardError; end
5
+ class Result < Struct.new(:output, :exit_status)
6
+ end
6
7
 
7
- def initialize(path = nil, read_interval = nil)
8
- @path = path || "."
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 run(command)
13
- Buildbox.logger.debug(command)
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
- # spawn the process in a pseudo terminal so colors out outputted
27
- read_io, write_io, pid = PTY.spawn("cd #{dir} && #{command}")
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, @read_interval)
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 = read_io.read_nonblock(10240)
40
- if block_given?
41
- yield result, chunk
42
- end
43
- result.output += chunk
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
- # output may be invalid UTF-8, as it is produced by the build command.
57
- result.finished = true
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
- def run!(command)
64
- result = run(command)
57
+ private
65
58
 
66
- unless result.success?
67
- raise Error, "Failed to run '#{command}': #{result.output}"
68
- end
59
+ def expanded_directory
60
+ File.expand_path(@directory)
61
+ end
69
62
 
70
- result
63
+ def read_interval
64
+ @read_interval
71
65
  end
72
66
  end
73
67
  end
@@ -1,61 +1,37 @@
1
- module Buildbox
2
- class Configuration
3
- def self.load(*args)
4
- new(*args).tap &:reload
5
- end
6
-
7
- require 'json'
1
+ require 'rubygems'
2
+ require 'hashie/dash'
3
+ require 'json'
8
4
 
9
- attr_accessor :worker_uuid
10
- attr_accessor :api_key
11
- attr_accessor :endpoint
12
- attr_accessor :use_ssl
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(key, value)
22
- self.public_send("#{key}=", value)
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+') do |file|
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
- json = if path.exist?
35
- read
36
- else
37
- save && read
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 to_json
48
- JSON.pretty_generate(:endpoint => endpoint,
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 read
56
- Buildbox.logger.debug "Reading configuration `#{path}`"
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
@@ -1,3 +1,3 @@
1
1
  module Buildbox
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1"
3
3
  end
@@ -1,21 +1,37 @@
1
1
  module Buildbox
2
2
  class Worker
3
- def process
4
- if scheduled = api.builds.payload.first
5
- start Build.new(scheduled)
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 start(build)
12
- api.update_build_state(build.uuid, 'started')
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 api
18
- @api ||= Buildbox::API.new(:api_key => Buildbox.configuration.api_key)
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 = command.run('echo hello world')
8
+ result = Buildbox::Command.run('echo hello world')
9
9
 
10
- result.should be_success
11
- result.output.should == 'hello world'
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 = command.run('echo hello world 1>&2')
15
+ result = Buildbox::Command.run('echo hello world 1>&2')
16
16
 
17
- result.should be_success
18
- result.output.should == 'hello world'
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 = command.run('(exit 1)')
22
+ result = Buildbox::Command.run('(exit 1)')
23
23
 
24
- result.should_not be_success
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 = command.run('if (')
29
+ result = Buildbox::Command.run('if (')
30
30
 
31
- result.should_not be_success
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 = command.run('echo hello world') do |result, chunk|
43
- chunked_output += chunk
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 be_success
47
- result.output.should == 'hello world'
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 = command.run('sleep 0.5; echo hello world') do |result, chunk|
56
- chunked_output += chunk
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 be_success
60
- result.output.should == 'hello world'
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 = command.run('echo before sleep; sleep 1; echo after sleep') do |result, chunk|
71
- chunked_output += chunk
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 be_success
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 = command.run('sillycommandlololol')
93
- second_result = command.run('export FOO=bar; doesntexist.rb')
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 be_success
101
+ result.exit_status.should_not == 0
98
102
  result.output.should =~ /sillycommandlololol.+not found/
99
103
 
100
- second_result.should_not be_success
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 = command.run("rspec #{FIXTURES_PATH.join('rspec', 'test_spec.rb')} --color") do |result, chunk|
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 be_success
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