bob-the-builder 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ dist/*
3
+ tmp/*
4
+ test/tmp/*
5
+ doc/*
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2008-2009 Nicolas Sanguinetti, entp.com
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 NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ = Bob the Builder
2
+
3
+ Given a Buildable object with the following public API:
4
+
5
+ * <tt>buildable.kind</tt>
6
+
7
+ Should return a Symbol with whatever kind of repository the buildable's code is
8
+ in (:git, :svn, etc).
9
+
10
+ * <tt>buildable.uri</tt>
11
+
12
+ Returns a string like "git://github.com/integrity/bob.git", pointing to the code
13
+ repository.
14
+
15
+ * <tt>buildable.branch</tt>
16
+
17
+ What branch of the repository should we build?
18
+
19
+ * <tt>buildable.build_script</tt>
20
+
21
+ Returns a string containing the build script to be run when "building".
22
+
23
+ * <tt>buildable.start_building(commit_id)</tt>
24
+
25
+ `commit_id` is a String that contains whatever is appropriate for the repo type,
26
+ so it would be a SHA1 hash for git repos, or a numeric id for svn, etc. This is a
27
+ callback so the buildable can determine how long it takes to build. It doesn't
28
+ need to return anything.
29
+
30
+ * <tt>buildable.finish_building(commit_id, build_status, build_output)</tt>
31
+
32
+ Callback for when the build finishes. It doesn't need to return anything. It will
33
+ receive a string with the commit identifier, a boolean for the build exit status
34
+ (true for successful builds, false fore failed ones) and a string with the build
35
+ output (both STDOUT and STDERR).
36
+
37
+ A successful build is one where the build script returns a zero status code.
38
+
39
+ Bob will, when called like:
40
+
41
+ Bob.build(buildable, commit_id)
42
+
43
+ 1. Checkout the buildable on the specified commit
44
+ 2. Call <tt>buildable.start_building</tt>
45
+ 3. Run the script provided in <tt>build_script</tt> in the buildable.
46
+ 4. When the build process finishes, it will call <tt>finish_building</tt> with
47
+ the commit_id, the build status (true if the script returns a status code
48
+ of 0, false otherwise), and a string with the build output (both STDOUT and STDERR).
49
+
50
+ == Do I need this?
51
+
52
+ Probably not. Check out integrity[http://integrityapp.com] for a full fledged
53
+ automated CI server, which is what most people need.
54
+
55
+ == Credits
56
+
57
+ Authors:: Nicolas Sanguinetti (foca[http://github.com/foca]) and Simon Rozet (sr[http://github.com/sr])
58
+ License:: MIT (Check LICENSE for details)
@@ -0,0 +1,43 @@
1
+ require "rake/testtask"
2
+
3
+ begin
4
+ require "hanna/rdoctask"
5
+ rescue LoadError
6
+ require "rake/rdoctask"
7
+ end
8
+
9
+ begin
10
+ require "metric_fu"
11
+ rescue LoadError
12
+ end
13
+
14
+ begin
15
+ require "mg"
16
+ MG.new("bob-the-builder.gemspec")
17
+ rescue LoadError
18
+ end
19
+
20
+ desc "Default: run all tests"
21
+ task :default => :test
22
+
23
+ SCMs = %w[git svn]
24
+
25
+ desc "Run unit tests"
26
+ task :test => SCMs.map { |scm| "test:#{scm}" } do
27
+ ruby "test/bob_test.rb"
28
+ ruby "test/background_engine/threaded_test.rb"
29
+ end
30
+
31
+ SCMs.each { |scm|
32
+ desc "Run unit tests with #{scm}"
33
+ task "test:#{scm}" do
34
+ ruby "test/scm/#{scm}_test.rb"
35
+ end
36
+ }
37
+
38
+ Rake::RDocTask.new do |rd|
39
+ rd.main = "README"
40
+ rd.title = "Documentation for Bob the Builder"
41
+ rd.rdoc_files.include("README.rdoc", "LICENSE", "lib/**/*.rb")
42
+ rd.rdoc_dir = "doc"
43
+ end
@@ -0,0 +1,46 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "bob-the-builder"
3
+ s.version = "0.1"
4
+ s.date = "2009-05-05"
5
+
6
+ s.description = "Bob the Builder will build your code. Simple."
7
+ s.summary = "Bob builds!"
8
+ s.homepage = "http://integrityapp.com"
9
+
10
+ s.authors = ["Nicolás Sanguinetti", "Simon Rozet"]
11
+ s.email = "info@integrityapp.com"
12
+
13
+ s.require_paths = ["lib"]
14
+ s.rubyforge_project = "bob-the-builder"
15
+ s.has_rdoc = true
16
+ s.rubygems_version = "1.3.1"
17
+
18
+ s.add_dependency "addressable"
19
+
20
+ if s.respond_to?(:add_development_dependency)
21
+ s.add_development_dependency "sr-mg"
22
+ s.add_development_dependency "contest"
23
+ s.add_development_dependency "redgreen"
24
+ s.add_development_dependency "ruby-debug"
25
+ end
26
+
27
+ s.files = %w[
28
+ .gitignore
29
+ LICENSE
30
+ README.rdoc
31
+ Rakefile
32
+ bob-the-builder.gemspec
33
+ lib/bob.rb
34
+ lib/bob/background_engines.rb
35
+ lib/bob/background_engines/foreground.rb
36
+ lib/bob/builder.rb
37
+ lib/bob/scm.rb
38
+ lib/bob/scm/abstract.rb
39
+ lib/bob/scm/git.rb
40
+ lib/core_ext/object.rb
41
+ test/bob_test.rb
42
+ test/helper.rb
43
+ test/helper/git_helper.rb
44
+ test/helper/buildable_stub.rb
45
+ ]
46
+ end
@@ -0,0 +1,41 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+ require "logger"
4
+ require "time"
5
+ require "addressable/uri"
6
+
7
+ require "bob/builder"
8
+ require "bob/scm"
9
+ require "bob/background_engines"
10
+
11
+ module Bob
12
+ # Builds the specified <tt>buildable</tt>. This object must understand
13
+ # the API described in the README.
14
+ def self.build(buildable, commit_ids)
15
+ Array(commit_ids).each do |commit_id|
16
+ Builder.new(buildable, commit_id).build
17
+ end
18
+ end
19
+
20
+ # Directory where the code for the different buildables will be checked out. Make sure
21
+ # the user running Bob is allowed to write to this directory.
22
+ def self.directory
23
+ @directory || "/tmp"
24
+ end
25
+
26
+ # What will you use to build in background. Must respond to <tt>call</tt> and take a block
27
+ # which will be run "in background". The default is to run in foreground.
28
+ def self.engine
29
+ @engine || BackgroundEngines::Foreground
30
+ end
31
+
32
+ # What to log with (must implement ruby's Logger interface). Logs to STDOUT by
33
+ # default.
34
+ def self.logger
35
+ @logger || Logger.new(STDOUT)
36
+ end
37
+
38
+ class << self
39
+ attr_writer :directory, :engine, :logger
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ module Bob
2
+ # Different engines to run code in background. An engine is any object
3
+ # that responds to #call and takes a Proc object, which should be executed
4
+ # "in the background". The different engines are:
5
+ module BackgroundEngines
6
+ autoload :Foreground, "bob/background_engines/foreground"
7
+ autoload :Threaded, "bob/background_engines/threaded"
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Bob
2
+ module BackgroundEngines
3
+ # Dummy engine that just runs in the foreground (useful for tests).
4
+ Foreground = lambda {|b| b.call }
5
+ end
6
+ end
@@ -0,0 +1,53 @@
1
+ module Bob
2
+ # A Builder will take care of building a buildable (wow, you didn't see that coming,
3
+ # right?).
4
+ class Builder
5
+ attr_reader :buildable, :commit_id
6
+
7
+ def initialize(buildable, commit_id)
8
+ @buildable = buildable
9
+ @commit_id = commit_id
10
+ end
11
+
12
+ # This is where the magic happens:
13
+ #
14
+ # 1. Check out the repo to the appropriate commit.
15
+ # 2. Notify the buildable that the build is starting.
16
+ # 3. Run the build script on it in the background.
17
+ # 4. Reports the build back to the buildable.
18
+ def build
19
+ Bob.logger.info "Building #{commit_id} of the #{buildable.kind} repo at #{buildable.uri}"
20
+ in_background do
21
+ scm.with_commit(commit_id) do
22
+ buildable.start_building(commit_id, scm.info(commit_id))
23
+ build_status, build_output = run_build_script
24
+ buildable.finish_building(commit_id, build_status, build_output)
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def run_build_script
32
+ build_output = nil
33
+
34
+ Bob.logger.debug "Running the build script for #{buildable.uri}"
35
+ IO.popen(build_script, "r") { |output| build_output = output.read }
36
+ Bob.logger.debug("Ran build script `#{build_script}` and got:\n#{build_output}")
37
+
38
+ [$?.success?, build_output]
39
+ end
40
+
41
+ def build_script
42
+ "(cd #{scm.working_dir} && #{buildable.build_script} 2>&1)"
43
+ end
44
+
45
+ def scm
46
+ @scm ||= SCM.new(buildable.kind, buildable.uri, buildable.branch)
47
+ end
48
+
49
+ def in_background(&block)
50
+ Bob.engine.call(block)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ require "bob/scm/abstract"
2
+
3
+ module Bob
4
+ module SCM
5
+ autoload :Git, "bob/scm/git"
6
+ autoload :Svn, "bob/scm/svn"
7
+
8
+ class CantRunCommand < RuntimeError; end
9
+
10
+ # Factory to return appropriate SCM instances (according to repository kind)
11
+ def self.new(kind, uri, branch)
12
+ class_for(kind).new(uri, branch)
13
+ end
14
+
15
+ # A copy of Inflector.camelize, from ActiveSupport. It will convert
16
+ # string to UpperCamelCase.
17
+ def self.class_for(kind)
18
+ class_name = kind.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
19
+ const_get(class_name)
20
+ end
21
+ private_class_method :class_for
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Bob
2
+ module SCM
3
+ class Abstract
4
+ attr_reader :uri, :branch
5
+
6
+ def initialize(uri, branch)
7
+ @uri = Addressable::URI.parse(uri)
8
+ @branch = branch
9
+ end
10
+
11
+ # Checkout the code into <tt>working_dir</tt> at the specified revision and
12
+ # call the passed block
13
+ def with_commit(commit_id)
14
+ update_code
15
+ checkout(commit_id)
16
+ yield
17
+ end
18
+
19
+ # Directory where the code will be checked out. Make sure the user running Bob is
20
+ # allowed to write to this directory (or you'll get a <tt>Errno::EACCESS</tt>)
21
+ def working_dir
22
+ @working_dir ||= "#{Bob.directory}/#{path_from_uri}".tap do |path|
23
+ FileUtils.mkdir_p path
24
+ end
25
+ end
26
+
27
+ # Get some information about the specified commit. Returns a hash with:
28
+ #
29
+ # [<tt>:author</tt>] Commit author's name and email
30
+ # [<tt>:message</tt>] Commit message
31
+ # [<tt>:committed_at</tt>] Commit date (as a <tt>Time</tt> object)
32
+ def info(commit_id)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ protected
37
+
38
+ def run(command)
39
+ command = "(cd #{working_dir} && #{command} &>/dev/null)"
40
+ Bob.logger.debug command
41
+ system(command) || raise(CantRunCommand, "Couldn't run SCM command `#{command}`")
42
+ end
43
+
44
+ def path_from_uri
45
+ raise NotImplementedError
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ module Bob
2
+ module SCM
3
+ class Git < Abstract
4
+ def info(commit_id)
5
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:committed_at: %ci%n)
6
+ YAML.load(`cd #{working_dir} && git show -s --pretty=format:"#{format}" #{commit_id}`).tap do |info|
7
+ info[:committed_at] = Time.parse(info[:committed_at])
8
+ end
9
+ end
10
+
11
+ protected
12
+
13
+ def path_from_uri
14
+ path = uri.path.
15
+ gsub(/\~[a-z0-9]*\//i, ""). # remove ~foobar/
16
+ gsub(/\s+|\.|\//, "-"). # periods, spaces, slashes -> hyphens
17
+ gsub(/^-+|-+$/, "") # remove trailing hyphens
18
+ path += "-#{branch}"
19
+ end
20
+
21
+ private
22
+
23
+ def update_code
24
+ cloned? ? fetch : clone
25
+ end
26
+
27
+ def cloned?
28
+ File.directory?("#{working_dir}/.git")
29
+ end
30
+
31
+ def clone
32
+ git "clone #{uri} #{working_dir}"
33
+ rescue CantRunCommand
34
+ FileUtils.rm_r working_dir
35
+ retry
36
+ end
37
+
38
+ def fetch
39
+ git "fetch origin"
40
+ end
41
+
42
+ def checkout(commit_id)
43
+ # First checkout the branch just in case the commit_id turns out to be HEAD or other non-sha identifier
44
+ git "checkout origin/#{branch}"
45
+ git "reset --hard #{commit_id}"
46
+ end
47
+
48
+ def reset(commit_id)
49
+ git "reset --hard #{commit_id}"
50
+ end
51
+
52
+ def git(command)
53
+ run "git #{command}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ class Object #:nodoc:
2
+ # Add Object#tap for backwards compatibility
3
+ def tap
4
+ yield self
5
+ self
6
+ end unless method_defined?(:tap)
7
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + "/helper"
2
+
3
+ class BobTest < Test::Unit::TestCase
4
+ test "directory" do
5
+ Bob.directory = "/foo/bar"
6
+ assert_equal "/foo/bar", Bob.directory
7
+ end
8
+
9
+ test "logger" do
10
+ logger = Logger.new("/tmp/bob.log")
11
+ Bob.logger = logger
12
+
13
+ assert_same logger, Bob.logger
14
+ end
15
+
16
+ test "engine" do
17
+ engine = Object.new
18
+ Bob.engine = engine
19
+
20
+ assert_same engine, Bob.engine
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ require "test/unit"
2
+ require "contest"
3
+ require "hpricot"
4
+
5
+ begin
6
+ require "redgreen"
7
+ require "ruby-debug"
8
+ rescue LoadError
9
+ end
10
+
11
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"),
12
+ File.expand_path(File.dirname(__FILE__) + "/../test/helper"))
13
+
14
+ require "bob"
15
+ require "git_helper"
16
+ require "svn_helper"
17
+ require "buildable_stub"
18
+
19
+ Bob.logger = Logger.new("/dev/null")
20
+ Bob.engine = Bob::BackgroundEngines::Foreground
21
+ Bob.directory = File.expand_path(File.dirname(__FILE__) + "/../tmp")
22
+
23
+ class Test::Unit::TestCase
24
+ include Bob
25
+ include TestHelper
26
+
27
+ attr_reader :repo, :buildable
28
+
29
+ def setup
30
+ FileUtils.rm_rf(Bob.directory)
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ module TestHelper
2
+ module BuildableStub
3
+ attr_reader :repo, :builds, :metadata
4
+
5
+ def initialize(repo)
6
+ @repo = repo
7
+ @builds = {}
8
+ @metadata = {}
9
+ end
10
+
11
+ def build_script
12
+ "./test"
13
+ end
14
+
15
+ def start_building(commit_id, commit_info)
16
+ @metadata[commit_id] = commit_info
17
+ end
18
+
19
+ def finish_building(commit_id, status, output)
20
+ @builds[commit_id] = [status ? :successful : :failed, output]
21
+ end
22
+ end
23
+
24
+ class GitBuildableStub
25
+ include BuildableStub
26
+
27
+ def kind
28
+ :git
29
+ end
30
+
31
+ def uri
32
+ repo.path
33
+ end
34
+
35
+ def branch
36
+ "master"
37
+ end
38
+ end
39
+
40
+ class SvnBuildableStub
41
+ include BuildableStub
42
+
43
+ def kind
44
+ :svn
45
+ end
46
+
47
+ def uri
48
+ "file://#{SvnRepo.server_root}/#{repo.name}"
49
+ end
50
+
51
+ def branch
52
+ ""
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ require File.dirname(__FILE__) + "/abstract_scm_helper"
2
+
3
+ module TestHelper
4
+ class GitRepo < AbstractSCMRepo
5
+ def create
6
+ FileUtils.mkdir_p @path
7
+
8
+ Dir.chdir(@path) do
9
+ system 'git init &>/dev/null'
10
+ system 'git config user.name "John Doe"'
11
+ system 'git config user.email "johndoe@example.org"'
12
+ system 'echo "just a test repo" >> README'
13
+ add 'README &>/dev/null'
14
+ commit "First commit"
15
+ end
16
+ end
17
+
18
+ def commits
19
+ Dir.chdir(@path) do
20
+ commits = `git log --pretty=oneline`.collect { |l| l.split(" ").first }
21
+ commits.inject([]) do |commits, sha1|
22
+ format = "---%n:message: >-%n %s%n:timestamp: %ci%n" +
23
+ ":identifier: %H%n:author: %n :name: %an%n :email: %ae%n"
24
+ commits << YAML.load(`git show -s --pretty=format:"#{format}" #{sha1}`)
25
+ end
26
+ end
27
+ end
28
+
29
+ def head
30
+ Dir.chdir(@path) do
31
+ `git log --pretty=format:%H | head -1`.chomp
32
+ end
33
+ end
34
+
35
+ def short_head
36
+ head[0..6]
37
+ end
38
+
39
+ protected
40
+ def add(file)
41
+ system "git add #{file}"
42
+ end
43
+
44
+ def commit(message)
45
+ system %Q{git commit -m "#{message}" &>/dev/null}
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bob-the-builder
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - "Nicol\xC3\xA1s Sanguinetti"
8
+ - Simon Rozet
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-05-05 00:00:00 -03:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: addressable
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: sr-mg
28
+ type: :development
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ version:
36
+ - !ruby/object:Gem::Dependency
37
+ name: contest
38
+ type: :development
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ - !ruby/object:Gem::Dependency
47
+ name: redgreen
48
+ type: :development
49
+ version_requirement:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ - !ruby/object:Gem::Dependency
57
+ name: ruby-debug
58
+ type: :development
59
+ version_requirement:
60
+ version_requirements: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ description: Bob the Builder will build your code. Simple.
67
+ email: info@integrityapp.com
68
+ executables: []
69
+
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - .gitignore
76
+ - LICENSE
77
+ - README.rdoc
78
+ - Rakefile
79
+ - bob-the-builder.gemspec
80
+ - lib/bob.rb
81
+ - lib/bob/background_engines.rb
82
+ - lib/bob/background_engines/foreground.rb
83
+ - lib/bob/builder.rb
84
+ - lib/bob/scm.rb
85
+ - lib/bob/scm/abstract.rb
86
+ - lib/bob/scm/git.rb
87
+ - lib/core_ext/object.rb
88
+ - test/bob_test.rb
89
+ - test/helper.rb
90
+ - test/helper/git_helper.rb
91
+ - test/helper/buildable_stub.rb
92
+ has_rdoc: true
93
+ homepage: http://integrityapp.com
94
+ post_install_message:
95
+ rdoc_options: []
96
+
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: "0"
104
+ version:
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: "0"
110
+ version:
111
+ requirements: []
112
+
113
+ rubyforge_project: bob-the-builder
114
+ rubygems_version: 1.3.1
115
+ signing_key:
116
+ specification_version: 2
117
+ summary: Bob builds!
118
+ test_files: []
119
+