buildbox 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/bin/buildbox +31 -0
  8. data/buildbox-ruby.gemspec +25 -0
  9. data/lib/buildbox.rb +37 -0
  10. data/lib/buildbox/api.rb +80 -0
  11. data/lib/buildbox/build.rb +50 -0
  12. data/lib/buildbox/client.rb +78 -0
  13. data/lib/buildbox/command.rb +67 -0
  14. data/lib/buildbox/configuration.rb +56 -0
  15. data/lib/buildbox/pid_file.rb +25 -0
  16. data/lib/buildbox/result.rb +7 -0
  17. data/lib/buildbox/utf8.rb +50 -0
  18. data/lib/buildbox/version.rb +3 -0
  19. data/lib/buildbox/worker.rb +25 -0
  20. data/spec/buildbox/buildbox/build_spec.rb +66 -0
  21. data/spec/buildbox/buildbox/command_spec.rb +115 -0
  22. data/spec/buildbox/buildbox/configuration_spec.rb +9 -0
  23. data/spec/buildbox/buildbox_spec.rb +4 -0
  24. data/spec/fixtures/repo.git/HEAD +1 -0
  25. data/spec/fixtures/repo.git/config +6 -0
  26. data/spec/fixtures/repo.git/description +1 -0
  27. data/spec/fixtures/repo.git/hooks/applypatch-msg.sample +15 -0
  28. data/spec/fixtures/repo.git/hooks/commit-msg.sample +24 -0
  29. data/spec/fixtures/repo.git/hooks/post-update.sample +8 -0
  30. data/spec/fixtures/repo.git/hooks/pre-applypatch.sample +14 -0
  31. data/spec/fixtures/repo.git/hooks/pre-commit.sample +50 -0
  32. data/spec/fixtures/repo.git/hooks/pre-push.sample +53 -0
  33. data/spec/fixtures/repo.git/hooks/pre-rebase.sample +169 -0
  34. data/spec/fixtures/repo.git/hooks/prepare-commit-msg.sample +36 -0
  35. data/spec/fixtures/repo.git/hooks/update.sample +128 -0
  36. data/spec/fixtures/repo.git/info/exclude +6 -0
  37. data/spec/fixtures/repo.git/objects/2d/762cdfd781dc4077c9f27a18969efbd186363c +2 -0
  38. data/spec/fixtures/repo.git/objects/3e/0c65433b241ff2c59220f80bcdcd2ebb7e4b96 +2 -0
  39. data/spec/fixtures/repo.git/objects/95/73fff3f9e2c38ccdd7755674ec87c31ca08270 +0 -0
  40. data/spec/fixtures/repo.git/objects/c4/01f49fe0172add6a09aec8a7808112ce5894dd +0 -0
  41. data/spec/fixtures/repo.git/objects/c9/3cd4edd7e0b6fd4c69e65fc7f25cbf06ac855c +0 -0
  42. data/spec/fixtures/repo.git/objects/e9/8d8a9be514ef53609a52c9e1b820dbcc8e6603 +0 -0
  43. data/spec/fixtures/repo.git/refs/heads/master +1 -0
  44. data/spec/fixtures/rspec/test_spec.rb +5 -0
  45. data/spec/integration/running_a_build_spec.rb +89 -0
  46. data/spec/spec_helper.rb +18 -0
  47. data/spec/support/dotenv.rb +3 -0
  48. data/spec/support/silence_logger.rb +7 -0
  49. metadata +177 -0
@@ -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
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ spec/tmp
16
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ci-ruby.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -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
@@ -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