janky 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGES +3 -0
  2. data/COPYING +22 -0
  3. data/Gemfile +2 -0
  4. data/README.md +211 -0
  5. data/Rakefile +19 -0
  6. data/config.ru +3 -0
  7. data/janky.gemspec +102 -0
  8. data/lib/janky.rb +224 -0
  9. data/lib/janky/app.rb +81 -0
  10. data/lib/janky/branch.rb +112 -0
  11. data/lib/janky/build.rb +223 -0
  12. data/lib/janky/build_request.rb +49 -0
  13. data/lib/janky/builder.rb +108 -0
  14. data/lib/janky/builder/client.rb +82 -0
  15. data/lib/janky/builder/http.rb +43 -0
  16. data/lib/janky/builder/mock.rb +45 -0
  17. data/lib/janky/builder/payload.rb +63 -0
  18. data/lib/janky/builder/receiver.rb +20 -0
  19. data/lib/janky/builder/runner.rb +47 -0
  20. data/lib/janky/campfire.rb +127 -0
  21. data/lib/janky/campfire/mock.rb +0 -0
  22. data/lib/janky/commit.rb +14 -0
  23. data/lib/janky/database/migrate/1312115512_init.rb +48 -0
  24. data/lib/janky/database/migrate/1312117285_non_unique_repo_uri.rb +10 -0
  25. data/lib/janky/database/migrate/1312198807_repo_enabled.rb +11 -0
  26. data/lib/janky/database/migrate/1313867551_add_build_output_column.rb +9 -0
  27. data/lib/janky/database/migrate/1313871652_add_commit_url_column.rb +9 -0
  28. data/lib/janky/database/migrate/1317384618_add_repo_hook_url.rb +9 -0
  29. data/lib/janky/database/migrate/1317384619_add_build_room_id.rb +9 -0
  30. data/lib/janky/database/migrate/1317384629_drop_default_room_id.rb +9 -0
  31. data/lib/janky/database/migrate/1317384649_github_team_id.rb +9 -0
  32. data/lib/janky/database/schema.rb +68 -0
  33. data/lib/janky/database/seed.dump.gz +0 -0
  34. data/lib/janky/exception.rb +62 -0
  35. data/lib/janky/github.rb +67 -0
  36. data/lib/janky/github/api.rb +69 -0
  37. data/lib/janky/github/commit.rb +27 -0
  38. data/lib/janky/github/mock.rb +47 -0
  39. data/lib/janky/github/payload.rb +34 -0
  40. data/lib/janky/github/payload_parser.rb +57 -0
  41. data/lib/janky/github/receiver.rb +69 -0
  42. data/lib/janky/helpers.rb +17 -0
  43. data/lib/janky/hubot.rb +117 -0
  44. data/lib/janky/job_creator.rb +111 -0
  45. data/lib/janky/notifier.rb +84 -0
  46. data/lib/janky/notifier/campfire.rb +21 -0
  47. data/lib/janky/notifier/mock.rb +55 -0
  48. data/lib/janky/notifier/multi.rb +22 -0
  49. data/lib/janky/public/css/base.css +204 -0
  50. data/lib/janky/public/images/building-bot.gif +0 -0
  51. data/lib/janky/public/images/disclosure-arrow.png +0 -0
  52. data/lib/janky/public/images/logo.png +0 -0
  53. data/lib/janky/public/images/robawt-status.gif +0 -0
  54. data/lib/janky/public/javascripts/application.js +3 -0
  55. data/lib/janky/public/javascripts/jquery.js +16 -0
  56. data/lib/janky/public/javascripts/jquery.relatize.js +111 -0
  57. data/lib/janky/repository.rb +174 -0
  58. data/lib/janky/tasks.rb +36 -0
  59. data/lib/janky/templates/console.mustache +4 -0
  60. data/lib/janky/templates/index.mustache +11 -0
  61. data/lib/janky/templates/layout.mustache +22 -0
  62. data/lib/janky/version.rb +3 -0
  63. data/lib/janky/views/console.rb +33 -0
  64. data/lib/janky/views/index.rb +35 -0
  65. data/lib/janky/views/layout.rb +19 -0
  66. data/test/default.xml.erb +0 -0
  67. data/test/janky_test.rb +271 -0
  68. data/test/test_helper.rb +107 -0
  69. 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
@@ -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