heidi 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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/heidi +70 -0
- data/bin/heidi_console +3 -0
- data/bin/heidi_cron +8 -0
- data/bin/heidi_web +12 -0
- data/heidi.gemspec +96 -0
- data/lib/heidi/build.rb +147 -0
- data/lib/heidi/builder.rb +95 -0
- data/lib/heidi/git.rb +113 -0
- data/lib/heidi/hook.rb +20 -0
- data/lib/heidi/integrator.rb +84 -0
- data/lib/heidi/project.rb +113 -0
- data/lib/heidi/tester.rb +52 -0
- data/lib/heidi/web/routes/home.rb +9 -0
- data/lib/heidi/web/routes/projects.rb +61 -0
- data/lib/heidi/web/routes.rb +13 -0
- data/lib/heidi/web.rb +8 -0
- data/lib/heidi.rb +25 -0
- data/spec/heidi/build_spec.rb +5 -0
- data/spec/heidi/builder_spec.rb +5 -0
- data/spec/heidi/git_spec.rb +36 -0
- data/spec/heidi/hook_spec.rb +5 -0
- data/spec/heidi/integrator_spec.rb +5 -0
- data/spec/heidi/project_spec.rb +5 -0
- data/spec/heidi/tester_spec.rb +5 -0
- data/spec/heidi/web_spec.rb +6 -0
- data/spec/heidi_spec.rb +14 -0
- data/spec/spec_helper.rb +12 -0
- metadata +177 -0
data/lib/heidi/git.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'simple_shell'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
class Heidi
|
6
|
+
|
7
|
+
# A very simple interface to git base on SimpleShell, no library ties and no
|
8
|
+
# fancy stuff
|
9
|
+
#
|
10
|
+
class Git
|
11
|
+
VERBOSE=false
|
12
|
+
|
13
|
+
def initialize(path=Dir.pwd, verbose=VERBOSE)
|
14
|
+
@path = path
|
15
|
+
SimpleShell.noisy = verbose
|
16
|
+
@shell = SimpleShell.new(@path)
|
17
|
+
end
|
18
|
+
|
19
|
+
# get the latest commit hash
|
20
|
+
def commit
|
21
|
+
res = @shell.git "log", "-n", "1", "--pretty=%H"
|
22
|
+
res.out
|
23
|
+
end
|
24
|
+
alias_method :HEAD, :commit
|
25
|
+
alias_method :head, :commit
|
26
|
+
|
27
|
+
# find the current branch (the one with the *)
|
28
|
+
def branch
|
29
|
+
res = @shell.git "branch", "--no-color"
|
30
|
+
active = res.out.scan(/\* \w+/).first
|
31
|
+
active.scan(/\w+$/).first
|
32
|
+
end
|
33
|
+
|
34
|
+
# git branch
|
35
|
+
def branches
|
36
|
+
res = @shell.git "branch", "--no-color"
|
37
|
+
res.out.split("\n").collect{ |b| b.gsub(/^[\s*]+/, '') }
|
38
|
+
end
|
39
|
+
|
40
|
+
# git checkout $name
|
41
|
+
def switch(name)
|
42
|
+
return nil unless branches.include?(name)
|
43
|
+
@shell.git "checkout", name
|
44
|
+
end
|
45
|
+
|
46
|
+
# git checkout -b $name [$base]
|
47
|
+
def checkout(name, base=nil)
|
48
|
+
command = [ "git", "checkout", "-b", name ]
|
49
|
+
command << base unless base.nil?
|
50
|
+
@shell.system(*command)
|
51
|
+
end
|
52
|
+
|
53
|
+
# git merge $base
|
54
|
+
def merge(base)
|
55
|
+
@shell.git %W(merge #{base})
|
56
|
+
end
|
57
|
+
|
58
|
+
# git fetch $where='origin'
|
59
|
+
def fetch(where="origin")
|
60
|
+
@shell.git %W(fetch #{where})
|
61
|
+
end
|
62
|
+
|
63
|
+
# git pull $where='origin' [$what]
|
64
|
+
def pull(where="origin", what=nil)
|
65
|
+
command = [ "git", "pull", where ]
|
66
|
+
command << what if !what.nil?
|
67
|
+
|
68
|
+
@shell.system(*command)
|
69
|
+
end
|
70
|
+
|
71
|
+
# git push $where [$what]
|
72
|
+
#
|
73
|
+
# $what may be '--tags'
|
74
|
+
#
|
75
|
+
def push(where="origin", what=nil)
|
76
|
+
command = [ "git", "push", where ]
|
77
|
+
if !what.nil?
|
78
|
+
if what == "--tags"
|
79
|
+
command.insert(2, what)
|
80
|
+
else
|
81
|
+
command << what
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
@shell.system(*command)
|
86
|
+
end
|
87
|
+
|
88
|
+
# git tags
|
89
|
+
def tags
|
90
|
+
res = @shell.system("git", "tag")
|
91
|
+
res.out.split("\n")
|
92
|
+
end
|
93
|
+
|
94
|
+
# git tag -a -m $message $name
|
95
|
+
def tag(name, message)
|
96
|
+
command = [ "git", "tag", "-a", "-m", message, name ]
|
97
|
+
if tags.include?(name)
|
98
|
+
command.insert(4, "-f")
|
99
|
+
end
|
100
|
+
@shell.system(*command)
|
101
|
+
end
|
102
|
+
|
103
|
+
# git config $key $value
|
104
|
+
def []=(key, value)
|
105
|
+
@shell.system("git", "config", "heidi.#{key}", value)
|
106
|
+
end
|
107
|
+
|
108
|
+
# git config $key
|
109
|
+
def [](key)
|
110
|
+
@shell.system("git", "config", "heidi.#{key}").out
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/heidi/hook.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'simple_shell'
|
2
|
+
|
3
|
+
class Heidi
|
4
|
+
class Hook
|
5
|
+
def initialize(project, script)
|
6
|
+
@project = project
|
7
|
+
@script = script
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform(where)
|
11
|
+
shell = SimpleShell.new(where)
|
12
|
+
res = shell.do @script
|
13
|
+
return res
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
File.basename(@script)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'heidi/build'
|
2
|
+
require 'heidi/builder'
|
3
|
+
require 'heidi/tester'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
class Heidi
|
7
|
+
class Integrator
|
8
|
+
attr_reader :build, :project
|
9
|
+
|
10
|
+
def initialize(project)
|
11
|
+
@project = project
|
12
|
+
@build = Heidi::Build.new(project)
|
13
|
+
@failed = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure
|
17
|
+
@failed = true
|
18
|
+
build.record(:failure)
|
19
|
+
end
|
20
|
+
|
21
|
+
def integrate
|
22
|
+
start = Time.now
|
23
|
+
build.lock
|
24
|
+
build.load_hooks
|
25
|
+
build.clean
|
26
|
+
|
27
|
+
return failure if !run_hooks(:before)
|
28
|
+
|
29
|
+
builder = Heidi::Builder.new(build)
|
30
|
+
tester = Heidi::Tester.new(build)
|
31
|
+
|
32
|
+
return failure if !builder.build!
|
33
|
+
return failure if !tester.test!
|
34
|
+
return failure if !run_hooks(:after)
|
35
|
+
|
36
|
+
# record the new succesful
|
37
|
+
build.record(:success)
|
38
|
+
|
39
|
+
# create a tarball
|
40
|
+
build.create_tar_ball
|
41
|
+
|
42
|
+
build.log :info, ("Integration took: %.2fs" % (Time.now - start))
|
43
|
+
return true
|
44
|
+
|
45
|
+
rescue Exception => e
|
46
|
+
$stderr.puts e.message
|
47
|
+
$stderr.puts e.backtrace.join("\n")
|
48
|
+
|
49
|
+
return $!
|
50
|
+
|
51
|
+
ensure
|
52
|
+
run_hooks(:failed) if @failed == true
|
53
|
+
|
54
|
+
# always unlock the build root, no matter what
|
55
|
+
build.unlock
|
56
|
+
|
57
|
+
return @failed == true ? false : true
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_hooks(where)
|
62
|
+
return true if build.hooks[where].nil? || build.hooks[where].empty?
|
63
|
+
|
64
|
+
hooks_failed = false
|
65
|
+
build.hooks[where].each do |hook|
|
66
|
+
res = hook.perform(build.build_root)
|
67
|
+
|
68
|
+
if res.S?.to_i != 0
|
69
|
+
build.log :error, "--- #{where} hook: #{hook.name} failed ---"
|
70
|
+
build.log :error, res.err
|
71
|
+
|
72
|
+
hooks_failed = true
|
73
|
+
break
|
74
|
+
|
75
|
+
else
|
76
|
+
build.log :info, res.out
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
return hooks_failed == true ? false : true
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'heidi/git'
|
2
|
+
require 'heidi/integrator'
|
3
|
+
require 'heidi/hook'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
class Heidi
|
7
|
+
class Project
|
8
|
+
attr_reader :root, :cached_root, :lock_file, :builds
|
9
|
+
|
10
|
+
def initialize(root)
|
11
|
+
@root = root
|
12
|
+
@lock_file = File.join(root, ".lock")
|
13
|
+
@cached_root = File.join(root, "cached")
|
14
|
+
@git = Heidi::Git.new(@cached_root)
|
15
|
+
load_builds
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_builds
|
19
|
+
@builds = []
|
20
|
+
Dir[File.join(root, "logs", "*")].each do |build|
|
21
|
+
next unless File.directory? build
|
22
|
+
@builds << Heidi::Build.new(self, File.basename(build))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def name=(name)
|
27
|
+
@name = name
|
28
|
+
@git["name"] = name
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
@name ||= @git[:name]
|
33
|
+
end
|
34
|
+
|
35
|
+
def commit
|
36
|
+
@git.commit[0..8]
|
37
|
+
end
|
38
|
+
|
39
|
+
def last_commit
|
40
|
+
@git["commit"]
|
41
|
+
end
|
42
|
+
def record_last_commit
|
43
|
+
@git["commit"] = self.commit
|
44
|
+
end
|
45
|
+
|
46
|
+
def latest_build
|
47
|
+
@git["build.latest"]
|
48
|
+
end
|
49
|
+
def record_latest_build
|
50
|
+
@git["build.latest"] = self.commit
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_status
|
54
|
+
@git["build.status"]
|
55
|
+
end
|
56
|
+
def build_status=(status)
|
57
|
+
@git["build.status"] = status
|
58
|
+
end
|
59
|
+
|
60
|
+
def integration_branch
|
61
|
+
name = @git["build.branch"]
|
62
|
+
name == "" ? nil : name
|
63
|
+
end
|
64
|
+
|
65
|
+
def integrate
|
66
|
+
return "locked" if locked?
|
67
|
+
|
68
|
+
status = ""
|
69
|
+
self.lock do
|
70
|
+
res = Heidi::Integrator.new(self).integrate
|
71
|
+
status = res != true ? "failed" : "passed"
|
72
|
+
end
|
73
|
+
|
74
|
+
return status
|
75
|
+
end
|
76
|
+
|
77
|
+
def fetch
|
78
|
+
@git.fetch
|
79
|
+
if integration_branch
|
80
|
+
if @git.branches.include? integration_branch
|
81
|
+
@git.switch(integration_branch)
|
82
|
+
@git.merge "origin/#{integration_branch}"
|
83
|
+
|
84
|
+
else
|
85
|
+
@git.checkout(integration_branch, "origin/#{integration_branch}")
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
record_last_commit
|
91
|
+
end
|
92
|
+
|
93
|
+
def lock(&block)
|
94
|
+
File.open(lock_file, File::TRUNC|File::CREAT|File::WRONLY) do |f|
|
95
|
+
f.puts Time.now.strftime("%c")
|
96
|
+
end
|
97
|
+
|
98
|
+
if block_given?
|
99
|
+
yield
|
100
|
+
self.unlock
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def unlock
|
105
|
+
File.unlink lock_file
|
106
|
+
end
|
107
|
+
|
108
|
+
def locked?
|
109
|
+
File.exists? lock_file
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
data/lib/heidi/tester.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
class Heidi
|
2
|
+
class Tester
|
3
|
+
attr_reader :build, :project, :message
|
4
|
+
|
5
|
+
def initialize(build)
|
6
|
+
@build = build
|
7
|
+
@project = build.project
|
8
|
+
@message = ""
|
9
|
+
end
|
10
|
+
|
11
|
+
def test!
|
12
|
+
build.log(:info, "Starting tests")
|
13
|
+
|
14
|
+
tests_failed = false
|
15
|
+
|
16
|
+
if build.hooks[:tests].empty?
|
17
|
+
build.log(:error, "There are no test hooks")
|
18
|
+
@message = "There are no test hooks"
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
|
22
|
+
build.hooks[:tests].each do |hook|
|
23
|
+
res = hook.perform(build.build_root)
|
24
|
+
|
25
|
+
if res.S?.to_i != 0
|
26
|
+
log "--- test #{hook.name} failed ---"
|
27
|
+
log res.err
|
28
|
+
|
29
|
+
@message = "tests failed"
|
30
|
+
tests_failed = true
|
31
|
+
break
|
32
|
+
|
33
|
+
else
|
34
|
+
log res.out
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
return tests_failed ? false : true
|
39
|
+
end
|
40
|
+
|
41
|
+
def log(string)
|
42
|
+
File.open(
|
43
|
+
File.join(build.root, "test.log"),
|
44
|
+
File::CREAT|File::WRONLY|File::APPEND
|
45
|
+
) do |f|
|
46
|
+
f.puts string
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Heidi; module Web; module Routes
|
2
|
+
|
3
|
+
module Projects
|
4
|
+
get '/projects' do
|
5
|
+
output = ""
|
6
|
+
$heidi.projects.each do |project|
|
7
|
+
output += "<a href='/projects/#{project.name}'>#{project.name}</a><br />"
|
8
|
+
end
|
9
|
+
|
10
|
+
output
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/projects/:name' do
|
14
|
+
project = $heidi[params[:name]]
|
15
|
+
if project.nil?
|
16
|
+
return "no project by that name: #{params[:name]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
output = "<h1>#{project.name}</h1>"
|
20
|
+
output += "Build status: #{project.build_status}"
|
21
|
+
output += "<br /><br /><h2>Build history</h2>"
|
22
|
+
project.builds.each do |build|
|
23
|
+
output += %Q{<a href="/projects/#{project.name}/build/#{build.commit}">#{build.commit}</a> - #{build.status}<br />}
|
24
|
+
end
|
25
|
+
|
26
|
+
output
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/projects/:name/build/:commit' do
|
30
|
+
project = $heidi[params[:name]]
|
31
|
+
if project.nil?
|
32
|
+
return "no project by that name: #{params[:name]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# load build of project
|
36
|
+
build = Heidi::Build.new(project, params[:commit])
|
37
|
+
output = "<h1>#{project.name}</h1>"
|
38
|
+
output += "<h2>Build: #{build.commit} - #{build.status}<h2>"
|
39
|
+
|
40
|
+
%w(heidi.info heidi.errors build.log test.log).each do |log_file|
|
41
|
+
log = build.logs(log_file)
|
42
|
+
if (!log.nil? and !log.empty?)
|
43
|
+
output += "<h3>#{log_file}</h3>"
|
44
|
+
output += "<pre>#{log}</pre>"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
output
|
49
|
+
end
|
50
|
+
|
51
|
+
put '/projects/:name/build' do
|
52
|
+
project = $heidi[params[:name]]
|
53
|
+
if project.nil?
|
54
|
+
return "no project by that name: #{params[:name]}"
|
55
|
+
end
|
56
|
+
|
57
|
+
project.integrate
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end; end; end
|
data/lib/heidi/web.rb
ADDED
data/lib/heidi.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'simple_shell'
|
3
|
+
|
4
|
+
require 'heidi/project'
|
5
|
+
|
6
|
+
class Heidi
|
7
|
+
attr_reader :projects
|
8
|
+
|
9
|
+
def initialize(root=Dir.pwd)
|
10
|
+
@root = root
|
11
|
+
@projects = []
|
12
|
+
Dir[File.join(root,"projects", "*")].each do |project|
|
13
|
+
next unless File.directory?(project)
|
14
|
+
|
15
|
+
@projects << Heidi::Project.new(project)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](name)
|
20
|
+
name = "#{name}"
|
21
|
+
@projects.select do |project|
|
22
|
+
project.name == name || File.basename(project.root) == name
|
23
|
+
end.first
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heidi::Git do
|
4
|
+
let(:git) { Heidi::Git.new() }
|
5
|
+
|
6
|
+
it "should can find the HEAD" do
|
7
|
+
git.commit.should_not be_empty
|
8
|
+
git.commit.should == git.HEAD
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should list all branches" do
|
12
|
+
git.branches.should be_kind_of(Array)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should list the current branch" do
|
16
|
+
git.branch.should_not be_empty
|
17
|
+
git.branch.should_not =~ /^\*/
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should list the tags" do
|
21
|
+
git.tags.should be_kind_of(Array)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "config" do
|
25
|
+
let(:random) { "#{rand(1000)}.#{rand(1000)}" }
|
26
|
+
|
27
|
+
it "should write configs" do
|
28
|
+
git["test.entry"].should_not == random
|
29
|
+
git["test.entry"] = random
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should read configs" do
|
33
|
+
git["test.entry"].should_not == be_empty
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/spec/heidi_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heidi do
|
4
|
+
before(:all) do
|
5
|
+
shell = SimpleShell.new("/")
|
6
|
+
shell.mkdir("-p", "/tmp/heidi_spec/projects/one")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should gather projects on load" do
|
10
|
+
h = Heidi.new("/tmp/heidi_spec")
|
11
|
+
h.projects.should be_kind_of(Array)
|
12
|
+
h.projects.count.should == 1
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'heidi'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|