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