buildbox 0.0.4 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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