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,47 @@
|
|
1
|
+
module Janky
|
2
|
+
module GitHub
|
3
|
+
class Mock
|
4
|
+
Response = Struct.new(:code, :body)
|
5
|
+
|
6
|
+
def initialize(user, password)
|
7
|
+
@repos = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def make_private(nwo)
|
11
|
+
@repos[nwo] = :private
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_public(nwo)
|
15
|
+
@repos[nwo] = :public
|
16
|
+
end
|
17
|
+
|
18
|
+
def make_unauthorized(nwo)
|
19
|
+
@repos[nwo] = :unauthorized
|
20
|
+
end
|
21
|
+
|
22
|
+
def create(nwo, secret, url)
|
23
|
+
data = {"url" => "https://api.github.com/hooks/#{Time.now.to_f}"}
|
24
|
+
Response.new("201", Yajl.dump(data))
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(url)
|
28
|
+
Response.new("200")
|
29
|
+
end
|
30
|
+
|
31
|
+
def repo_get(nwo)
|
32
|
+
repo = {
|
33
|
+
"name" => nwo.split("/").last,
|
34
|
+
"private" => (@repos[nwo] == :private),
|
35
|
+
"git_url" => "git://github.com/#{nwo}",
|
36
|
+
"ssh_url" => "git@github.com:#{nwo}"
|
37
|
+
}
|
38
|
+
|
39
|
+
if @repos[nwo] == :unauthorized
|
40
|
+
Response.new("404", Yajl.dump({}))
|
41
|
+
else
|
42
|
+
Response.new("200", Yajl.dump(repo))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Janky
|
2
|
+
module GitHub
|
3
|
+
class Payload
|
4
|
+
def self.parse(json)
|
5
|
+
parsed = PayloadParser.new(json)
|
6
|
+
new(parsed.uri, parsed.branch, parsed.head, parsed.commits, parsed.compare)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(uri, branch, head, commits, compare)
|
10
|
+
@uri = uri
|
11
|
+
@branch = branch
|
12
|
+
@head = head
|
13
|
+
@commits = commits
|
14
|
+
@compare = compare
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :uri, :branch, :head, :commits, :compare
|
18
|
+
|
19
|
+
def head_commit
|
20
|
+
@commits.detect do |commit|
|
21
|
+
commit.sha1 == @head
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_json
|
26
|
+
{ :after => @head,
|
27
|
+
:ref => "refs/heads/#{@branch}",
|
28
|
+
:uri => @uri,
|
29
|
+
:commits => @commits,
|
30
|
+
:compare => @compare }.to_json
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Janky
|
2
|
+
module GitHub
|
3
|
+
class PayloadParser
|
4
|
+
def initialize(json)
|
5
|
+
@payload = Yajl.load(json)
|
6
|
+
end
|
7
|
+
|
8
|
+
def head
|
9
|
+
@payload["after"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def compare
|
13
|
+
@payload["compare"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def commits
|
17
|
+
@payload["commits"].map do |commit|
|
18
|
+
GitHub::Commit.new(
|
19
|
+
commit["id"],
|
20
|
+
commit["url"],
|
21
|
+
commit["message"],
|
22
|
+
normalize_author(commit["author"]),
|
23
|
+
commit["timestamp"]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def normalize_author(author)
|
29
|
+
if email = author["email"]
|
30
|
+
"#{author["name"]} <#{email}>"
|
31
|
+
else
|
32
|
+
author
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def uri
|
37
|
+
if uri = @payload["uri"]
|
38
|
+
return uri
|
39
|
+
end
|
40
|
+
|
41
|
+
repository = @payload["repository"]
|
42
|
+
|
43
|
+
if repository["private"]
|
44
|
+
"git@github.com:#{URI(repository["url"]).path[1..-1]}"
|
45
|
+
else
|
46
|
+
uri = URI(repository["url"])
|
47
|
+
uri.scheme = "git"
|
48
|
+
uri.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def branch
|
53
|
+
@payload["ref"].split("refs/heads/").last
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Janky
|
2
|
+
module GitHub
|
3
|
+
# Rack app handling GitHub Post-Receive [1] requests.
|
4
|
+
#
|
5
|
+
# The JSON payload is parsed into a GitHub::Payload. We then find the
|
6
|
+
# associated Repository record based on the Payload's repository git URL
|
7
|
+
# and create the associated records: Branch, Commit and Build.
|
8
|
+
#
|
9
|
+
# Finally, we trigger a new Jenkins build.
|
10
|
+
#
|
11
|
+
# [1]: http://help.github.com/post-receive-hooks/
|
12
|
+
class Receiver
|
13
|
+
def initialize(secret)
|
14
|
+
@secret = secret
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
dup.call!(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def call!(env)
|
22
|
+
@request = Rack::Request.new(env)
|
23
|
+
|
24
|
+
if !valid_signature?
|
25
|
+
return Rack::Response.new("Invalid signature", 403).finish
|
26
|
+
end
|
27
|
+
|
28
|
+
if !payload.head_commit
|
29
|
+
return Rack::Response.new("Ignored", 400).finish
|
30
|
+
end
|
31
|
+
|
32
|
+
result = BuildRequest.handle(
|
33
|
+
payload.uri,
|
34
|
+
payload.branch,
|
35
|
+
payload.head_commit,
|
36
|
+
payload.compare,
|
37
|
+
@request.POST["room"]
|
38
|
+
)
|
39
|
+
|
40
|
+
Rack::Response.new("OK: #{result}", 201).finish
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid_signature?
|
44
|
+
digest = OpenSSL::Digest::Digest.new("sha1")
|
45
|
+
signature = @request.env["HTTP_X_HUB_SIGNATURE"].split("=").last
|
46
|
+
|
47
|
+
signature == OpenSSL::HMAC.hexdigest(digest, @secret, data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def payload
|
51
|
+
@payload ||= GitHub::Payload.parse(data)
|
52
|
+
end
|
53
|
+
|
54
|
+
def data
|
55
|
+
@data ||= data!
|
56
|
+
end
|
57
|
+
|
58
|
+
def data!
|
59
|
+
if @request.content_type != "application/json"
|
60
|
+
return Rack::Response.new("Invalid Content-Type", 400).finish
|
61
|
+
end
|
62
|
+
|
63
|
+
body = ""
|
64
|
+
@request.body.each { |chunk| body << chunk }
|
65
|
+
body
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Janky
|
2
|
+
module Helpers
|
3
|
+
def self.registered(app)
|
4
|
+
app.enable :raise_errors
|
5
|
+
app.disable :show_exceptions
|
6
|
+
app.helpers self
|
7
|
+
end
|
8
|
+
|
9
|
+
def find_repo(name)
|
10
|
+
unless repo = Repository.find_by_name(name)
|
11
|
+
halt(404, "Unknown repository: #{name.inspect}")
|
12
|
+
end
|
13
|
+
|
14
|
+
repo
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/janky/hubot.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module Janky
|
2
|
+
# Web API taylored for Hubot's needs. Supports setting up and disabling
|
3
|
+
# repositories, querying the status of branch or a repository and triggering
|
4
|
+
# builds.
|
5
|
+
#
|
6
|
+
# The client side implementation is at
|
7
|
+
# <https://github.com/github/hubot/blob/master/scripts/ci.js>
|
8
|
+
class Hubot < Sinatra::Base
|
9
|
+
register Helpers
|
10
|
+
|
11
|
+
# Setup a new repository.
|
12
|
+
post "/setup" do
|
13
|
+
nwo = params["nwo"]
|
14
|
+
name = params["name"]
|
15
|
+
repo = Repository.setup(nwo, name)
|
16
|
+
|
17
|
+
if repo
|
18
|
+
url = "#{settings.base_url}/#{repo.name}"
|
19
|
+
[201, "Setup #{repo.name} at #{repo.uri} | #{url}"]
|
20
|
+
else
|
21
|
+
[400, "Couldn't access #{nwo}. Check the permissions."]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Activate/deactivate auto-build for the given repository.
|
26
|
+
post "/toggle/:repo_name" do |repo_name|
|
27
|
+
repo = find_repo(repo_name)
|
28
|
+
status = repo.toggle_auto_build ? "enabled" : "disabled"
|
29
|
+
|
30
|
+
[200, "#{repo.name} is now #{status}"]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Build a repository's branch.
|
34
|
+
post "/:repo_name/:branch" do |repo_name, branch_name|
|
35
|
+
repo = find_repo(repo_name)
|
36
|
+
branch = repo.branch_for(branch_name)
|
37
|
+
build = branch.current_build
|
38
|
+
room_id = params["room_id"] && Integer(params["room_id"])
|
39
|
+
|
40
|
+
if build
|
41
|
+
build.rerun(room_id)
|
42
|
+
|
43
|
+
[201, "Going ham on #{build.repo_name}/#{build.branch_name}"]
|
44
|
+
else
|
45
|
+
[404, "Unknown branch #{branch_name.inspect}. Push again"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get a list of available rooms.
|
50
|
+
get "/rooms" do
|
51
|
+
Yajl.dump(Campfire.room_names)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Update a repository's notification room.
|
55
|
+
put "/:repo_name" do |repo_name|
|
56
|
+
repo = find_repo(repo_name)
|
57
|
+
room = params["room"]
|
58
|
+
|
59
|
+
if room_id = Campfire.room_id(room)
|
60
|
+
repo.update_attributes!(:room_id => room_id)
|
61
|
+
[200, "Room for #{repo.name} updated to #{room}"]
|
62
|
+
else
|
63
|
+
[403, "Unknown room: #{room.inspect}"]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the status of all projects.
|
68
|
+
get "/" do
|
69
|
+
content_type "text/plain"
|
70
|
+
repos = Repository.all.map do |repo|
|
71
|
+
master = repo.branch_for("master")
|
72
|
+
|
73
|
+
"%-17s %-13s %-10s %40s" % [
|
74
|
+
repo.name,
|
75
|
+
master.status,
|
76
|
+
repo.campfire_room,
|
77
|
+
repo.uri
|
78
|
+
]
|
79
|
+
end
|
80
|
+
repos.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the status of a repository's branch.
|
84
|
+
get "/:repo_name/:branch_name" do |repo_name, branch_name|
|
85
|
+
limit = params["limit"]
|
86
|
+
|
87
|
+
repo = find_repo(repo_name)
|
88
|
+
branch = repo.branch_for(branch_name)
|
89
|
+
builds = branch.completed_builds.limit(limit).map do |build|
|
90
|
+
{ :sha1 => build.sha1,
|
91
|
+
:repo => build.repo_name,
|
92
|
+
:branch => build.branch_name,
|
93
|
+
:green => build.green?,
|
94
|
+
:building => branch.building?,
|
95
|
+
:number => build.number,
|
96
|
+
:status => (build.green? ? "was successful" : "failed"),
|
97
|
+
:compare => build.compare,
|
98
|
+
:duration => build.duration }
|
99
|
+
end
|
100
|
+
|
101
|
+
builds.to_json
|
102
|
+
end
|
103
|
+
|
104
|
+
# Learn everything you need to know about Janky.
|
105
|
+
get "/help" do
|
106
|
+
content_type "text/plain"
|
107
|
+
<<-EOS
|
108
|
+
hubot ci build janky
|
109
|
+
hubot ci build janky/fix-everything
|
110
|
+
hubot ci setup github/janky [name]
|
111
|
+
hubot ci toggle janky
|
112
|
+
hubot ci rooms
|
113
|
+
hubot ci set room janky The Danger Room
|
114
|
+
EOS
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Janky
|
2
|
+
class JobCreator
|
3
|
+
def initialize(server_url, callback_url)
|
4
|
+
@server_url = server_url
|
5
|
+
@callback_url = callback_url
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(name, uri, template_path)
|
9
|
+
creator.run(name, uri, template_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def creator
|
13
|
+
@creator ||= Creator.new(HTTP, @server_url, @callback_url)
|
14
|
+
end
|
15
|
+
|
16
|
+
def enable_mock!
|
17
|
+
@creator = Creator.new(Mock.new, @server_url, @callback_url)
|
18
|
+
end
|
19
|
+
|
20
|
+
class Creator
|
21
|
+
def initialize(adapter, server_url, callback_url)
|
22
|
+
@adapter = adapter
|
23
|
+
@server_url = server_url
|
24
|
+
@callback_url = callback_url
|
25
|
+
end
|
26
|
+
|
27
|
+
def run(name, uri, template_path)
|
28
|
+
template = Tilt.new(template_path.to_s)
|
29
|
+
config = template.render(Object.new, {
|
30
|
+
:name => name,
|
31
|
+
:repo => uri,
|
32
|
+
:callback_url => @callback_url
|
33
|
+
})
|
34
|
+
|
35
|
+
exception_context(config, name, uri)
|
36
|
+
|
37
|
+
if !@adapter.exists?(@server_url, name)
|
38
|
+
@adapter.run(@server_url, name, config)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def exception_context(config, name, uri)
|
44
|
+
Exception.push(
|
45
|
+
:server_url => @server_url.inspect,
|
46
|
+
:callback_url => @callback_url.inspect,
|
47
|
+
:adapter => @adapter.inspect,
|
48
|
+
:config => config.inspect,
|
49
|
+
:name => name.inspect,
|
50
|
+
:repo => uri.inspect
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Mock
|
56
|
+
def run(server_url, name, config)
|
57
|
+
name || raise(Error, "no name")
|
58
|
+
config || raise(Error, "no config")
|
59
|
+
(URI === server_url) || raise(Error, "server_url is not a URI")
|
60
|
+
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def exists?(server_url, name)
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class HTTP
|
70
|
+
def self.exists?(server_url, name)
|
71
|
+
uri = server_url
|
72
|
+
user = uri.user
|
73
|
+
pass = uri.password
|
74
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
75
|
+
|
76
|
+
get = Net::HTTP::Get.new("/job/#{name}/")
|
77
|
+
get.basic_auth(user, pass) if user && pass
|
78
|
+
response = http.request(get)
|
79
|
+
|
80
|
+
case response.code
|
81
|
+
when "200"
|
82
|
+
true
|
83
|
+
when "404"
|
84
|
+
false
|
85
|
+
else
|
86
|
+
Exception.push_http_response(response)
|
87
|
+
raise "Failed to determine job existance"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.run(server_url, name, config)
|
92
|
+
uri = server_url
|
93
|
+
user = uri.user
|
94
|
+
pass = uri.password
|
95
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
96
|
+
|
97
|
+
post = Net::HTTP::Post.new("/createItem?name=#{name}")
|
98
|
+
post.basic_auth(user, pass) if user && pass
|
99
|
+
post["Content-Type"] = "application/xml"
|
100
|
+
post.body = config
|
101
|
+
|
102
|
+
response = http.request(post)
|
103
|
+
|
104
|
+
unless response.code == "200"
|
105
|
+
Exception.push_http_response(response)
|
106
|
+
raise Error, "Failed to create job"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|