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