kid80-cijoe 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Chris Wanstrath
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,147 @@
1
+ CI Joe
2
+ ======
3
+
4
+ Joe is a [Continuous
5
+ Integration](http://en.wikipedia.org/wiki/Continuous_integration)
6
+ server that'll run your tests on demand and report their pass/fail status.
7
+
8
+ Because knowing is half the battle.
9
+
10
+ ![The Battle](http://img.skitch.com/20090805-g4a2qhttwij8n2jr9t552efn3k.png)
11
+
12
+ Quickstart
13
+ ----------
14
+
15
+ Rip:
16
+
17
+ $ rip install git://github.com/defunkt/cijoe.git
18
+ $ git clone git://github.com/you/yourrepo.git
19
+ $ cijoe yourrepo
20
+
21
+ Gemcutter:
22
+
23
+ $ gem install cijoe
24
+ $ git clone git://github.com/you/yourrepo.git
25
+ $ cijoe yourrepo
26
+
27
+ Boom. Navigate to http://localhost:4567 to see Joe in action.
28
+ Check `cijoe -h` for other options.
29
+
30
+ Basically you need to run `cijoe` and hand it the path to a git
31
+ repo. Make sure this isn't a shared repo: Joe needs to own it.
32
+
33
+ Joe looks for various git config settings in the repo you hand it. For
34
+ instance, you can tell Joe what command to run by setting
35
+ `cijoe.runner`:
36
+
37
+ $ git config --add cijoe.runner "rake -s test:units"
38
+
39
+ Joe doesn't care about Ruby, Python, or whatever. As long as the
40
+ runner returns a non-zero exit status on fail and a zero on success,
41
+ everyone is happy.
42
+
43
+ Need to do some massaging of your repo before the tests run, like
44
+ maybe swapping in a new database.yml? No problem - Joe will try to
45
+ run `.git/hooks/after-reset` if it exists before each build phase.
46
+ Do it in there. Just make sure it's executable.
47
+
48
+ Want to notify IRC or email on test pass or failure? Joe will run
49
+ `.git/hooks/build-failed` or `.git/hooks/build-worked` if they exist
50
+ and are executable on build pass / fail. They're just shell scripts -
51
+ put whatever you want in there.
52
+
53
+ Tip: your repo's `HEAD` will point to the commit used to run the
54
+ build. Pull any metadata you want out of that scro.
55
+
56
+
57
+ Other Branches
58
+ --------------
59
+
60
+ Want joe to run against a branch other than `master`? No problem:
61
+
62
+ $ git config --add cijoe.branch deploy
63
+
64
+
65
+ Campfire
66
+ --------
67
+
68
+ Campfire notification is included, because it's what we use. Want Joe
69
+ notify your Campfire? Put this in your repo's `.git/config`:
70
+
71
+ [campfire]
72
+ user = your@campfire.email
73
+ pass = passw0rd
74
+ subdomain = whatever
75
+ room = Awesomeness
76
+ ssl = false
77
+
78
+ Or do it the old fashion way:
79
+
80
+ $ cd yourrepo
81
+ $ git config --add campfire.user chris@ozmm.org
82
+ $ git config --add campfire.subdomain github
83
+ etc.
84
+
85
+
86
+ Multiple Projects
87
+ -----------------
88
+
89
+ Want CI for multiple projects? Just start multiple instances of Joe!
90
+ He can run on any port - try `cijoe -h` for more options.
91
+
92
+
93
+ HTTP Auth
94
+ ---------
95
+
96
+ Worried about people triggering your builds? Setup HTTP auth:
97
+
98
+ $ git config --add cijoe.user chris
99
+ $ git config --add cijoe.pass secret
100
+
101
+
102
+ GitHub Integration
103
+ ------------------
104
+
105
+ Any POST to Joe will trigger a build. If you are hiding Joe behind
106
+ HTTP auth, that's okay - GitHub knows how to authenticate properly.
107
+
108
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
109
+
110
+ You can find the Post-Receive option under the 'Service Hooks' subtab
111
+ of your project's "Admin" tab.
112
+
113
+
114
+ Daemonize
115
+ ---------
116
+
117
+ Want to run Joe as a daemon? Use `nohup`:
118
+
119
+ $ nohup cijoe -p 4444 repo &
120
+
121
+
122
+ Other CI Servers
123
+ ----------------
124
+
125
+ Need more features? More notifiers? Check out one of these bad boys:
126
+
127
+ * [Cerberus](http://cerberus.rubyforge.org/)
128
+ * [Integrity](http://integrityapp.com/)
129
+ * [CruiseControl.rb](http://cruisecontrolrb.thoughtworks.com/)
130
+ * [BuildBot](http://buildbot.net/trac)
131
+
132
+
133
+ Screenshots
134
+ -----------
135
+
136
+ ![Building](http://img.skitch.com/20090806-ryw34ksi5ixnrdwxcptqy28iy7.png)
137
+
138
+ ![Built](http://img.skitch.com/20090806-f7j3r65yecaq13hdcxqwtc5krd.)
139
+
140
+
141
+ Questions? Concerns?
142
+ --------------------
143
+
144
+ [Issues](http://github.com/defunkt/cijoe/issues) or [the mailing list](http://groups.google.com/group/cijoe).
145
+
146
+
147
+ ( Chris Wanstrath :: chris@ozmm.org )
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ desc "Build a gem"
2
+ task :gem => [ :gemspec, :build ]
3
+
4
+ begin
5
+ require 'jeweler'
6
+
7
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'
8
+ require 'cijoe/version'
9
+
10
+ Jeweler::Tasks.new do |gemspec|
11
+ gemspec.name = "cijoe"
12
+ gemspec.summary = "CI Joe is a simple Continuous Integration server."
13
+ gemspec.description = "CI Joe is a simple Continuous Integration server."
14
+ gemspec.email = "chris@ozmm.org"
15
+ gemspec.homepage = "http://github.com/defunkt/cijoe"
16
+ gemspec.authors = ["Chris Wanstrath"]
17
+ gemspec.add_dependency 'choice'
18
+ gemspec.add_dependency 'sinatra'
19
+ gemspec.add_dependency 'open4'
20
+ gemspec.version = CIJoe::Version.to_s
21
+ end
22
+ rescue LoadError
23
+ puts "Jeweler not available."
24
+ puts "Install it with: gem install jeweler"
25
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.3
data/bin/cijoe ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'cijoe'
5
+ require 'choice'
6
+
7
+ Choice.options do
8
+ banner "Usage: #{File.basename(__FILE__)} [-hpv] path_to_git_repo"
9
+ header ''
10
+ header 'Server options:'
11
+
12
+ option :host do
13
+ d = "0.0.0.0"
14
+ short '-h'
15
+ long '--host=HOST'
16
+ desc "The hostname or ip of the host to bind to (default #{d})"
17
+ default d
18
+ end
19
+
20
+ option :port do
21
+ d = 4567
22
+ short '-p'
23
+ long '--port=PORT'
24
+ desc "The port to listen on (default #{d})"
25
+ cast Integer
26
+ default d
27
+ end
28
+
29
+ separator ''
30
+ separator 'Common options: '
31
+
32
+ option :help do
33
+ long '--help'
34
+ desc 'Show this message'
35
+ end
36
+
37
+ option :version do
38
+ short '-v'
39
+ long '--version'
40
+ desc 'Show version'
41
+ action do
42
+ puts "#{File.basename(__FILE__)} v#{CIJoe::Version}"
43
+ exit
44
+ end
45
+ end
46
+ end
47
+
48
+ options = Choice.choices
49
+ CIJoe::Server.start(options[:host], options[:port], Choice.rest[0])
data/deps.rip ADDED
@@ -0,0 +1,5 @@
1
+ git://github.com/Manfred/broach.git HEAD
2
+ git://github.com/sinatra/sinatra.git 0.9.4
3
+ git://github.com/rack/rack.git 1.0
4
+ git://github.com/defunkt/choice.git 8b125564
5
+ git://github.com/ahoward/open4.git 25c3ed8
data/examples/cijoe.ru ADDED
@@ -0,0 +1,15 @@
1
+ # Example CI Joe rackup config. Drop a cijoe.ru file
2
+ # in your projects direct
3
+ require 'cijoe'
4
+
5
+ # setup middleware
6
+ use Rack::CommonLogger
7
+
8
+ # configure joe
9
+ CIJoe::Server.configure do |config|
10
+ config.set :project_path, File.dirname(__FILE__)
11
+ config.set :show_exceptions, true
12
+ config.set :lock, true
13
+ end
14
+
15
+ run CIJoe::Server
data/examples/cijoed ADDED
@@ -0,0 +1,53 @@
1
+ #!/bin/sh
2
+ ### BEGIN INIT INFO
3
+ # Provides: cijoe
4
+ # Required-Start: $syslog $local_fs $network
5
+ # Required-Stop: $syslog $local_fs $network
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: 0 1
8
+ # Description: Run the CIJoe CI server. Yo Joe!!
9
+ ### END INIT INFO
10
+
11
+ . /lib/lsb/init-functions
12
+
13
+ REPO=/path/to/your/git/repository
14
+ PORT=4567
15
+
16
+ NAME=cijoe
17
+ INSTALL_DIR=/usr/sbin
18
+ DAEMON=$INSTALL_DIR/$NAME
19
+ DAEMON_ARGS="-p $PORT $REPO"
20
+ PIDFILE=/var/run/$NAME.pid
21
+ DAEMON_USER=www-data
22
+ DAEMON_GROUP=$DAEMON_USER
23
+
24
+ # test -f $DAEMON || exit 0
25
+ # test -f $PROJECT_DIR || exit 0
26
+
27
+ case "$1" in
28
+ start)
29
+ log_daemon_msg "Starting cijoe" "cijoe"
30
+ start-stop-daemon --background --make-pidfile --exec $DAEMON --start --name $NAME --pidfile $PIDFILE --chuid $DAEMON_USER:$DAEMON_GROUP -- $DAEMON_ARGS
31
+ log_end_msg $?
32
+ ;;
33
+ stop)
34
+ log_daemon_msg "Stopping cijoe" "cijoe"
35
+ start-stop-daemon --stop --pidfile $PIDFILE --quiet --retry 10
36
+ log_end_msg $?
37
+ ;;
38
+ restart)
39
+ log_daemon_msg "Restarting cijoe" "cijoe"
40
+ start-stop-daemon --stop --pidfile $PIDFILE --quiet --retry 10
41
+ start-stop-daemon --background --make-pidfile --exec $DAEMON --start --name $NAME --pidfile $PIDFILE --chuid $DAEMON_USER:$DAEMON_GROUP -- $DAEMON_ARGS
42
+ log_end_msg $?
43
+ ;;
44
+ status)
45
+ status_of_proc $DAEMON $NAME && exit 0 || exit $?
46
+ ;;
47
+ *)
48
+ log_action_msg "Usage: /etc/init.d/cijoe (start|stop|restart)"
49
+ exit 2
50
+ ;;
51
+ esac
52
+
53
+ exit 0
@@ -0,0 +1,49 @@
1
+ require 'yaml'
2
+
3
+ class CIJoe
4
+ class Build < Struct.new(:user, :project, :started_at, :finished_at, :sha, :status, :output, :pid)
5
+ def initialize(*args)
6
+ super
7
+ self.started_at ||= Time.now
8
+ end
9
+
10
+ def status
11
+ return super if started_at && finished_at
12
+ :building
13
+ end
14
+
15
+ def failed?
16
+ status == :failed
17
+ end
18
+
19
+ def worked?
20
+ status == :worked
21
+ end
22
+
23
+ def short_sha
24
+ sha[0,7] if sha
25
+ end
26
+
27
+ def clean_output
28
+ output.gsub(/\e\[.+?m/, '').strip
29
+ end
30
+
31
+ def commit
32
+ return if sha.nil?
33
+ @commit ||= Commit.new(sha, user, project)
34
+ end
35
+
36
+ def dump(file)
37
+ config = [user, project, started_at, finished_at, sha, status, output, pid]
38
+ data = YAML.dump(config)
39
+ File.open(file, 'wb') { |io| io.write(data) }
40
+ end
41
+
42
+ def self.load(file)
43
+ if File.exist?(file)
44
+ config = YAML.load(File.read(file))
45
+ new *config
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,71 @@
1
+ class CIJoe
2
+ module Campfire
3
+ def self.activate
4
+ if valid_config?
5
+ require 'broach'
6
+
7
+ CIJoe::Build.class_eval do
8
+ include CIJoe::Campfire
9
+ end
10
+
11
+ puts "Loaded Campfire notifier"
12
+ else
13
+ puts "Can't load Campfire notifier."
14
+ puts "Please add the following to your project's .git/config:"
15
+ puts "[campfire]"
16
+ puts "\ttoken = your_api_token"
17
+ puts "\taccount = whatever"
18
+ puts "\troom = Awesomeness"
19
+ puts "\tssl = false"
20
+ end
21
+ end
22
+
23
+ def self.config
24
+ @config ||= {
25
+ :account => Config.campfire.account.to_s,
26
+ :token => Config.campfire.token.to_s,
27
+ :room => Config.campfire.room.to_s,
28
+ :use_ssl => Config.campfire.ssl.to_s.strip == 'true'
29
+ }
30
+ end
31
+
32
+ def self.valid_config?
33
+ %w( account token room ).all? do |key|
34
+ puts config.inspect if config[key.intern].nil?
35
+ !config[key.intern].empty?
36
+ end
37
+ end
38
+
39
+ def notify
40
+ room.speak "#{short_message}. #{commit.url} #{play_sound}"
41
+ room.paste full_message if failed?
42
+ room.leave
43
+ end
44
+
45
+ private
46
+ def room
47
+ @room ||= begin
48
+ Broach.settings = Campfire.config
49
+ Broach::Room.find_by_name(Campfire.config['room'])
50
+ end
51
+ end
52
+
53
+ def short_message
54
+ "Build #{short_sha} of #{project} #{worked? ? "was successful" : "failed"}"
55
+ end
56
+
57
+ def play_sound
58
+ "/play #{worked? ? "rimshot" : "trombone"}"
59
+ end
60
+
61
+ def full_message
62
+ <<-EOM
63
+ Commit Message: #{commit.message}
64
+ Commit Date: #{commit.committed_at}
65
+ Commit Author: #{commit.author}
66
+
67
+ #{clean_output}
68
+ EOM
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ class CIJoe
2
+ class Commit < Struct.new(:sha, :user, :project)
3
+ def url
4
+ "http://github.com/#{user}/#{project}/commit/#{sha}"
5
+ end
6
+
7
+ def author
8
+ raw_commit_lines.grep(/Author:/).first.split(':', 2)[-1]
9
+ end
10
+
11
+ def committed_at
12
+ raw_commit_lines.grep(/Date:/).first.split(':', 2)[-1]
13
+ end
14
+
15
+ def message
16
+ raw_commit.split("\n\n", 3)[1].to_s.strip
17
+ end
18
+
19
+ def raw_commit
20
+ @raw_commit ||= `git show #{sha}`.chomp
21
+ end
22
+
23
+ def raw_commit_lines
24
+ @raw_commit_lines ||= raw_commit.split("\n")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ class CIJoe
2
+ class Config
3
+ def self.method_missing(command, *args)
4
+ new(command)
5
+ end
6
+
7
+ def initialize(command, parent = nil)
8
+ @command = command
9
+ @parent = parent
10
+ end
11
+
12
+ def method_missing(command, *args)
13
+ Config.new(command, self)
14
+ end
15
+
16
+ def to_s
17
+ `git config #{config_string}`.chomp
18
+ end
19
+
20
+ def config_string
21
+ @parent ? "#{@parent.config_string}.#{@command}" : @command
22
+ end
23
+ end
24
+ end
Binary file
@@ -0,0 +1,212 @@
1
+ /*****************************************************************************/
2
+ /*
3
+ /* Common
4
+ /*
5
+ /*****************************************************************************/
6
+
7
+ /* Global Reset */
8
+
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ html, body {
15
+ height: 100%;
16
+ }
17
+
18
+ body {
19
+ background-color: white;
20
+ font: 13.34px helvetica, arial, clean, sans-serif;
21
+ *font-size: small;
22
+ text-align: center;
23
+ }
24
+
25
+ h1, h2, h3, h4, h5, h6 {
26
+ font-size: 100%;
27
+ }
28
+
29
+ h1 {
30
+ margin-bottom: 1em;
31
+ }
32
+
33
+ h1 a {
34
+ text-decoration: none;
35
+ color: #000;
36
+ }
37
+
38
+ .failed, .color31 {
39
+ color: red !important;
40
+ }
41
+
42
+ .worked, .color32 {
43
+ color: green !important;
44
+ }
45
+
46
+ .errored, .color33 {
47
+ color: yellow !important;
48
+ }
49
+
50
+ p {
51
+ margin: 1em 0;
52
+ }
53
+
54
+ a {
55
+ color: #00a;
56
+ }
57
+
58
+ a:hover {
59
+ color: black;
60
+ }
61
+
62
+ a:visited {
63
+ color: #a0a;
64
+ }
65
+
66
+ table {
67
+ font-size: inherit;
68
+ font: 100%;
69
+ }
70
+
71
+ /*****************************************************************************/
72
+ /*
73
+ /* Home
74
+ /*
75
+ /*****************************************************************************/
76
+
77
+ ul.posts {
78
+ list-style-type: none;
79
+ margin-bottom: 2em;
80
+ }
81
+
82
+ ul.posts li {
83
+ line-height: 1.75em;
84
+ }
85
+
86
+ ul.posts .date {
87
+ color: #aaa;
88
+ font-family: Monaco, "Courier New", monospace;
89
+ font-size: 80%;
90
+ }
91
+
92
+ /*****************************************************************************/
93
+ /*
94
+ /* Site
95
+ /*
96
+ /*****************************************************************************/
97
+
98
+ .site {
99
+ font-size: 110%;
100
+ text-align: justify;
101
+ width: 40em;
102
+ margin: 3em auto 2em auto;
103
+ line-height: 1.5em;
104
+ }
105
+
106
+ .title {
107
+ color: #a00;
108
+ font-weight: bold;
109
+ margin-bottom: 2em;
110
+ }
111
+
112
+ .site .title a {
113
+ color: #a00;
114
+ text-decoration: none;
115
+ }
116
+
117
+ .site .title a:hover {
118
+ color: black;
119
+ }
120
+
121
+ .site .title .extra {
122
+ color: #aaa;
123
+ text-decoration: none;
124
+ margin-left: 1em;
125
+ font-size: 0.9em;
126
+ }
127
+
128
+ .site .title a.extra:hover {
129
+ color: black;
130
+ }
131
+
132
+ .site .meta {
133
+ color: #aaa;
134
+ }
135
+
136
+ .site .footer {
137
+ font-size: 80%;
138
+ color: #666;
139
+ border-top: 4px solid #eee;
140
+ margin-top: 2em;
141
+ overflow: hidden;
142
+ }
143
+
144
+ .site .footer .contact {
145
+ float: left;
146
+ margin-right: 3em;
147
+ }
148
+
149
+ .site .footer .contact a {
150
+ color: #8085C1;
151
+ }
152
+
153
+ .site .footer .rss {
154
+ margin-top: 1.1em;
155
+ margin-right: -.2em;
156
+ float: right;
157
+ }
158
+
159
+ .site .footer .rss img {
160
+ border: 0;
161
+ }
162
+
163
+ /*****************************************************************************/
164
+ /*
165
+ /* Posts
166
+ /*
167
+ /*****************************************************************************/
168
+
169
+ #post {
170
+
171
+ }
172
+
173
+ /* standard */
174
+
175
+ #post pre {
176
+ border: 1px solid #ddd;
177
+ background-color: #eef;
178
+ padding: 0 .4em;
179
+ }
180
+
181
+ #post ul,
182
+ #post ol {
183
+ margin-left: 1.25em;
184
+ }
185
+
186
+ #post code {
187
+ border: 1px solid #ddd;
188
+ background-color: #eef;
189
+ font-size: 95%;
190
+ padding: 0 .2em;
191
+ }
192
+
193
+ #post pre code {
194
+ border: none;
195
+ }
196
+
197
+ /* terminal */
198
+
199
+ pre.terminal {
200
+ border: 1px solid black;
201
+ background-color: #333;
202
+ color: white;
203
+ padding: 5px;
204
+ overflow: auto;
205
+ word-wrap: break-word;
206
+ }
207
+
208
+ pre.terminal code {
209
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
210
+ background-color: #333;
211
+ }
212
+
@@ -0,0 +1,77 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+
4
+ class CIJoe
5
+ class Server < Sinatra::Base
6
+ dir = File.dirname(File.expand_path(__FILE__))
7
+
8
+ set :views, "#{dir}/views"
9
+ set :public, "#{dir}/public"
10
+ set :static, true
11
+ set :lock, true
12
+
13
+ before { @joe.restore }
14
+
15
+ get '/?' do
16
+ erb(:template, {}, :joe => @joe)
17
+ end
18
+
19
+ post '/?' do
20
+ payload = params[:payload].to_s
21
+ if payload.empty? || payload.include?(@joe.git_branch)
22
+ @joe.build
23
+ end
24
+ redirect request.path
25
+ end
26
+
27
+ user, pass = Config.cijoe.user.to_s, Config.cijoe.pass.to_s
28
+ if user != '' && pass != ''
29
+ use Rack::Auth::Basic do |username, password|
30
+ [ username, password ] == [ user, pass ]
31
+ end
32
+ puts "Using HTTP basic auth"
33
+ end
34
+
35
+ helpers do
36
+ include Rack::Utils
37
+ alias_method :h, :escape_html
38
+
39
+ # thanks integrity!
40
+ def ansi_color_codes(string)
41
+ string.gsub("\e[0m", '</span>').
42
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
43
+ end
44
+
45
+ def pretty_time(time)
46
+ time.strftime("%Y-%m-%d %H:%M")
47
+ end
48
+
49
+ def cijoe_root
50
+ root = request.path
51
+ root = "" if root == "/"
52
+ root
53
+ end
54
+ end
55
+
56
+ def initialize(*args)
57
+ super
58
+ check_project
59
+ @joe = CIJoe.new(options.project_path)
60
+
61
+ CIJoe::Campfire.activate
62
+ end
63
+
64
+ def self.start(host, port, project_path)
65
+ set :project_path, project_path
66
+ CIJoe::Server.run! :host => host, :port => port
67
+ end
68
+
69
+ def check_project
70
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
71
+ puts "Whoops! I need the path to a Git repo."
72
+ puts " $ git clone git@github.com:username/project.git project"
73
+ abort " $ cijoe project"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ class CIJoe
2
+ Version = "0.1.2"
3
+ end
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link href="<%= cijoe_root %>/screen.css" media="screen" rel="stylesheet" type="text/css" />
5
+ <title><%= h(joe.project) %>: CI Joe</title>
6
+ </head>
7
+ <body>
8
+ <div class="site">
9
+ <div class="title">
10
+ <a href="<%= cijoe_root %>/">CI Joe</a>
11
+ <span class="extra">because knowing is half the battle</span>
12
+ </div>
13
+
14
+ <div id="home">
15
+ <h1><a href="<%= joe.url %>"><%= joe.project %></a></h1>
16
+ <ul class="posts">
17
+ <% if joe.current_build %>
18
+ <li>
19
+ <span class="date"><%= pretty_time(joe.current_build.started_at) if joe.current_build %></span> &raquo;
20
+ <% if joe.current_build.sha %>
21
+ Building <a href="<%= joe.url %>/commits/<%= joe.current_build.sha %>"><%= joe.current_build.short_sha %></a> <small>(pid: <%= joe.pid %>)</small>
22
+ <% else %>
23
+ Build starting...
24
+ <% end %>
25
+ </li>
26
+ <% else %>
27
+ <li><form method="POST"><input type="submit" value="Build"/></form></li>
28
+ <% end %>
29
+
30
+ <% if joe.last_build %>
31
+ <li><span class="date"><%= pretty_time(joe.last_build.finished_at) %></span> &raquo; Built <a href="<%= joe.url %>/commits/<%= joe.last_build.sha %>"><%= joe.last_build.short_sha %></a> <span class="<%= joe.last_build.status %>">(<%= joe.last_build.status %>)</span></li>
32
+ <% if joe.last_build.failed? %>
33
+ <li><pre class="terminal"><code><%=ansi_color_codes h(joe.last_build.output) %></code></pre></li>
34
+ <% end %>
35
+ <% end %>
36
+ </ul>
37
+ </div>
38
+
39
+ <div class="footer">
40
+ <div class="contact">
41
+ <p>
42
+ <a href="http://github.com/defunkt/cijoe/tree/master#readme">Documentation</a><br/>
43
+ <a href="http://github.com/defunkt/cijoe">Source</a><br/>
44
+ <a href="http://github.com/defunkt/cijoe/issues">Issues</a><br/>
45
+ <a href="http://twitter.com/defunkt">Twitter</a>
46
+ </p>
47
+ </div>
48
+ <div class="contact">
49
+ <p>
50
+ Designed by <a href="http://tom.preston-werner.com/">Tom Preston-Werner</a><br/>
51
+ Influenced by <a href="http://integrityapp.com/">Integrity</a><br/>
52
+ Built with <a href="http://sinatrarb.com/">Sinatra</a><br/>
53
+ Keep it simple, Sam.
54
+ </p>
55
+ </div>
56
+ <div class="rss">
57
+ <a href="http://github.com/defunkt/cijoe">
58
+ <img src="<%= cijoe_root %>/octocat.png" alt="Octocat!" />
59
+ </a>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </body>
64
+ </html>
data/lib/cijoe.rb ADDED
@@ -0,0 +1,180 @@
1
+ ##
2
+ # CI Joe.
3
+ # Because knowing is half the battle.
4
+ #
5
+ # This is a stupid simple CI server. It can build one (1)
6
+ # git-based project only.
7
+ #
8
+ # It only remembers the last build.
9
+ #
10
+ # It only notifies to Campfire.
11
+ #
12
+ # It's a RAH (Real American Hero).
13
+ #
14
+ # Seriously, I'm gonna be nuts about keeping this simple.
15
+
16
+ begin
17
+ require 'open4'
18
+ rescue LoadError
19
+ abort "** Please install open4"
20
+ end
21
+
22
+ require 'cijoe/version'
23
+ require 'cijoe/config'
24
+ require 'cijoe/commit'
25
+ require 'cijoe/build'
26
+ require 'cijoe/campfire'
27
+ require 'cijoe/server'
28
+
29
+ class CIJoe
30
+ attr_reader :user, :project, :url, :current_build, :last_build
31
+
32
+ def initialize(project_path)
33
+ project_path = File.expand_path(project_path)
34
+ Dir.chdir(project_path)
35
+
36
+ @user, @project = git_user_and_project
37
+ @url = "http://github.com/#{@user}/#{@project}"
38
+
39
+ @last_build = nil
40
+ @current_build = nil
41
+
42
+ trap("INT") { stop }
43
+ end
44
+
45
+ # is a build running?
46
+ def building?
47
+ !!@current_build
48
+ end
49
+
50
+ # the pid of the running child process
51
+ def pid
52
+ building? and current_build.pid
53
+ end
54
+
55
+ # kill the child and exit
56
+ def stop
57
+ Process.kill(9, pid) if pid
58
+ exit!
59
+ end
60
+
61
+ # build callbacks
62
+ def build_failed(output, error)
63
+ finish_build :failed, "#{error}\n\n#{output}"
64
+ run_hook "build-failed"
65
+ end
66
+
67
+ def build_worked(output)
68
+ finish_build :worked, output
69
+ run_hook "build-worked"
70
+ end
71
+
72
+ def finish_build(status, output)
73
+ @current_build.finished_at = Time.now
74
+ @current_build.status = status
75
+ @current_build.output = output
76
+ @last_build = @current_build
77
+ @current_build = nil
78
+ write_build 'current', @current_build
79
+ write_build 'last', @last_build
80
+ @last_build.notify if @last_build.respond_to? :notify
81
+ end
82
+
83
+ # run the build but make sure only
84
+ # one is running at a time
85
+ def build
86
+ return if building?
87
+ @current_build = Build.new(@user, @project)
88
+ write_build 'current', @current_build
89
+ Thread.new { build! }
90
+ end
91
+
92
+ # update git then run the build
93
+ def build!
94
+ build = @current_build
95
+ out, err, status = '', '', nil
96
+ git_update
97
+ build.sha = git_sha
98
+ write_build 'current', build
99
+
100
+ status = Open4.popen4(runner_command) do |pid, stdin, stdout, stderr|
101
+ build.pid = pid
102
+ write_build 'current', build
103
+ err, out = stderr.read.strip, stdout.read.strip
104
+ end
105
+
106
+ status.exitstatus.to_i == 0 ? build_worked(out) : build_failed(out, err)
107
+ rescue Object => e
108
+ build_failed('', e.to_s)
109
+ end
110
+
111
+ # shellin' out
112
+ def runner_command
113
+ runner = Config.cijoe.runner.to_s
114
+ runner == '' ? "rake -s test:units" : runner
115
+ end
116
+
117
+ def git_sha
118
+ `git rev-parse origin/#{git_branch}`.chomp
119
+ end
120
+
121
+ def git_update
122
+ `git fetch origin && git reset --hard origin/#{git_branch}`
123
+ run_hook "after-reset"
124
+ end
125
+
126
+ def git_user_and_project
127
+ Config.remote.origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
128
+ end
129
+
130
+ def git_branch
131
+ branch = Config.cijoe.branch.to_s
132
+ branch == '' ? "master" : branch
133
+ end
134
+
135
+ # massage our repo
136
+ def run_hook(hook)
137
+ if File.exists?(file=".git/hooks/#{hook}") && File.executable?(file)
138
+ data =
139
+ if @last_build && @last_build.commit
140
+ {
141
+ "MESSAGE" => @last_build.commit.message,
142
+ "AUTHOR" => @last_build.commit.author,
143
+ "SHA" => @last_build.commit.sha,
144
+ "OUTPUT" => @last_build.clean_output
145
+ }
146
+ else
147
+ {}
148
+ end
149
+ env = data.collect { |k, v| %(#{k}=#{v.inspect}) }.join(" ")
150
+ `#{env} sh #{file}`
151
+ end
152
+ end
153
+
154
+ # restore current / last build state from disk.
155
+ def restore
156
+ @last_build = read_build('last')
157
+ @current_build = read_build('current')
158
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
159
+ rescue Errno::ESRCH
160
+ # build pid isn't running anymore. assume previous
161
+ # server died and reset.
162
+ @current_build = nil
163
+ end
164
+
165
+ # write build info for build to file.
166
+ def write_build(name, build)
167
+ filename = ".git/builds/#{name}"
168
+ Dir.mkdir '.git/builds' unless File.directory?('.git/builds')
169
+ if build
170
+ build.dump filename
171
+ elsif File.exist?(filename)
172
+ File.unlink filename
173
+ end
174
+ end
175
+
176
+ # load build info from file.
177
+ def read_build(name)
178
+ Build.load(".git/builds/#{name}")
179
+ end
180
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kid80-cijoe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Chris Wanstrath
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-18 00:00:00 +00:00
13
+ default_executable: cijoe
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: choice
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: sinatra
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: open4
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ description: CI Joe is a simple Continuous Integration server.
46
+ email: chris@ozmm.org
47
+ executables:
48
+ - cijoe
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - LICENSE
53
+ - README.markdown
54
+ files:
55
+ - LICENSE
56
+ - README.markdown
57
+ - Rakefile
58
+ - VERSION
59
+ - bin/cijoe
60
+ - deps.rip
61
+ - examples/cijoe.ru
62
+ - examples/cijoed
63
+ - lib/cijoe.rb
64
+ - lib/cijoe/build.rb
65
+ - lib/cijoe/campfire.rb
66
+ - lib/cijoe/commit.rb
67
+ - lib/cijoe/config.rb
68
+ - lib/cijoe/public/octocat.png
69
+ - lib/cijoe/public/screen.css
70
+ - lib/cijoe/server.rb
71
+ - lib/cijoe/version.rb
72
+ - lib/cijoe/views/template.erb
73
+ has_rdoc: true
74
+ homepage: http://github.com/defunkt/cijoe
75
+ licenses: []
76
+
77
+ post_install_message:
78
+ rdoc_options:
79
+ - --charset=UTF-8
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ requirements: []
95
+
96
+ rubyforge_project:
97
+ rubygems_version: 1.3.5
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: CI Joe is a simple Continuous Integration server.
101
+ test_files: []
102
+