janky 0.9.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.
- 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
|