mheffner-cijoe 0.2.0

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,62 @@
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 building?
24
+ status == :building
25
+ end
26
+
27
+ def duration
28
+ return if building?
29
+ finished_at - started_at
30
+ end
31
+
32
+ def short_sha
33
+ if sha
34
+ sha[0,7]
35
+ else
36
+ "<unknown>"
37
+ end
38
+ end
39
+
40
+ def clean_output
41
+ output.gsub(/\e\[.+?m/, '').strip
42
+ end
43
+
44
+ def commit
45
+ return if sha.nil?
46
+ @commit ||= Commit.new(sha, user, project)
47
+ end
48
+
49
+ def dump(file)
50
+ config = [user, project, started_at, finished_at, sha, status, output, pid]
51
+ data = YAML.dump(config)
52
+ File.open(file, 'wb') { |io| io.write(data) }
53
+ end
54
+
55
+ def self.load(file)
56
+ if File.exist?(file)
57
+ config = YAML.load(File.read(file))
58
+ new *config
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,82 @@
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 = 12345abcd"
17
+ puts "\tsubdomain = whatever"
18
+ puts "\troom = Awesomeness"
19
+ puts "\tssl = false"
20
+ end
21
+ end
22
+
23
+ def self.config
24
+ @config ||= {
25
+ :subdomain => Config.campfire.subdomain.to_s,
26
+ :token => Config.campfire.token.to_s,
27
+ :room => Config.campfire.room.to_s,
28
+ :ssl => Config.campfire.ssl.to_s.strip == 'true'
29
+ }
30
+ end
31
+
32
+ def self.valid_config?
33
+ %w( subdomain token room ).all? do |key|
34
+ !config[key.intern].empty?
35
+ end
36
+ end
37
+
38
+ def notify
39
+ config = Campfire.config
40
+ Broach.settings = {
41
+ 'account' => config[:subdomain],
42
+ 'token' => config[:token],
43
+ 'use_ssl' => config[:ssl] ? true : false
44
+ }
45
+
46
+ # Only complain if failed
47
+ # XXX: yes this is lame, but we should we really only announce a
48
+ # successful build if the repo changed.
49
+ #
50
+ Broach.speak(config[:room], "#{short_message}. #{commit.url}") if failed?
51
+ #room.speak "#{short_message}. #{commit.url}"
52
+ #room.paste full_message if failed?
53
+ #room.leave
54
+ end
55
+
56
+ private
57
+ def room
58
+ @room ||= begin
59
+ config = Campfire.config
60
+ options = {}
61
+ options[:ssl] = config[:ssl] ? true : false
62
+ campfire = Tinder::Campfire.new(config[:subdomain], options)
63
+ campfire.login(config[:user], config[:pass])
64
+ campfire.find_room_by_name(config[:room])
65
+ end
66
+ end
67
+
68
+ def short_message
69
+ "Build #{short_sha} of #{project} #{worked? ? "was successful" : "failed"}"
70
+ end
71
+
72
+ def full_message
73
+ <<-EOM
74
+ Commit Message: #{commit.message}
75
+ Commit Date: #{commit.committed_at}
76
+ Commit Author: #{commit.author}
77
+
78
+ #{clean_output}
79
+ EOM
80
+ end
81
+ end
82
+ 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,42 @@
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_command = "git config #{config_string}"
18
+ result = `#{git_command} 2>&1`.chomp
19
+ process_status = $?
20
+
21
+ if successful_command?(process_status) || config_command_with_empty_value?(result,process_status)
22
+ return result
23
+ else
24
+ raise "Error calling git config, is a recent version of git installed? Command: #{git_command}, Error: #{result}"
25
+ end
26
+ end
27
+
28
+ def config_string
29
+ @parent ? "#{@parent.config_string}.#{@command}" : @command
30
+ end
31
+
32
+ private
33
+
34
+ def successful_command?(process_status)
35
+ process_status.exitstatus.to_i == 0
36
+ end
37
+
38
+ def config_command_with_empty_value?(result, process_status)
39
+ process_status.exitstatus.to_i == 1 && result.empty?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,213 @@
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
+ ul.posts .duration {
88
+ color: #aaa;
89
+ font-family: Monaco, "Courier New", monospace;
90
+ font-size: 80%;
91
+ }
92
+
93
+ /*****************************************************************************/
94
+ /*
95
+ /* Site
96
+ /*
97
+ /*****************************************************************************/
98
+
99
+ .site {
100
+ font-size: 110%;
101
+ text-align: justify;
102
+ width: 40em;
103
+ margin: 3em auto 2em auto;
104
+ line-height: 1.5em;
105
+ }
106
+
107
+ .title {
108
+ color: #a00;
109
+ font-weight: bold;
110
+ margin-bottom: 2em;
111
+ }
112
+
113
+ .site .title a {
114
+ color: #a00;
115
+ text-decoration: none;
116
+ }
117
+
118
+ .site .title a:hover {
119
+ color: black;
120
+ }
121
+
122
+ .site .title .extra {
123
+ color: #aaa;
124
+ text-decoration: none;
125
+ margin-left: 1em;
126
+ font-size: 0.9em;
127
+ }
128
+
129
+ .site .title a.extra:hover {
130
+ color: black;
131
+ }
132
+
133
+ .site .meta {
134
+ color: #aaa;
135
+ }
136
+
137
+ .site .footer {
138
+ font-size: 80%;
139
+ color: #666;
140
+ border-top: 4px solid #eee;
141
+ margin-top: 2em;
142
+ overflow: hidden;
143
+ }
144
+
145
+ .site .footer .contact {
146
+ float: left;
147
+ margin-right: 3em;
148
+ }
149
+
150
+ .site .footer .contact a {
151
+ color: #8085C1;
152
+ }
153
+
154
+ .site .footer .rss {
155
+ margin-top: 1.1em;
156
+ margin-right: -.2em;
157
+ float: right;
158
+ }
159
+
160
+ .site .footer .rss img {
161
+ border: 0;
162
+ }
163
+
164
+ /*****************************************************************************/
165
+ /*
166
+ /* Posts
167
+ /*
168
+ /*****************************************************************************/
169
+
170
+ #post {
171
+
172
+ }
173
+
174
+ /* standard */
175
+
176
+ #post pre {
177
+ border: 1px solid #ddd;
178
+ background-color: #eef;
179
+ padding: 0 .4em;
180
+ }
181
+
182
+ #post ul,
183
+ #post ol {
184
+ margin-left: 1.25em;
185
+ }
186
+
187
+ #post code {
188
+ border: 1px solid #ddd;
189
+ background-color: #eef;
190
+ font-size: 95%;
191
+ padding: 0 .2em;
192
+ }
193
+
194
+ #post pre code {
195
+ border: none;
196
+ }
197
+
198
+ /* terminal */
199
+
200
+ pre.terminal {
201
+ border: 1px solid black;
202
+ background-color: #333;
203
+ color: white;
204
+ padding: 5px;
205
+ overflow: auto;
206
+ word-wrap: break-word;
207
+ }
208
+
209
+ pre.terminal code {
210
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
211
+ background-color: #333;
212
+ }
213
+
@@ -0,0 +1,87 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+
4
+ class CIJoe
5
+ class Server < Sinatra::Base
6
+ attr_reader :joe
7
+
8
+ dir = File.dirname(File.expand_path(__FILE__))
9
+
10
+ set :views, "#{dir}/views"
11
+ set :public, "#{dir}/public"
12
+ set :static, true
13
+ set :lock, true
14
+
15
+ before { joe.restore }
16
+
17
+ get '/ping' do
18
+ if joe.building? || !joe.last_build || !joe.last_build.worked?
19
+ halt 412, joe.last_build ? joe.last_build.sha : "building"
20
+ end
21
+
22
+ joe.last_build.sha
23
+ end
24
+
25
+ get '/?' do
26
+ erb(:template, {}, :joe => joe)
27
+ end
28
+
29
+ post '/?' do
30
+ payload = params[:payload].to_s
31
+ if payload.empty? || payload.include?(joe.git_branch)
32
+ joe.build
33
+ end
34
+ redirect request.path
35
+ end
36
+
37
+ user, pass = Config.cijoe.user.to_s, Config.cijoe.pass.to_s
38
+ if user != '' && pass != ''
39
+ use Rack::Auth::Basic do |username, password|
40
+ [ username, password ] == [ user, pass ]
41
+ end
42
+ puts "Using HTTP basic auth"
43
+ end
44
+
45
+ helpers do
46
+ include Rack::Utils
47
+ alias_method :h, :escape_html
48
+
49
+ # thanks integrity!
50
+ def ansi_color_codes(string)
51
+ string.gsub("\e[0m", '</span>').
52
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
53
+ end
54
+
55
+ def pretty_time(time)
56
+ time.strftime("%Y-%m-%d %H:%M")
57
+ end
58
+
59
+ def cijoe_root
60
+ root = request.path
61
+ root = "" if root == "/"
62
+ root
63
+ end
64
+ end
65
+
66
+ def initialize(*args)
67
+ super
68
+ check_project
69
+ @joe = CIJoe.new(options.project_path)
70
+
71
+ CIJoe::Campfire.activate
72
+ end
73
+
74
+ def self.start(host, port, project_path)
75
+ set :project_path, project_path
76
+ CIJoe::Server.run! :host => host, :port => port
77
+ end
78
+
79
+ def check_project
80
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
81
+ puts "Whoops! I need the path to a Git repo."
82
+ puts " $ git clone git@github.com:username/project.git project"
83
+ abort " $ cijoe project"
84
+ end
85
+ end
86
+ end
87
+ end