buildbox 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/bin/buildbox +31 -0
- data/buildbox-ruby.gemspec +25 -0
- data/lib/buildbox.rb +37 -0
- data/lib/buildbox/api.rb +80 -0
- data/lib/buildbox/build.rb +50 -0
- data/lib/buildbox/client.rb +78 -0
- data/lib/buildbox/command.rb +67 -0
- data/lib/buildbox/configuration.rb +56 -0
- data/lib/buildbox/pid_file.rb +25 -0
- data/lib/buildbox/result.rb +7 -0
- data/lib/buildbox/utf8.rb +50 -0
- data/lib/buildbox/version.rb +3 -0
- data/lib/buildbox/worker.rb +25 -0
- data/spec/buildbox/buildbox/build_spec.rb +66 -0
- data/spec/buildbox/buildbox/command_spec.rb +115 -0
- data/spec/buildbox/buildbox/configuration_spec.rb +9 -0
- data/spec/buildbox/buildbox_spec.rb +4 -0
- data/spec/fixtures/repo.git/HEAD +1 -0
- data/spec/fixtures/repo.git/config +6 -0
- data/spec/fixtures/repo.git/description +1 -0
- data/spec/fixtures/repo.git/hooks/applypatch-msg.sample +15 -0
- data/spec/fixtures/repo.git/hooks/commit-msg.sample +24 -0
- data/spec/fixtures/repo.git/hooks/post-update.sample +8 -0
- data/spec/fixtures/repo.git/hooks/pre-applypatch.sample +14 -0
- data/spec/fixtures/repo.git/hooks/pre-commit.sample +50 -0
- data/spec/fixtures/repo.git/hooks/pre-push.sample +53 -0
- data/spec/fixtures/repo.git/hooks/pre-rebase.sample +169 -0
- data/spec/fixtures/repo.git/hooks/prepare-commit-msg.sample +36 -0
- data/spec/fixtures/repo.git/hooks/update.sample +128 -0
- data/spec/fixtures/repo.git/info/exclude +6 -0
- data/spec/fixtures/repo.git/objects/2d/762cdfd781dc4077c9f27a18969efbd186363c +2 -0
- data/spec/fixtures/repo.git/objects/3e/0c65433b241ff2c59220f80bcdcd2ebb7e4b96 +2 -0
- data/spec/fixtures/repo.git/objects/95/73fff3f9e2c38ccdd7755674ec87c31ca08270 +0 -0
- data/spec/fixtures/repo.git/objects/c4/01f49fe0172add6a09aec8a7808112ce5894dd +0 -0
- data/spec/fixtures/repo.git/objects/c9/3cd4edd7e0b6fd4c69e65fc7f25cbf06ac855c +0 -0
- data/spec/fixtures/repo.git/objects/e9/8d8a9be514ef53609a52c9e1b820dbcc8e6603 +0 -0
- data/spec/fixtures/repo.git/refs/heads/master +1 -0
- data/spec/fixtures/rspec/test_spec.rb +5 -0
- data/spec/integration/running_a_build_spec.rb +89 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/dotenv.rb +3 -0
- data/spec/support/silence_logger.rb +7 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9695125aaee3d822bac58ccacfe3acb3e6af8a9c
|
4
|
+
data.tar.gz: c03eddbac8c47d1022f0efb5dd33e0f4c1b5afd3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e957e054f341e19ef76811b343de8954fb860709f7afd7723e010c03d87cbc42d6ae5df09de42b5a1ff44143f38f82afc299d255a5e047a0726d07ac91078d8a
|
7
|
+
data.tar.gz: 52ab5893fe9cc9405a11807e16fc7d36b7e0bc5f94fe2b0a3f4bc5fd6a01e75c2e879434b906ad9ed4d9d2908c9ac5c9b6f77898e3ddf7f7e9c69dfd878b4f19
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Keith Pitt
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Ci::Ruby
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'ci-ruby'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install ci-ruby
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/buildbox
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
|
5
|
+
require 'trigger'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
COMMANDS = %w(start stop)
|
9
|
+
|
10
|
+
options = {}.tap do |options|
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.version = Trigger::VERSION
|
13
|
+
opts.banner = 'Usage: trigger command [options]'
|
14
|
+
|
15
|
+
opts.separator ""
|
16
|
+
opts.separator "Options:"
|
17
|
+
|
18
|
+
opts.on("-d", "--daemon", "Runs trigger as a daemon") do |user_agent|
|
19
|
+
options[:daemon] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
end.parse!
|
23
|
+
end
|
24
|
+
|
25
|
+
client = Trigger::Client.new(options)
|
26
|
+
|
27
|
+
if command = ARGV[0]
|
28
|
+
client.public_send(command)
|
29
|
+
else
|
30
|
+
abort 'No command specified'
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'buildbox/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "buildbox"
|
8
|
+
spec.version = Buildbox::VERSION
|
9
|
+
spec.authors = ["Keith Pitt"]
|
10
|
+
spec.email = ["me@keithpitt.com"]
|
11
|
+
spec.description = %q{Ruby client for buildbox}
|
12
|
+
spec.summary = %q{Ruby client for buildbox}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "dotenv"
|
25
|
+
end
|
data/lib/buildbox.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "buildbox/utf8"
|
2
|
+
require "buildbox/command"
|
3
|
+
require "buildbox/result"
|
4
|
+
require "buildbox/build"
|
5
|
+
require "buildbox/version"
|
6
|
+
require "buildbox/client"
|
7
|
+
require "buildbox/api"
|
8
|
+
require "buildbox/worker"
|
9
|
+
require "buildbox/pid_file"
|
10
|
+
require "buildbox/configuration"
|
11
|
+
|
12
|
+
module Buildbox
|
13
|
+
require 'fileutils'
|
14
|
+
require 'pathname'
|
15
|
+
require 'logger'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def configuration
|
19
|
+
@configuration ||= Configuration.load
|
20
|
+
end
|
21
|
+
|
22
|
+
def root_path
|
23
|
+
path = Pathname.new File.join(ENV['HOME'], ".buildbox")
|
24
|
+
path.mkpath unless path.exist?
|
25
|
+
|
26
|
+
Pathname.new(path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def logger=(logger)
|
30
|
+
@logger = logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def logger
|
34
|
+
@logger ||= Logger.new(STDOUT)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/buildbox/api.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Buildbox
|
2
|
+
class API
|
3
|
+
require 'net/http'
|
4
|
+
require 'openssl'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
def update(build, data)
|
8
|
+
put("repos/#{build.repository_uuid}/builds/#{build.uuid}", normalize_data('build' => data))
|
9
|
+
end
|
10
|
+
|
11
|
+
def scheduled(options = {})
|
12
|
+
builds = []
|
13
|
+
|
14
|
+
options[:repositories].each do |repository|
|
15
|
+
response = get("repos/#{repository}/builds/scheduled")
|
16
|
+
json = JSON.parse(response.body)
|
17
|
+
|
18
|
+
json['response']['builds'].map do |build|
|
19
|
+
# really smelly way of converting keys to symbols
|
20
|
+
builds << Build.new(symbolize_keys(build).merge(:repository_uuid => repository))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
builds
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def http(uri)
|
30
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
31
|
+
http.use_ssl = uri.scheme == "https"
|
32
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(path)
|
37
|
+
uri = URI.parse(endpoint(path))
|
38
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
39
|
+
|
40
|
+
Buildbox.logger.debug "GET #{uri}"
|
41
|
+
|
42
|
+
http(uri).request(request)
|
43
|
+
end
|
44
|
+
|
45
|
+
def put(path, data)
|
46
|
+
uri = URI.parse(endpoint(path))
|
47
|
+
request = Net::HTTP::Put.new(uri.request_uri)
|
48
|
+
request.set_form_data data
|
49
|
+
|
50
|
+
Buildbox.logger.debug "PUT #{uri}"
|
51
|
+
|
52
|
+
response = http(uri).request(request)
|
53
|
+
raise response.body unless response.code.to_i == 200
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
def endpoint(path)
|
58
|
+
(Buildbox.configuration.use_ssl ? "https://" : "http://") +
|
59
|
+
"#{Buildbox.configuration.endpoint}/v#{Buildbox.configuration.api_version}/#{path}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def symbolize_keys(hash)
|
63
|
+
Hash[hash.map{ |k, v| [k.to_sym, v] }]
|
64
|
+
end
|
65
|
+
|
66
|
+
def normalize_data(hash)
|
67
|
+
hash.inject({}) do |target, member|
|
68
|
+
key, value = member
|
69
|
+
|
70
|
+
if value.kind_of?(Hash)
|
71
|
+
value.each { |key2, value2| target["#{key}[#{key2}]"] = value2.to_s }
|
72
|
+
else
|
73
|
+
target[key] = value.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
target
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Buildbox
|
2
|
+
class Build
|
3
|
+
attr_reader :uuid, :repository_uuid
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@uuid = options[:uuid]
|
7
|
+
@repo = options[:repo]
|
8
|
+
@commit = options[:commit]
|
9
|
+
@repository_uuid = options[:repository_uuid]
|
10
|
+
@command = options[:command] || "bundle && rspec"
|
11
|
+
end
|
12
|
+
|
13
|
+
def start(&block)
|
14
|
+
checkout
|
15
|
+
update
|
16
|
+
|
17
|
+
@result = command.run(@command) do |chunk|
|
18
|
+
yield(chunk) if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def checkout
|
25
|
+
unless build_path.exist?
|
26
|
+
build_path.mkpath
|
27
|
+
|
28
|
+
command.run! %{git clone "#{@repo}" .}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def update
|
33
|
+
command.run! %{git clean -fd}
|
34
|
+
command.run! %{git fetch}
|
35
|
+
command.run! %{git checkout -qf "#{@commit}"}
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_path
|
39
|
+
Buildbox.root_path.join folder_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def folder_name
|
43
|
+
@repo.gsub(/[^a-zA-Z0-9]/, '-')
|
44
|
+
end
|
45
|
+
|
46
|
+
def command
|
47
|
+
Buildbox::Command.new(build_path)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Buildbox
|
2
|
+
class Client
|
3
|
+
def initialize(options)
|
4
|
+
@options = options
|
5
|
+
@interval = 5
|
6
|
+
end
|
7
|
+
|
8
|
+
def start
|
9
|
+
exit_if_already_running
|
10
|
+
|
11
|
+
Buildbox.logger.info "Starting client..."
|
12
|
+
|
13
|
+
begin
|
14
|
+
daemonize if @options[:daemon]
|
15
|
+
pid_file.save
|
16
|
+
|
17
|
+
loop do
|
18
|
+
reload_configuration
|
19
|
+
process_build_queue
|
20
|
+
wait_for_interval
|
21
|
+
end
|
22
|
+
ensure
|
23
|
+
pid_file.delete
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
Buildbox.logger.info "Stopping client..."
|
29
|
+
|
30
|
+
Process.kill(:KILL, pid_file.delete)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def daemonize
|
36
|
+
if @options[:daemon]
|
37
|
+
Process.daemon
|
38
|
+
|
39
|
+
Buildbox.logger = Logger.new(Buildbox.root_path.join("ci.log"))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def process_build_queue
|
45
|
+
build = api.scheduled(:repositories => Buildbox.configuration.repositories).first
|
46
|
+
|
47
|
+
Buildbox::Worker.new(build, api).run if build
|
48
|
+
end
|
49
|
+
|
50
|
+
def reload_configuration
|
51
|
+
Buildbox.logger.info "Reloading configuration"
|
52
|
+
|
53
|
+
Buildbox.configuration.reload
|
54
|
+
end
|
55
|
+
|
56
|
+
def wait_for_interval
|
57
|
+
Buildbox.logger.info "Sleeping for #{@interval} seconds"
|
58
|
+
|
59
|
+
sleep(@interval)
|
60
|
+
end
|
61
|
+
|
62
|
+
def exit_if_already_running
|
63
|
+
if pid_file.exist?
|
64
|
+
Buildbox.logger.error "Process (#{pid_file.pid} - #{pid_file.path}) is already running."
|
65
|
+
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def api
|
71
|
+
@api ||= Buildbox::API.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def pid_file
|
75
|
+
@pid_file ||= Buildbox::PidFile.new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Buildbox
|
2
|
+
class Command
|
3
|
+
require 'pty'
|
4
|
+
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
def initialize(path = nil, read_interval = nil)
|
8
|
+
@path = path || "."
|
9
|
+
@read_interval = read_interval || 5
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(command)
|
13
|
+
Buildbox.logger.debug(command)
|
14
|
+
|
15
|
+
output = ""
|
16
|
+
read_io, write_io, pid = nil
|
17
|
+
|
18
|
+
begin
|
19
|
+
dir = File.expand_path(@path)
|
20
|
+
|
21
|
+
# spawn the process in a pseudo terminal so colors out outputted
|
22
|
+
read_io, write_io, pid = PTY.spawn("cd #{dir} && #{command}")
|
23
|
+
rescue Errno::ENOENT => e
|
24
|
+
return Buildbox::Result.new(false, e.message)
|
25
|
+
end
|
26
|
+
|
27
|
+
write_io.close
|
28
|
+
|
29
|
+
loop do
|
30
|
+
fds, = IO.select([read_io], nil, nil, @read_interval)
|
31
|
+
if fds
|
32
|
+
# should have some data to read
|
33
|
+
begin
|
34
|
+
chunk = read_io.read_nonblock(10240)
|
35
|
+
if block_given?
|
36
|
+
yield chunk
|
37
|
+
end
|
38
|
+
output += chunk
|
39
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
40
|
+
# do select again
|
41
|
+
rescue EOFError, Errno::EIO # EOFError from OSX, EIO is raised by ubuntu
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# if fds are empty, timeout expired - run another iteration
|
46
|
+
end
|
47
|
+
|
48
|
+
read_io.close
|
49
|
+
Process.waitpid(pid)
|
50
|
+
|
51
|
+
# output may be invalid UTF-8, as it is produced by the build command.
|
52
|
+
output = Buildbox::UTF8.clean(output)
|
53
|
+
|
54
|
+
Buildbox::Result.new(output.chomp, $?.exitstatus)
|
55
|
+
end
|
56
|
+
|
57
|
+
def run!(command)
|
58
|
+
result = run(command)
|
59
|
+
|
60
|
+
unless result.success?
|
61
|
+
raise Error, "Failed to run '#{command}': #{result.output}"
|
62
|
+
end
|
63
|
+
|
64
|
+
result
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|