build_status_server 0.4
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/Gemfile +14 -0
- data/Gemfile.lock +46 -0
- data/LICENSE +19 -0
- data/README.md +45 -0
- data/bin/build_status_server +43 -0
- data/build_status_server.gemspec +29 -0
- data/config/config-example.yml +14 -0
- data/lib/build_status_server/requirements.rb~ +8 -0
- data/lib/build_status_server/server.rb +199 -0
- data/lib/build_status_server/server.rb~ +189 -0
- data/lib/build_status_server/version.rb +3 -0
- data/lib/build_status_server/version.rb~ +3 -0
- data/lib/build_status_server.rb +10 -0
- data/lib/build_status_server.rb~ +11 -0
- data/spec/lib/build_status_server_spec.rb +124 -0
- data/spec/lib/build_status_server_spec.rb~ +124 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/spec_helper.rb~ +15 -0
- data/spec/support/build_result.yml +3 -0
- data/spec/support/sample.json +11 -0
- metadata +126 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
build_status_server (0.3)
|
5
|
+
json
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
columnize (0.3.6)
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
json (1.6.5)
|
13
|
+
linecache (0.46)
|
14
|
+
rbx-require-relative (> 0.0.4)
|
15
|
+
rack (1.4.0)
|
16
|
+
rack-protection (1.2.0)
|
17
|
+
rack
|
18
|
+
rbx-require-relative (0.0.5)
|
19
|
+
rspec (2.9.0)
|
20
|
+
rspec-core (~> 2.9.0)
|
21
|
+
rspec-expectations (~> 2.9.0)
|
22
|
+
rspec-mocks (~> 2.9.0)
|
23
|
+
rspec-core (2.9.0)
|
24
|
+
rspec-expectations (2.9.0)
|
25
|
+
diff-lcs (~> 1.1.3)
|
26
|
+
rspec-mocks (2.9.0)
|
27
|
+
ruby-debug (0.10.4)
|
28
|
+
columnize (>= 0.1)
|
29
|
+
ruby-debug-base (~> 0.10.4.0)
|
30
|
+
ruby-debug-base (0.10.4)
|
31
|
+
linecache (>= 0.3)
|
32
|
+
sinatra (1.3.2)
|
33
|
+
rack (~> 1.3, >= 1.3.6)
|
34
|
+
rack-protection (~> 1.2)
|
35
|
+
tilt (~> 1.3, >= 1.3.3)
|
36
|
+
tilt (1.3.3)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
build_status_server!
|
43
|
+
json
|
44
|
+
rspec
|
45
|
+
ruby-debug
|
46
|
+
sinatra
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Juan C. Muller <jcmuller@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Build Notifier
|
2
|
+
|
3
|
+
This utility is part of an XFD (eXtreeme Feedback Device) solution designed and
|
4
|
+
built for my employer [ChallengePost](http://challengepost.com). It works in
|
5
|
+
conjunction with our [Jenkins](http://jenkins-ci.org) Continuous Integration
|
6
|
+
server (and its
|
7
|
+
[Notification Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin))
|
8
|
+
and an [Arduino](http://arduino.cc) powered
|
9
|
+
[Traffic Light controller](https://github.com/jcmuller/TrafficLightController)
|
10
|
+
with a
|
11
|
+
pseudo-[REST](http://en.wikipedia.org/wiki/Representational_state_transfer)ful
|
12
|
+
API.
|
13
|
+
|
14
|
+
To run, you need to copy `config/config-example.yml` into `config/config.yml`
|
15
|
+
and mofify accordingly.
|
16
|
+
|
17
|
+
# Configuration file
|
18
|
+
## UDP Server
|
19
|
+
This section defines what interface and port should the UDP server listen at.
|
20
|
+
The Jenkins' Notification Plugin should be set to this parameters as well.
|
21
|
+
|
22
|
+
## TCP Client
|
23
|
+
This section is where we tell the server how to communicate with the web
|
24
|
+
enabled XFD. In the example case, there is a web server running somewhere
|
25
|
+
listening on port 4567 that responds to `/green` and `/red`.
|
26
|
+
|
27
|
+
On our installation, this represents the Traffic Light's Arduino web server.
|
28
|
+
|
29
|
+
## Store
|
30
|
+
Where the persistent state will be stored.
|
31
|
+
|
32
|
+
## Mask (optional)
|
33
|
+
You can decide to either include or ignore certain builds whose names match a
|
34
|
+
given [Regular Expression](http://en.wikipedia.org/wiki/Regular_expression).
|
35
|
+
|
36
|
+
# Development
|
37
|
+
|
38
|
+
`bin/build_status_server_traffic_light_mock` is provided for development
|
39
|
+
purposes only.
|
40
|
+
|
41
|
+
# Finished product
|
42
|
+

|
43
|
+
|
44
|
+
# Wiring the traffic light
|
45
|
+

|
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'getoptlong'
|
4
|
+
|
5
|
+
possible_arguments = [
|
6
|
+
['--config', '-c', GetoptLong::OPTIONAL_ARGUMENT],
|
7
|
+
['--development', '-d', GetoptLong::NO_ARGUMENT],
|
8
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
9
|
+
['--verbose', '-v', GetoptLong::NO_ARGUMENT]
|
10
|
+
]
|
11
|
+
|
12
|
+
opts = GetoptLong.new(*possible_arguments)
|
13
|
+
|
14
|
+
options = {}
|
15
|
+
showhelp = false
|
16
|
+
|
17
|
+
opts.each do |opt, arg|
|
18
|
+
case opt
|
19
|
+
when '--development'
|
20
|
+
$:.push 'lib'
|
21
|
+
when '--help'
|
22
|
+
puts <<-EOT
|
23
|
+
#{File.basename(__FILE__)} [#{possible_arguments.map{|arg| arg[0] + (arg[2] == GetoptLong::OPTIONAL_ARGUMENT ? ' argument' : '')}.join('], [')}]
|
24
|
+
|
25
|
+
All the arguments are optional.
|
26
|
+
|
27
|
+
--config, -c Specify what configuration file to load
|
28
|
+
--development, -d Whether we should load libraries from ./lib
|
29
|
+
--help, -h Display this very helpful text
|
30
|
+
--verbose, -v Be more informative about what's going on
|
31
|
+
|
32
|
+
EOT
|
33
|
+
exit
|
34
|
+
when '--config'
|
35
|
+
options[:config] = arg
|
36
|
+
when '--verbose'
|
37
|
+
options[:verbose] = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require 'build_status_server'
|
42
|
+
|
43
|
+
BuildStatusServer::Server.new(options).listen
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "build_status_server/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "build_status_server"
|
6
|
+
s.version = BuildStatusServer::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
|
9
|
+
s.author = "Juan C. Muller"
|
10
|
+
s.email = "jcmuller@gmail.com"
|
11
|
+
s.homepage = "http://github.com/jcmuller/build_status_server"
|
12
|
+
s.license = "GPL"
|
13
|
+
s.summary = "A build notifier server for Jenkins CI that controls an XFD over HTTP"
|
14
|
+
s.description = "A build notifier server for Jenkins CI that controls an XFD over HTTP"
|
15
|
+
|
16
|
+
s.files = Dir["{lib/**/*,spec/**/*}"] + %w(bin/build_status_server config/config-example.yml LICENSE README.md Gemfile Gemfile.lock build_status_server.gemspec)
|
17
|
+
s.require_path = "lib"
|
18
|
+
s.bindir = "bin"
|
19
|
+
s.executables = %w(build_status_server)
|
20
|
+
|
21
|
+
s.homepage = "http://github.com/jcmuller/build_status_server"
|
22
|
+
s.test_files = Dir["spec/**/*_spec.rb"]
|
23
|
+
|
24
|
+
s.add_development_dependency("ruby-debug")
|
25
|
+
s.add_development_dependency("sinatra")
|
26
|
+
|
27
|
+
s.add_dependency("json")
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module BuildStatusServer
|
2
|
+
class Server
|
3
|
+
attr_reader :config, :store_file, :mask_policy, :verbose
|
4
|
+
attr_accessor :store, :mask
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
load_config_file(options[:config])
|
8
|
+
@verbose = options[:verbose] || config["verbose"]
|
9
|
+
@store_file = File.expand_path(".", config["store"]["filename"])
|
10
|
+
@mask = Regexp.new(config["mask"]["regex"])
|
11
|
+
@mask_policy = config["mask"]["policy"] || "exclude"
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_store
|
15
|
+
@store = begin
|
16
|
+
YAML.load_file(store_file)
|
17
|
+
rescue
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
@store = {} unless store.class == Hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def listen
|
24
|
+
sock = UDPSocket.new
|
25
|
+
udp_server = config["udp_server"]
|
26
|
+
|
27
|
+
begin
|
28
|
+
sock.bind(udp_server["address"], udp_server["port"])
|
29
|
+
rescue Errno::EADDRINUSE
|
30
|
+
STDERR.puts <<-EOT
|
31
|
+
There appears that another instance is running, or another process
|
32
|
+
is listening at the same port (#{udp_server["address"]}:#{udp_server["port"]}
|
33
|
+
|
34
|
+
EOT
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "Listening on UDP #{udp_server["address"]}:#{udp_server["port"]}" if verbose
|
39
|
+
|
40
|
+
while true
|
41
|
+
data, addr = sock.recvfrom(2048)
|
42
|
+
#require "ruby-debug"; debugger
|
43
|
+
if process_job(data)
|
44
|
+
status = process_all_statuses
|
45
|
+
notify(status)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
sock.close
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_job(data = "{}")
|
53
|
+
job = JSON.parse(data)
|
54
|
+
|
55
|
+
build_name = job["name"]
|
56
|
+
|
57
|
+
unless should_process_build(build_name)
|
58
|
+
STDOUT.puts "Ignoring #{build_name} (#{mask}--#{mask_policy})" if verbose
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
if job.class != Hash or
|
63
|
+
job["build"].class != Hash
|
64
|
+
STDERR.puts "Pinged with an invalid payload"
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
phase = job["build"]["phase"]
|
69
|
+
status = job["build"]["status"]
|
70
|
+
|
71
|
+
if phase == "FINISHED"
|
72
|
+
STDOUT.puts "Got #{status} for #{build_name} on #{Time.now} [#{job.inspect}]" if verbose
|
73
|
+
case status
|
74
|
+
when "SUCCESS", "FAILURE"
|
75
|
+
load_store
|
76
|
+
store[build_name] = status
|
77
|
+
File.open(store_file, "w") { |file| YAML.dump(store, file) }
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
else
|
81
|
+
STDOUT.puts "Started for #{build_name} on #{Time.now} [#{job.inspect}]" if verbose
|
82
|
+
end
|
83
|
+
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
# Ensure config file exists. If not, copy example into it
|
88
|
+
def load_config_file(config_file)
|
89
|
+
curated_file = nil
|
90
|
+
|
91
|
+
if config_file
|
92
|
+
f = File.expand_path(config_file)
|
93
|
+
if File.exists?(f)
|
94
|
+
curated_file = f
|
95
|
+
else
|
96
|
+
puts "Supplied config file (#{config_file}) doesn't seem to exist" if verbose
|
97
|
+
exit
|
98
|
+
end
|
99
|
+
else
|
100
|
+
locations_to_try = %w(
|
101
|
+
~/.config/build_status_server/config.yml
|
102
|
+
config/config.yml
|
103
|
+
/etc/build_status_server/config.yml
|
104
|
+
/usr/local/etc/build_status_server/config.yml
|
105
|
+
)
|
106
|
+
|
107
|
+
locations_to_try.each do |possible_conf_file|
|
108
|
+
f = File.expand_path(possible_conf_file)
|
109
|
+
if File.exists?(f)
|
110
|
+
puts "Using #{possible_conf_file}!" if verbose if verbose
|
111
|
+
curated_file = f
|
112
|
+
break
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
puts <<-EOT
|
117
|
+
Looks like there isn't an available configuration file for this program. You
|
118
|
+
can create one in any of the following locations:
|
119
|
+
|
120
|
+
#{locations_to_try.map{|l| File.expand_path(l)}.join("\n ")}
|
121
|
+
|
122
|
+
Here is a sample of the contents for that file:
|
123
|
+
|
124
|
+
#{File.open("#{File.dirname(File.expand_path(__FILE__))}/../../config/config-example.yml").read}
|
125
|
+
|
126
|
+
EOT
|
127
|
+
|
128
|
+
exit
|
129
|
+
end
|
130
|
+
|
131
|
+
puts "Using #{curated_file}!" if verbose
|
132
|
+
@config = YAML.load_file(curated_file)
|
133
|
+
end
|
134
|
+
|
135
|
+
def should_process_build(build_name)
|
136
|
+
# If mask exists, then ...
|
137
|
+
! (!!mask && ((mask_policy == "include" && build_name !~ mask) ||
|
138
|
+
(mask_policy != "include" && build_name =~ mask)))
|
139
|
+
end
|
140
|
+
|
141
|
+
def process_all_statuses
|
142
|
+
pass = true
|
143
|
+
|
144
|
+
@store.values.each do |val|
|
145
|
+
pass &&= (val == "pass" || val == "SUCCESS")
|
146
|
+
end
|
147
|
+
|
148
|
+
pass
|
149
|
+
end
|
150
|
+
|
151
|
+
def notify(status)
|
152
|
+
tcp_client = config["tcp_client"]
|
153
|
+
|
154
|
+
attempts = 0
|
155
|
+
light = status ? tcp_client["pass"] : tcp_client["fail"]
|
156
|
+
|
157
|
+
begin
|
158
|
+
timeout(5) do
|
159
|
+
attempts += 1
|
160
|
+
client = TCPSocket.new(tcp_client["host"], tcp_client["port"])
|
161
|
+
client.print "GET #{light} HTTP/1.0\n\n"
|
162
|
+
answer = client.gets(nil)
|
163
|
+
STDOUT.puts answer if verbose
|
164
|
+
client.close
|
165
|
+
end
|
166
|
+
rescue Timeout::Error => ex
|
167
|
+
STDERR.puts "Error: #{ex} while trying to send #{light}"
|
168
|
+
retry unless attempts > 2
|
169
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => ex
|
170
|
+
STDERR.puts "Error: #{ex} while trying to send #{light}"
|
171
|
+
STDERR.puts "Will wait for 2 seconds and try again..."
|
172
|
+
sleep 2
|
173
|
+
retry unless attempts > 2
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
__END__
|
180
|
+
|
181
|
+
Example payload:
|
182
|
+
{
|
183
|
+
"name":"test",
|
184
|
+
"url":"job/test/",
|
185
|
+
"build":{
|
186
|
+
"full_url":"http://cronus.local:3001/job/test/20/",
|
187
|
+
"number":20,
|
188
|
+
"phase":"FINISHED",
|
189
|
+
"status":"SUCCESS",
|
190
|
+
"url":"job/test/20/"
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
We're getting this error once in a while:
|
195
|
+
/usr/local/lib/ruby/1.8/timeout.rb:64:in `notify': execution expired (Timeout::Error)
|
196
|
+
from /home/jcmuller/build_notifier/lib/server.rb:102:in `notify'
|
197
|
+
from /home/jcmuller/build_notifier/lib/server.rb:33:in `listen'
|
198
|
+
from bin/server:5
|
199
|
+
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module BuildStatusServer
|
2
|
+
class Server
|
3
|
+
attr_reader :config, :store_file, :mask_policy, :verbose
|
4
|
+
attr_accessor :store, :mask
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
load_config_file(options[:config])
|
8
|
+
@verbose = options[:verbose] || config["verbose"]
|
9
|
+
@store_file = File.expand_path(".", config["store"]["filename"])
|
10
|
+
@mask = Regexp.new(config["mask"]["regex"])
|
11
|
+
@mask_policy = config["mask"]["policy"] || "exclude"
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_store
|
15
|
+
@store = begin
|
16
|
+
YAML.load_file(store_file)
|
17
|
+
rescue
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
@store = {} unless store.class == Hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def listen
|
24
|
+
sock = UDPSocket.new
|
25
|
+
udp_server = config["udp_server"]
|
26
|
+
sock.bind(udp_server["address"], udp_server["port"])
|
27
|
+
|
28
|
+
puts "Listening on UDP #{udp_server["address"]}:#{udp_server["port"]}" if verbose
|
29
|
+
|
30
|
+
while true
|
31
|
+
data, addr = sock.recvfrom(2048)
|
32
|
+
#require "ruby-debug"; debugger
|
33
|
+
if process_job(data)
|
34
|
+
status = process_all_statuses
|
35
|
+
notify(status)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
sock.close
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_job(data = "{}")
|
43
|
+
job = JSON.parse(data)
|
44
|
+
|
45
|
+
build_name = job["name"]
|
46
|
+
|
47
|
+
unless should_process_build(build_name)
|
48
|
+
STDOUT.puts "Ignoring #{build_name} (#{mask}--#{mask_policy})" if verbose
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
if job.class != Hash or
|
53
|
+
job["build"].class != Hash
|
54
|
+
STDERR.puts "Pinged with an invalid payload"
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
phase = job["build"]["phase"]
|
59
|
+
status = job["build"]["status"]
|
60
|
+
|
61
|
+
if phase == "FINISHED"
|
62
|
+
STDOUT.puts "Got #{status} for #{build_name} on #{Time.now} [#{job.inspect}]" if verbose
|
63
|
+
case status
|
64
|
+
when "SUCCESS", "FAILURE"
|
65
|
+
load_store
|
66
|
+
store[build_name] = status
|
67
|
+
File.open(store_file, "w") { |file| YAML.dump(store, file) }
|
68
|
+
return true
|
69
|
+
end
|
70
|
+
else
|
71
|
+
STDOUT.puts "Started for #{build_name} on #{Time.now} [#{job.inspect}]" if verbose
|
72
|
+
end
|
73
|
+
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
# Ensure config file exists. If not, copy example into it
|
78
|
+
def load_config_file(config_file)
|
79
|
+
curated_file = nil
|
80
|
+
|
81
|
+
if config_file
|
82
|
+
f = File.expand_path(config_file)
|
83
|
+
if File.exists?(f)
|
84
|
+
curated_file = f
|
85
|
+
else
|
86
|
+
puts "Supplied config file (#{config_file}) doesn't seem to exist" if verbose
|
87
|
+
exit
|
88
|
+
end
|
89
|
+
else
|
90
|
+
locations_to_try = %w(
|
91
|
+
~/.config/build_status_server/config.yml
|
92
|
+
config/config.yml
|
93
|
+
/etc/build_status_server/config.yml
|
94
|
+
/usr/local/etc/build_status_server/config.yml
|
95
|
+
)
|
96
|
+
|
97
|
+
locations_to_try.each do |possible_conf_file|
|
98
|
+
f = File.expand_path(possible_conf_file)
|
99
|
+
if File.exists?(f)
|
100
|
+
puts "Using #{possible_conf_file}!" if verbose if verbose
|
101
|
+
curated_file = f
|
102
|
+
break
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
puts <<-EOT
|
107
|
+
Looks like there isn't an available configuration file for this program. You
|
108
|
+
can create one in any of the following locations:
|
109
|
+
|
110
|
+
#{locations_to_try.map{|l| File.expand_path(l)}.join("\n ")}
|
111
|
+
|
112
|
+
Here is a sample of the contents for that file:
|
113
|
+
|
114
|
+
#{File.open("#{File.dirname(File.expand_path(__FILE__))}/../../config/config-example.yml").read}
|
115
|
+
|
116
|
+
EOT
|
117
|
+
|
118
|
+
exit
|
119
|
+
end
|
120
|
+
|
121
|
+
puts "Using #{curated_file}!" if verbose
|
122
|
+
@config = YAML.load_file(curated_file)
|
123
|
+
end
|
124
|
+
|
125
|
+
def should_process_build(build_name)
|
126
|
+
# If mask exists, then ...
|
127
|
+
! (!!mask && ((mask_policy == "include" && build_name !~ mask) ||
|
128
|
+
(mask_policy != "include" && build_name =~ mask)))
|
129
|
+
end
|
130
|
+
|
131
|
+
def process_all_statuses
|
132
|
+
pass = true
|
133
|
+
|
134
|
+
@store.values.each do |val|
|
135
|
+
pass &&= (val == "pass" || val == "SUCCESS")
|
136
|
+
end
|
137
|
+
|
138
|
+
pass
|
139
|
+
end
|
140
|
+
|
141
|
+
def notify(status)
|
142
|
+
tcp_client = config["tcp_client"]
|
143
|
+
|
144
|
+
attempts = 0
|
145
|
+
light = status ? tcp_client["pass"] : tcp_client["fail"]
|
146
|
+
|
147
|
+
begin
|
148
|
+
timeout(5) do
|
149
|
+
attempts += 1
|
150
|
+
client = TCPSocket.new(tcp_client["host"], tcp_client["port"])
|
151
|
+
client.print "GET #{light} HTTP/1.0\n\n"
|
152
|
+
answer = client.gets(nil)
|
153
|
+
STDOUT.puts answer if verbose
|
154
|
+
client.close
|
155
|
+
end
|
156
|
+
rescue Timeout::Error => ex
|
157
|
+
STDERR.puts "Error: #{ex} while trying to send #{light}"
|
158
|
+
retry unless attempts > 2
|
159
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => ex
|
160
|
+
STDERR.puts "Error: #{ex} while trying to send #{light}"
|
161
|
+
STDERR.puts "Will wait for 2 seconds and try again..."
|
162
|
+
sleep 2
|
163
|
+
retry unless attempts > 2
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
__END__
|
170
|
+
|
171
|
+
Example payload:
|
172
|
+
{
|
173
|
+
"name":"test",
|
174
|
+
"url":"job/test/",
|
175
|
+
"build":{
|
176
|
+
"full_url":"http://cronus.local:3001/job/test/20/",
|
177
|
+
"number":20,
|
178
|
+
"phase":"FINISHED",
|
179
|
+
"status":"SUCCESS",
|
180
|
+
"url":"job/test/20/"
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
We're getting this error once in a while:
|
185
|
+
/usr/local/lib/ruby/1.8/timeout.rb:64:in `notify': execution expired (Timeout::Error)
|
186
|
+
from /home/jcmuller/build_notifier/lib/server.rb:102:in `notify'
|
187
|
+
from /home/jcmuller/build_notifier/lib/server.rb:33:in `listen'
|
188
|
+
from bin/server:5
|
189
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
$:.push File.expand_path("lib", __FILE__)
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'build_status_server'
|
5
|
+
|
6
|
+
describe BuildStatusServer::Server, :pending => true do
|
7
|
+
let!(:server) { BuildStatusServer::Server.new(:config => '/dev/null') }
|
8
|
+
|
9
|
+
describe "#load_config_file" do
|
10
|
+
it "should use the supplied argument" do
|
11
|
+
config_file = '/dev/null'
|
12
|
+
File.should_receive(:exists?).and_return(true)
|
13
|
+
server.load_config_file(config_file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#load_store" do
|
18
|
+
|
19
|
+
before do
|
20
|
+
server.stub!(:store_file).and_return("/tmp/build")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "initializes an empty hash if store file doesn't exist" do
|
24
|
+
server.load_store
|
25
|
+
server.store.should == {}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "initializes an empty hash if store file is empty" do
|
29
|
+
require "tempfile"
|
30
|
+
f = Tempfile.new("server_spec")
|
31
|
+
server.stub!(:store_file).and_return(f.path)
|
32
|
+
|
33
|
+
server.load_store
|
34
|
+
server.store.should == {}
|
35
|
+
end
|
36
|
+
|
37
|
+
it "initializes a hash with the contents of the store file" do
|
38
|
+
server.stub!(:store_file).and_return("spec/support/build_result.yml")
|
39
|
+
server.load_store
|
40
|
+
|
41
|
+
server.store.should == {"blah" => "SUCCESS", "test" => "SUCCESS"}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#notify"
|
46
|
+
describe "#process_all_statuses"
|
47
|
+
describe "#process_job"
|
48
|
+
|
49
|
+
describe "#should_process_build" do
|
50
|
+
context "when mask exists" do
|
51
|
+
before do
|
52
|
+
server.stub!(:mask).and_return(/.*master.*/)
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when policy is include" do
|
56
|
+
before do
|
57
|
+
server.stub!(:mask_policy).and_return("include")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "ignores builds if mask doesn't match build name" do
|
61
|
+
server.should_process_build("blah-development").should be_false
|
62
|
+
end
|
63
|
+
|
64
|
+
it "processes builds if mask matches build name" do
|
65
|
+
server.should_process_build("blah-master").should be_true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when policy is exclude" do
|
70
|
+
before do
|
71
|
+
server.stub!(:mask_policy).and_return("exclude")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "ignores builds if mask matches build name" do
|
75
|
+
server.should_process_build("blah-master").should be_false
|
76
|
+
end
|
77
|
+
|
78
|
+
it "processes builds if mask doesn't match build name" do
|
79
|
+
server.should_process_build("blah-development").should be_true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when policy is undefined" do
|
84
|
+
before do
|
85
|
+
server.stub!(:mask_policy).and_return(nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "ignores builds if mask matches build name" do
|
89
|
+
server.should_process_build("blah-master").should be_false
|
90
|
+
end
|
91
|
+
|
92
|
+
it "processes builds if mask doesn't match build name" do
|
93
|
+
server.should_process_build("blah-development").should be_true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when policy is unexpected" do
|
98
|
+
before do
|
99
|
+
server.stub!(:mask_policy).and_return("trash")
|
100
|
+
end
|
101
|
+
|
102
|
+
it "ignores builds if mask matches build name" do
|
103
|
+
server.should_process_build("blah-master").should be_false
|
104
|
+
end
|
105
|
+
|
106
|
+
it "processes builds if mask doesn't match build name" do
|
107
|
+
server.should_process_build("blah-development").should be_true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when mask doesn't" do
|
113
|
+
before do
|
114
|
+
server.stub!(:mask).and_return(nil)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should process all jobs" do
|
118
|
+
server.should_process_build("blah-development").should be_true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# vim:set foldmethod=syntax foldlevel=1:
|
@@ -0,0 +1,124 @@
|
|
1
|
+
$:.push File.expand_path("lib", __FILE__)
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'build_status_server'
|
5
|
+
|
6
|
+
describe BuildStatusServer::Server do
|
7
|
+
let!(:server) { BuildStatusServer::Server.new(:config => '/dev/null') }
|
8
|
+
|
9
|
+
describe "#load_config_file" do
|
10
|
+
it "should use the supplied argument" do
|
11
|
+
config_file = '/dev/null'
|
12
|
+
File.should_receive(:exists?).and_return(true)
|
13
|
+
server.load_config_file(config_file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#load_store" do
|
18
|
+
|
19
|
+
before do
|
20
|
+
server.stub!(:store_file).and_return("/tmp/build")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "initializes an empty hash if store file doesn't exist" do
|
24
|
+
server.load_store
|
25
|
+
server.store.should == {}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "initializes an empty hash if store file is empty" do
|
29
|
+
require "tempfile"
|
30
|
+
f = Tempfile.new("server_spec")
|
31
|
+
server.stub!(:store_file).and_return(f.path)
|
32
|
+
|
33
|
+
server.load_store
|
34
|
+
server.store.should == {}
|
35
|
+
end
|
36
|
+
|
37
|
+
it "initializes a hash with the contents of the store file" do
|
38
|
+
server.stub!(:store_file).and_return("spec/support/build_result.yml")
|
39
|
+
server.load_store
|
40
|
+
|
41
|
+
server.store.should == {"blah" => "SUCCESS", "test" => "SUCCESS"}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#notify"
|
46
|
+
describe "#process_all_statuses"
|
47
|
+
describe "#process_job"
|
48
|
+
|
49
|
+
describe "#should_process_build" do
|
50
|
+
context "when mask exists" do
|
51
|
+
before do
|
52
|
+
server.stub!(:mask).and_return(/.*master.*/)
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when policy is include" do
|
56
|
+
before do
|
57
|
+
server.stub!(:mask_policy).and_return("include")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "ignores builds if mask doesn't match build name" do
|
61
|
+
server.should_process_build("blah-development").should be_false
|
62
|
+
end
|
63
|
+
|
64
|
+
it "processes builds if mask matches build name" do
|
65
|
+
server.should_process_build("blah-master").should be_true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when policy is exclude" do
|
70
|
+
before do
|
71
|
+
server.stub!(:mask_policy).and_return("exclude")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "ignores builds if mask matches build name" do
|
75
|
+
server.should_process_build("blah-master").should be_false
|
76
|
+
end
|
77
|
+
|
78
|
+
it "processes builds if mask doesn't match build name" do
|
79
|
+
server.should_process_build("blah-development").should be_true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when policy is undefined" do
|
84
|
+
before do
|
85
|
+
server.stub!(:mask_policy).and_return(nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "ignores builds if mask matches build name" do
|
89
|
+
server.should_process_build("blah-master").should be_false
|
90
|
+
end
|
91
|
+
|
92
|
+
it "processes builds if mask doesn't match build name" do
|
93
|
+
server.should_process_build("blah-development").should be_true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when policy is unexpected" do
|
98
|
+
before do
|
99
|
+
server.stub!(:mask_policy).and_return("trash")
|
100
|
+
end
|
101
|
+
|
102
|
+
it "ignores builds if mask matches build name" do
|
103
|
+
server.should_process_build("blah-master").should be_false
|
104
|
+
end
|
105
|
+
|
106
|
+
it "processes builds if mask doesn't match build name" do
|
107
|
+
server.should_process_build("blah-development").should be_true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when mask doesn't" do
|
113
|
+
before do
|
114
|
+
server.stub!(:mask).and_return(nil)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should process all jobs" do
|
118
|
+
server.should_process_build("blah-development").should be_true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# vim:set foldmethod=syntax foldlevel=1:
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require "rspec/core"
|
9
|
+
require "rspec/mocks"
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
config.run_all_when_everything_filtered = true
|
14
|
+
config.filter_run :focus
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
#require "rspec/core"
|
9
|
+
#require "rspec/mocks"
|
10
|
+
#
|
11
|
+
#RSpec.configure do |config|
|
12
|
+
# config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
# config.run_all_when_everything_filtered = true
|
14
|
+
# config.filter_run :focus
|
15
|
+
#end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: build_status_server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 3
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
version: "0.4"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Juan C. Muller
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-04-26 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: ruby-debug
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: sinatra
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: json
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
description: A build notifier server for Jenkins CI that controls an XFD over HTTP
|
63
|
+
email: jcmuller@gmail.com
|
64
|
+
executables:
|
65
|
+
- build_status_server
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files: []
|
69
|
+
|
70
|
+
files:
|
71
|
+
- lib/build_status_server/requirements.rb~
|
72
|
+
- lib/build_status_server/server.rb
|
73
|
+
- lib/build_status_server/server.rb~
|
74
|
+
- lib/build_status_server/version.rb
|
75
|
+
- lib/build_status_server/version.rb~
|
76
|
+
- lib/build_status_server.rb
|
77
|
+
- lib/build_status_server.rb~
|
78
|
+
- spec/lib/build_status_server_spec.rb
|
79
|
+
- spec/lib/build_status_server_spec.rb~
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
- spec/spec_helper.rb~
|
82
|
+
- spec/support/build_result.yml
|
83
|
+
- spec/support/sample.json
|
84
|
+
- bin/build_status_server
|
85
|
+
- config/config-example.yml
|
86
|
+
- LICENSE
|
87
|
+
- README.md
|
88
|
+
- Gemfile
|
89
|
+
- Gemfile.lock
|
90
|
+
- build_status_server.gemspec
|
91
|
+
has_rdoc: true
|
92
|
+
homepage: http://github.com/jcmuller/build_status_server
|
93
|
+
licenses:
|
94
|
+
- GPL
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
hash: 3
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
version: "0"
|
118
|
+
requirements: []
|
119
|
+
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.6.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: A build notifier server for Jenkins CI that controls an XFD over HTTP
|
125
|
+
test_files:
|
126
|
+
- spec/lib/build_status_server_spec.rb
|