bob 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.
@@ -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.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"
3
+ s.version = "0.1"
4
+ s.date = "2009-04-12"
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 = "integrity"
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.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
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-04-12 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.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: integrity
114
+ rubygems_version: 1.3.1
115
+ signing_key:
116
+ specification_version: 2
117
+ summary: Bob builds!
118
+ test_files: []
119
+