janky 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +3 -0
- data/COPYING +22 -0
- data/Gemfile +2 -0
- data/README.md +211 -0
- data/Rakefile +19 -0
- data/config.ru +3 -0
- data/janky.gemspec +102 -0
- data/lib/janky.rb +224 -0
- data/lib/janky/app.rb +81 -0
- data/lib/janky/branch.rb +112 -0
- data/lib/janky/build.rb +223 -0
- data/lib/janky/build_request.rb +49 -0
- data/lib/janky/builder.rb +108 -0
- data/lib/janky/builder/client.rb +82 -0
- data/lib/janky/builder/http.rb +43 -0
- data/lib/janky/builder/mock.rb +45 -0
- data/lib/janky/builder/payload.rb +63 -0
- data/lib/janky/builder/receiver.rb +20 -0
- data/lib/janky/builder/runner.rb +47 -0
- data/lib/janky/campfire.rb +127 -0
- data/lib/janky/campfire/mock.rb +0 -0
- data/lib/janky/commit.rb +14 -0
- data/lib/janky/database/migrate/1312115512_init.rb +48 -0
- data/lib/janky/database/migrate/1312117285_non_unique_repo_uri.rb +10 -0
- data/lib/janky/database/migrate/1312198807_repo_enabled.rb +11 -0
- data/lib/janky/database/migrate/1313867551_add_build_output_column.rb +9 -0
- data/lib/janky/database/migrate/1313871652_add_commit_url_column.rb +9 -0
- data/lib/janky/database/migrate/1317384618_add_repo_hook_url.rb +9 -0
- data/lib/janky/database/migrate/1317384619_add_build_room_id.rb +9 -0
- data/lib/janky/database/migrate/1317384629_drop_default_room_id.rb +9 -0
- data/lib/janky/database/migrate/1317384649_github_team_id.rb +9 -0
- data/lib/janky/database/schema.rb +68 -0
- data/lib/janky/database/seed.dump.gz +0 -0
- data/lib/janky/exception.rb +62 -0
- data/lib/janky/github.rb +67 -0
- data/lib/janky/github/api.rb +69 -0
- data/lib/janky/github/commit.rb +27 -0
- data/lib/janky/github/mock.rb +47 -0
- data/lib/janky/github/payload.rb +34 -0
- data/lib/janky/github/payload_parser.rb +57 -0
- data/lib/janky/github/receiver.rb +69 -0
- data/lib/janky/helpers.rb +17 -0
- data/lib/janky/hubot.rb +117 -0
- data/lib/janky/job_creator.rb +111 -0
- data/lib/janky/notifier.rb +84 -0
- data/lib/janky/notifier/campfire.rb +21 -0
- data/lib/janky/notifier/mock.rb +55 -0
- data/lib/janky/notifier/multi.rb +22 -0
- data/lib/janky/public/css/base.css +204 -0
- data/lib/janky/public/images/building-bot.gif +0 -0
- data/lib/janky/public/images/disclosure-arrow.png +0 -0
- data/lib/janky/public/images/logo.png +0 -0
- data/lib/janky/public/images/robawt-status.gif +0 -0
- data/lib/janky/public/javascripts/application.js +3 -0
- data/lib/janky/public/javascripts/jquery.js +16 -0
- data/lib/janky/public/javascripts/jquery.relatize.js +111 -0
- data/lib/janky/repository.rb +174 -0
- data/lib/janky/tasks.rb +36 -0
- data/lib/janky/templates/console.mustache +4 -0
- data/lib/janky/templates/index.mustache +11 -0
- data/lib/janky/templates/layout.mustache +22 -0
- data/lib/janky/version.rb +3 -0
- data/lib/janky/views/console.rb +33 -0
- data/lib/janky/views/index.rb +35 -0
- data/lib/janky/views/layout.rb +19 -0
- data/test/default.xml.erb +0 -0
- data/test/janky_test.rb +271 -0
- data/test/test_helper.rb +107 -0
- metadata +319 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
module Janky
|
2
|
+
# Triggers Jenkins builds and handles callbacks.
|
3
|
+
#
|
4
|
+
# The HTTP requests flow goes like this:
|
5
|
+
#
|
6
|
+
# 1. Send a Build request to the Jenkins server over HTTP. The resulting
|
7
|
+
# build URL is stored in Build#url.
|
8
|
+
#
|
9
|
+
# 2. Once Jenkins picks up the build and starts running it, it sends a callback
|
10
|
+
# handled by the `receiver` Rack app, which transitions the build into a
|
11
|
+
# building state.
|
12
|
+
#
|
13
|
+
# 3. Finally, Jenkins sends another callback with the build result and the
|
14
|
+
# build is transitioned to a completed and green/red state.
|
15
|
+
#
|
16
|
+
# The Mock adapter provides methods to simulate that flow without having to
|
17
|
+
# go over the wire.
|
18
|
+
module Builder
|
19
|
+
# Set the callback URL of builder clients. Must be called before
|
20
|
+
# registering any client.
|
21
|
+
#
|
22
|
+
# callback_url - The absolute callback URL as a String.
|
23
|
+
#
|
24
|
+
# Returns nothing.
|
25
|
+
def self.setup(callback_url)
|
26
|
+
@callback_url = callback_url
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Define the rule for picking a builder.
|
30
|
+
#
|
31
|
+
# block - Required block that will be given a Repository object when
|
32
|
+
# picking a builder. Must return a Client object.
|
33
|
+
#
|
34
|
+
# Returns nothing.
|
35
|
+
def self.choose(&block)
|
36
|
+
@chooser = block
|
37
|
+
end
|
38
|
+
|
39
|
+
# Pick the appropriate builder for a repo based on the rule set by the
|
40
|
+
# choose method. Uses the default builder when no rule is defined.
|
41
|
+
#
|
42
|
+
# repo - a Repository object.
|
43
|
+
#
|
44
|
+
# Returns a Client object.
|
45
|
+
def self.pick_for(repo)
|
46
|
+
if block = @chooser
|
47
|
+
block.call(repo)
|
48
|
+
else
|
49
|
+
self[:default]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Register a new build host.
|
54
|
+
#
|
55
|
+
# url - The String URL of the Jenkins server.
|
56
|
+
#
|
57
|
+
# Returns the new Client instance.
|
58
|
+
def self.[]=(builder, url)
|
59
|
+
builders[builder] = Client.new(url, @callback_url)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get the Client for a registered build host.
|
63
|
+
#
|
64
|
+
# builder - the String name of the build host.
|
65
|
+
#
|
66
|
+
# Returns the Client instance.
|
67
|
+
def self.[](builder)
|
68
|
+
builders[builder] ||
|
69
|
+
raise(Error, "Unknown builder: #{builder.inspect}")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Registered build hosts.
|
73
|
+
#
|
74
|
+
# Returns an Array of Client.
|
75
|
+
def self.builders
|
76
|
+
@builders ||= {}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Rack app handling HTTP callbacks coming from the Jenkins server.
|
80
|
+
def self.receiver
|
81
|
+
@receiver ||= Janky::Builder::Receiver
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.enable_mock!
|
85
|
+
builders.values.each { |b| b.enable_mock! }
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.green!
|
89
|
+
builders.values.each { |b| b.green! }
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.red!
|
93
|
+
builders.values.each { |b| b.red! }
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.reset!
|
97
|
+
builders.values.each { |b| b.reset! }
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.start!
|
101
|
+
builders.values.each { |b| b.start! }
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.complete!
|
105
|
+
builders.values.each { |b| b.complete! }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class Client
|
4
|
+
def initialize(url, callback_url)
|
5
|
+
@url = URI(url)
|
6
|
+
@callback_url = URI(callback_url)
|
7
|
+
end
|
8
|
+
|
9
|
+
# The String absolute URL of the Jenkins server.
|
10
|
+
attr_reader :url
|
11
|
+
|
12
|
+
# The String absoulte URL callback of this Janky host.
|
13
|
+
attr_reader :callback_url
|
14
|
+
|
15
|
+
# Trigger a Jenkins build for the given Build.
|
16
|
+
#
|
17
|
+
# build - a Build object.
|
18
|
+
#
|
19
|
+
# Returns the Jenkins build URL.
|
20
|
+
def run(build)
|
21
|
+
Runner.new(@url, build, adapter).run
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve the output of the given Build.
|
25
|
+
#
|
26
|
+
# build - a Build object. Must have an url attribute.
|
27
|
+
#
|
28
|
+
# Returns the String build output.
|
29
|
+
def output(build)
|
30
|
+
Runner.new(@url, build, adapter).output
|
31
|
+
end
|
32
|
+
|
33
|
+
# Setup a job on the Jenkins server.
|
34
|
+
#
|
35
|
+
# name - The desired job name as a String.
|
36
|
+
# repo_uri - The repository git URI as a String.
|
37
|
+
# template_path - The Pathname to the XML config template.
|
38
|
+
#
|
39
|
+
# Returns nothing.
|
40
|
+
def setup(name, repo_uri, template_path)
|
41
|
+
job_creator.run(name, repo_uri, template_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
# The adapter used to trigger builds. Defaults to HTTP, which hits the
|
45
|
+
# Jenkins server configured by `setup`.
|
46
|
+
def adapter
|
47
|
+
@adapter ||= HTTP.new(url.user, url.password)
|
48
|
+
end
|
49
|
+
|
50
|
+
def job_creator
|
51
|
+
@job_creator ||= JobCreator.new(url, @callback_url)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Enable the mock adapter and make subsequent builds green.
|
55
|
+
def green!
|
56
|
+
@adapter = Mock.new(true, Janky.app)
|
57
|
+
job_creator.enable_mock!
|
58
|
+
end
|
59
|
+
|
60
|
+
# Alias green! as enable_mock!
|
61
|
+
alias_method :enable_mock!, :green!
|
62
|
+
|
63
|
+
# Alias green! as reset!
|
64
|
+
alias_method :reset!, :green!
|
65
|
+
|
66
|
+
# Enable the mock adapter and make subsequent builds red.
|
67
|
+
def red!
|
68
|
+
@adapter = Mock.new(false, Janky.app)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Simulate the first callback. Only available when mocked.
|
72
|
+
def start!
|
73
|
+
@adapter.start
|
74
|
+
end
|
75
|
+
|
76
|
+
# Simulate the last callback. Only available when mocked.
|
77
|
+
def complete!
|
78
|
+
@adapter.complete
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class HTTP
|
4
|
+
def initialize(username, password)
|
5
|
+
@username = username
|
6
|
+
@password = password
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(params, create_url)
|
10
|
+
http = Net::HTTP.new(create_url.host, create_url.port)
|
11
|
+
request = Net::HTTP::Post.new(create_url.path)
|
12
|
+
if @username && @password
|
13
|
+
request.basic_auth(@username, @password)
|
14
|
+
end
|
15
|
+
request.form_data = {"json" => params}
|
16
|
+
|
17
|
+
response = http.request(request)
|
18
|
+
|
19
|
+
unless response.code == "302"
|
20
|
+
Exception.push_http_response(response)
|
21
|
+
raise Error, "Failed to create build"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def output(url)
|
26
|
+
http = Net::HTTP.new(url.host, url.port)
|
27
|
+
request = Net::HTTP::Get.new(url.path)
|
28
|
+
if @username && @password
|
29
|
+
request.basic_auth(@username, @password)
|
30
|
+
end
|
31
|
+
|
32
|
+
response = http.request(request)
|
33
|
+
|
34
|
+
unless response.code == "200"
|
35
|
+
Exception.push_http_response(response)
|
36
|
+
raise Error, "Failed to get build output"
|
37
|
+
end
|
38
|
+
|
39
|
+
response.body
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class Mock
|
4
|
+
def initialize(green, app)
|
5
|
+
@green = green
|
6
|
+
@app = app
|
7
|
+
@builds = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(params, create_url)
|
11
|
+
params = Yajl.load(params)["parameter"]
|
12
|
+
param = params.detect{ |p| p["name"] == "JANKY_ID" }
|
13
|
+
build_id = param["value"]
|
14
|
+
url = create_url.to_s.gsub("build", build_id.to_s)
|
15
|
+
|
16
|
+
@builds << [build_id, "#{url}/", @green]
|
17
|
+
end
|
18
|
+
|
19
|
+
def output(build)
|
20
|
+
"....FFFUUUUUUU"
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
@builds.each do |id, url, _|
|
25
|
+
payload = Payload.start(id, url)
|
26
|
+
request(payload)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def complete
|
31
|
+
@builds.each do |id, _, green|
|
32
|
+
payload = Payload.complete(id, green)
|
33
|
+
request(payload)
|
34
|
+
end
|
35
|
+
@builds.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def request(payload)
|
39
|
+
Rack::MockRequest.new(@app).post("/_builder",
|
40
|
+
:input => payload.to_json
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class Payload
|
4
|
+
def self.parse(json)
|
5
|
+
parsed = Yajl.load(json)
|
6
|
+
build = parsed["build"]
|
7
|
+
|
8
|
+
new(
|
9
|
+
build["phase"],
|
10
|
+
build["parameters"]["JANKY_ID"],
|
11
|
+
build["full_url"],
|
12
|
+
build["status"]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.start(id, url)
|
17
|
+
new("STARTED", id, url, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.complete(id, green)
|
21
|
+
status = (green ? "SUCCESS" : "FAILED")
|
22
|
+
new("FINISHED", id, nil, status)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(phase, id, url, status)
|
26
|
+
@phase = phase
|
27
|
+
@id = id
|
28
|
+
@url = url
|
29
|
+
@status = status
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :id, :url
|
33
|
+
|
34
|
+
def started?
|
35
|
+
@phase == "STARTED"
|
36
|
+
end
|
37
|
+
|
38
|
+
def completed?
|
39
|
+
@phase == "FINISHED"
|
40
|
+
end
|
41
|
+
|
42
|
+
def green?
|
43
|
+
if completed?
|
44
|
+
@status == "SUCCESS"
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_json
|
51
|
+
{ :build => {
|
52
|
+
:phase => @phase,
|
53
|
+
:status => @status,
|
54
|
+
:full_url => @url,
|
55
|
+
:parameters => {
|
56
|
+
"JANKY_ID" => @id
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}.to_json
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class Receiver
|
4
|
+
def self.call(env)
|
5
|
+
request = Rack::Request.new(env)
|
6
|
+
payload = Payload.parse(request.body)
|
7
|
+
|
8
|
+
if payload.started?
|
9
|
+
Build.start(payload.id, payload.url)
|
10
|
+
elsif payload.completed?
|
11
|
+
Build.complete(payload.id, payload.green?)
|
12
|
+
else
|
13
|
+
return Rack::Response.new("Invalid", 402).finish
|
14
|
+
end
|
15
|
+
|
16
|
+
Rack::Response.new("OK", 201).finish
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Janky
|
2
|
+
module Builder
|
3
|
+
class Runner
|
4
|
+
def initialize(base_url, build, adapter)
|
5
|
+
@base_url = base_url
|
6
|
+
@build = build
|
7
|
+
@adapter = adapter
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
context_push
|
12
|
+
@adapter.run(json_params, create_url)
|
13
|
+
end
|
14
|
+
|
15
|
+
def output
|
16
|
+
context_push
|
17
|
+
@adapter.output(output_url)
|
18
|
+
end
|
19
|
+
|
20
|
+
def json_params
|
21
|
+
Yajl.dump(:parameter => [
|
22
|
+
{ :name => "JANKY_SHA1", :value => @build.sha1 },
|
23
|
+
{ :name => "JANKY_ID", :value => @build.id }
|
24
|
+
])
|
25
|
+
end
|
26
|
+
|
27
|
+
def output_url
|
28
|
+
URI(@build.url + "consoleText")
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_url
|
32
|
+
URI("#{@base_url}job/#{@build.repo_job_name}/build")
|
33
|
+
end
|
34
|
+
|
35
|
+
def context_push
|
36
|
+
Exception.push(
|
37
|
+
:base_url => @base_url.inspect,
|
38
|
+
:build => @build.inspect,
|
39
|
+
:adapter => @adapter.inspect,
|
40
|
+
:params => json_params.inspect,
|
41
|
+
:create_url => create_url.inspect
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Janky
|
2
|
+
# Sends messages to Campfire and accesses available rooms.
|
3
|
+
module Campfire
|
4
|
+
# Setup the Campfire client with the given credentials.
|
5
|
+
#
|
6
|
+
# account - the Campfire account name as a String.
|
7
|
+
# token - the Campfire API token as a String.
|
8
|
+
# default - the name of the default Campfire room as a String.
|
9
|
+
#
|
10
|
+
# Returns nothing.
|
11
|
+
def self.setup(account, token, default)
|
12
|
+
::Broach.settings = {
|
13
|
+
"account" => account,
|
14
|
+
"token" => token,
|
15
|
+
"use_ssl" => true
|
16
|
+
}
|
17
|
+
|
18
|
+
self.default_room_name = default
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :default_room_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.default_room_id
|
26
|
+
room_id(default_room_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Send a message to a Campfire room.
|
30
|
+
#
|
31
|
+
# message - The String message.
|
32
|
+
# room_id - The Integer room ID.
|
33
|
+
#
|
34
|
+
# Returns nothing.
|
35
|
+
def self.speak(message, room_id)
|
36
|
+
adapter.speak(room_name(room_id), message)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the ID of a room.
|
40
|
+
#
|
41
|
+
# slug - the String name of the room.
|
42
|
+
#
|
43
|
+
# Returns the room ID or nil for unknown rooms.
|
44
|
+
def self.room_id(name)
|
45
|
+
if room = rooms.detect { |room| room.name == name }
|
46
|
+
room.id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the name of a room given its ID.
|
51
|
+
#
|
52
|
+
# id - the Fixnum room ID.
|
53
|
+
#
|
54
|
+
# Returns the name as a String or nil when not found.
|
55
|
+
def self.room_name(id)
|
56
|
+
if room = rooms.detect { |room| room.id.to_s == id.to_s }
|
57
|
+
room.name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get a list of all rooms names.
|
62
|
+
#
|
63
|
+
# Returns an Array of room name as Strings.
|
64
|
+
def self.room_names
|
65
|
+
rooms.map { |room| room.name }.sort
|
66
|
+
end
|
67
|
+
|
68
|
+
# Memoized list of available rooms.
|
69
|
+
#
|
70
|
+
# Returns an Array of Broach::Room objects.
|
71
|
+
def self.rooms
|
72
|
+
@rooms ||= adapter.rooms
|
73
|
+
end
|
74
|
+
|
75
|
+
# Enable mocking. Once enabled, messages are discarded.
|
76
|
+
#
|
77
|
+
# Returns nothing.
|
78
|
+
def self.enable_mock!
|
79
|
+
@adapter = Mock.new
|
80
|
+
end
|
81
|
+
|
82
|
+
# Configure available rooms. Only available in mock mode.
|
83
|
+
#
|
84
|
+
# value - Hash of room map (Fixnum ID => String name)
|
85
|
+
#
|
86
|
+
# Returns nothing.
|
87
|
+
def self.rooms=(value)
|
88
|
+
adapter.rooms = value
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.adapter
|
92
|
+
@adapter ||= Broach.new
|
93
|
+
end
|
94
|
+
|
95
|
+
class Broach
|
96
|
+
def speak(room_name, message)
|
97
|
+
::Broach.speak(room_name, message)
|
98
|
+
end
|
99
|
+
|
100
|
+
def rooms
|
101
|
+
::Broach.rooms
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Mock
|
106
|
+
def initialize
|
107
|
+
@rooms = {}
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_writer :rooms
|
111
|
+
|
112
|
+
def speak(room_name, message)
|
113
|
+
if !@rooms.values.include?(room_name)
|
114
|
+
raise Error, "Unknown room #{room_name.inspect}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def rooms
|
119
|
+
acc = []
|
120
|
+
@rooms.each do |id, name|
|
121
|
+
acc << ::Broach::Room.new("id" => id, "name" => name)
|
122
|
+
end
|
123
|
+
acc
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|