felixclack-cijoe 0.9.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,67 @@
1
+ require 'yaml'
2
+
3
+ class CIJoe
4
+ class Build < Struct.new(:project_path, :user, :project, :started_at, :finished_at, :sha, :status, :output, :pid, :branch)
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 env_output
45
+ out = clean_output
46
+ out.size > 100_000 ? out[-100_000,100_000] : out
47
+ end
48
+
49
+ def commit
50
+ return if sha.nil?
51
+ @commit ||= Commit.new(sha, user, project, project_path)
52
+ end
53
+
54
+ def dump(file)
55
+ config = [user, project, started_at, finished_at, sha, status, output, pid, branch]
56
+ data = YAML.dump(config)
57
+ File.open(file, 'wb') { |io| io.write(data) }
58
+ end
59
+
60
+ def self.load(file, project_path)
61
+ if File.exist?(file)
62
+ config = YAML.load(File.read(file)).unshift(project_path)
63
+ new *config
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,78 @@
1
+ class CIJoe
2
+ module Campfire
3
+ def self.activate(project_path)
4
+ @project_path = project_path
5
+
6
+ if valid_config?
7
+ require 'tinder'
8
+
9
+ CIJoe::Build.class_eval do
10
+ include CIJoe::Campfire
11
+ end
12
+
13
+ puts "Loaded Campfire notifier"
14
+ elsif ENV['RACK_ENV'] != 'test'
15
+ puts "Can't load Campfire notifier."
16
+ puts "Please add the following to your project's .git/config:"
17
+ puts "[campfire]"
18
+ puts "\ttoken = your_api_token"
19
+ puts "\tsubdomain = whatever"
20
+ puts "\troom = Awesomeness"
21
+ puts "\tssl = false"
22
+ end
23
+ end
24
+
25
+ def self.config
26
+ campfire_config = Config.new('campfire', @project_path)
27
+ @config ||= {
28
+ :subdomain => campfire_config.subdomain.to_s,
29
+ :token => campfire_config.token.to_s,
30
+ :room => campfire_config.room.to_s,
31
+ :ssl => campfire_config.ssl.to_s.strip == 'true'
32
+ }
33
+ end
34
+
35
+ def self.valid_config?
36
+ %w( subdomain token room ).all? do |key|
37
+ !config[key.intern].empty?
38
+ end
39
+ end
40
+
41
+ def notify
42
+ room.speak "#{short_message}. #{commit.url}"
43
+ room.play "#{play_sound}"
44
+ room.paste full_message if failed?
45
+ room.leave
46
+ end
47
+
48
+ private
49
+ def room
50
+ @room ||= begin
51
+ config = Campfire.config
52
+ campfire = Tinder::Campfire.new(config[:subdomain],
53
+ :token => config[:token],
54
+ :ssl => config[:ssl] || false)
55
+ campfire.find_room_by_name(config[:room])
56
+ end
57
+ end
58
+
59
+ def short_message
60
+ "#{branch} at #{short_sha} of #{project} " +
61
+ (worked? ? "passed" : "failed") + " (#{duration.to_i}s)"
62
+ end
63
+
64
+ def play_sound
65
+ "#{worked? ? "rimshot" : "trombone"}"
66
+ end
67
+
68
+ def full_message
69
+ <<-EOM
70
+ Commit Message: #{commit.message}
71
+ Commit Date: #{commit.committed_at}
72
+ Commit Author: #{commit.author}
73
+
74
+ #{clean_output}
75
+ EOM
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,27 @@
1
+ class CIJoe
2
+ class Commit < Struct.new(:sha, :user, :project, :project_path)
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 ||= `cd #{project_path} && 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,43 @@
1
+ class CIJoe
2
+ class Config
3
+ def self.method_missing(command, *args)
4
+ new(command, *args)
5
+ end
6
+
7
+ def initialize(command, project_path = nil, parent = nil)
8
+ @command = command
9
+ @parent = parent
10
+ @project_path = project_path || File.join(File.dirname(__FILE__), '../../')
11
+ end
12
+
13
+ def method_missing(command, *args)
14
+ Config.new(command, @project_path, self)
15
+ end
16
+
17
+ def to_s
18
+ git_command = "cd #{@project_path} && git config #{config_string}"
19
+ result = `#{git_command} 2>&1`.chomp
20
+ process_status = $?
21
+
22
+ if successful_command?(process_status) || config_command_with_empty_value?(result,process_status)
23
+ return result
24
+ else
25
+ raise "Error calling git config, is a recent version of git installed? Command: #{git_command.inspect}, Error: #{result.inspect}, Status: #{process_status.inspect}"
26
+ end
27
+ end
28
+
29
+ def config_string
30
+ @parent ? "#{@parent.config_string}.#{@command}" : @command
31
+ end
32
+
33
+ private
34
+
35
+ def successful_command?(process_status)
36
+ process_status.exitstatus.to_i == 0
37
+ end
38
+
39
+ def config_command_with_empty_value?(result, process_status)
40
+ process_status.exitstatus.to_i == 1 && result.empty?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,222 @@
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: 4px;
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: #c4b430 !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: #00a;
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: 100%;
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
+ input {
164
+ float: right;
165
+ margin-right:5px;
166
+ }
167
+ /*****************************************************************************/
168
+ /*
169
+ /* Posts
170
+ /*
171
+ /*****************************************************************************/
172
+
173
+ #post {
174
+
175
+ }
176
+
177
+ /* standard */
178
+
179
+ #post pre {
180
+ border: 2px solid #ddd;
181
+ background-color: #333;
182
+ padding: 0 .4em;
183
+ }
184
+
185
+ #post ul,
186
+ #post ol {
187
+ margin-left: 1.25em;
188
+ }
189
+
190
+ #post code {
191
+ border: 1px solid #ddd;
192
+ background-color: #eef;
193
+ font-size: 95%;
194
+ padding: 0 .2em;
195
+ }
196
+
197
+ #post pre code {
198
+ border: none;
199
+ }
200
+
201
+ /* terminal */
202
+
203
+ pre.terminal {
204
+ border: 2px solid #999;
205
+ background-color: #fff;
206
+ color: #333;
207
+ padding: 15px;
208
+ /* overflow: auto; */
209
+ word-wrap: break-word;
210
+ }
211
+
212
+ pre.terminal code {
213
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
214
+ background-color: #fff;
215
+ }
216
+
217
+ pre {
218
+ white-space: pre-wrap; /* CSS2.1 compliant */
219
+ white-space: -moz-pre-wrap; /* Mozilla-based browsers */
220
+ white-space: o-pre-wrap; /* Opera 7+ */
221
+ }
222
+
@@ -0,0 +1,115 @@
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.building? || joe.last_build.nil?) ? "building" : joe.last_build.sha
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 =~ /"ref":"(.+?)"/
32
+ pushed_branch = $1.split('/').last
33
+ end
34
+
35
+ # Only build if we were given an explicit branch via `?branch=blah`,
36
+ # no payload exists (we're probably testing), or the payload exists and
37
+ # the "ref" property matches our specified build branch.
38
+ if params[:branch] || payload.empty? || pushed_branch == joe.git_branch
39
+ joe.build(params[:branch])
40
+ end
41
+
42
+ redirect request.path
43
+ end
44
+
45
+ get '/api/json' do
46
+ response = [200, {'Content-Type' => 'application/json'}]
47
+ response_json = erb(:json, {}, :joe => joe)
48
+ if params[:jsonp]
49
+ response << params[:jsonp] + '(' + response_json + ')'
50
+ else
51
+ response << response_json
52
+ end
53
+ response
54
+ end
55
+
56
+
57
+ helpers do
58
+ include Rack::Utils
59
+ alias_method :h, :escape_html
60
+
61
+ # thanks integrity!
62
+ def ansi_color_codes(string)
63
+ string.gsub("\e[0m", '</span>').
64
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
65
+ end
66
+
67
+ def pretty_time(time)
68
+ time.strftime("%Y-%m-%d %H:%M")
69
+ end
70
+
71
+ def cijoe_root
72
+ root = request.path
73
+ root = "" if root == "/"
74
+ root
75
+ end
76
+ end
77
+
78
+ def initialize(*args)
79
+ super
80
+ check_project
81
+ @joe = CIJoe.new(options.project_path)
82
+
83
+ CIJoe::Campfire.activate(options.project_path)
84
+ end
85
+
86
+ def self.start(host, port, project_path)
87
+ set :project_path, project_path
88
+ CIJoe::Server.run! :host => host, :port => port
89
+ end
90
+
91
+ def self.rack_start(project_path)
92
+ set :project_path, project_path
93
+ self.new
94
+ end
95
+
96
+ def self.project_path=(project_path)
97
+ user, pass = Config.cijoe(project_path).user.to_s, Config.cijoe(project_path).pass.to_s
98
+ if user != '' && pass != ''
99
+ use Rack::Auth::Basic do |username, password|
100
+ [ username, password ] == [ user, pass ]
101
+ end
102
+ puts "Using HTTP basic auth"
103
+ end
104
+ set :project_path, Proc.new{project_path}
105
+ end
106
+
107
+ def check_project
108
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
109
+ puts "Whoops! I need the path to a Git repo."
110
+ puts " $ git clone git@github.com:username/project.git project"
111
+ abort " $ cijoe project"
112
+ end
113
+ end
114
+ end
115
+ end