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,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
|