buildbox 0.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.
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