build-buddy 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0b2d3eab19b59adf57beb5489d827f289c871f8e
4
+ data.tar.gz: b6ea074f9c8e13b36247c839715e549bc25428bc
5
+ SHA512:
6
+ metadata.gz: 66842f29b1f61fd65b290d88e346fa364dcdce66585100f77370c5216ed673e3b2c4a8831c800084eab887f7ffc122549eded0d780b6eacdfe72b640c02a37fb
7
+ data.tar.gz: 20063e74483fc136eab52fbbb6a772491a1a6b6fd7f600a0d02b4efa22ecd3d9d4f1e255d627d897f9cf1ef1148c75388c2117449328fba45b2a3bd6b62aecbd
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'celluloid/current'
6
+ require 'celluloid/supervision'
7
+ require 'celluloid/supervision/container'
8
+ require 'methadone'
9
+ require 'build_buddy'
10
+
11
+ class Tool
12
+ include Methadone::Main
13
+ include Methadone::CLILogging
14
+
15
+ main do |config_name|
16
+ config_file_name = config_name
17
+
18
+ if File.extname(config_file_name) != '.bbconfig'
19
+ config_file_name += '.bbconfig'
20
+ end
21
+
22
+ load config_file_name
23
+
24
+ build_log_dir = BuildBuddy::Config.build_log_dir
25
+
26
+ unless Dir.exist?(build_log_dir)
27
+ Dir.mkdir(build_log_dir)
28
+ end
29
+
30
+ Slack.configure do |config|
31
+ config.token = BuildBuddy::Config.slack_api_token
32
+ end
33
+
34
+ Celluloid.logger = Reel::Logger.logger
35
+
36
+ BuildBuddy::Builder.supervise as: :builder
37
+ BuildBuddy::Server.supervise as: :server
38
+
39
+ sleep
40
+ end
41
+
42
+ version BuildBuddy::VERSION
43
+ description 'Build Buddy'
44
+ arg :config_name, :required
45
+
46
+ go!
47
+ end
@@ -0,0 +1,9 @@
1
+ require 'build_buddy/config'
2
+ require 'build_buddy/server'
3
+ require 'build_buddy/builder'
4
+ require 'build_buddy/watcher'
5
+
6
+ module BuildBuddy
7
+ VERSION = "1.0.6"
8
+ end
9
+
@@ -0,0 +1,80 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'celluloid'
4
+ require 'ostruct'
5
+ require_relative './watcher.rb'
6
+ require_relative './config.rb'
7
+
8
+ module BuildBuddy
9
+ class Builder
10
+ include Celluloid
11
+ include Celluloid::Internals::Logger
12
+
13
+ # TODO: Respond to request to kill the build.
14
+ # TODO: Kill the build pid after a certain amount of time has elapsed and report.
15
+
16
+ def initialize
17
+ @pid = nil
18
+ @watcher = nil
19
+ end
20
+
21
+ def start_build(build_data)
22
+ @build_data = build_data
23
+ repo_parts = build_data.repo_full_name.split('/')
24
+ command = "bash "
25
+ env = {
26
+ "GIT_REPO_OWNER" => repo_parts[0],
27
+ "GIT_REPO_NAME" => repo_parts[1],
28
+ "RBENV_DIR" => nil,
29
+ "RBENV_VERSION" => nil,
30
+ "PATH" => ENV['PATH'].split(':').select { |v| !v.match(/\.rbenv\/versions/) }.join(':')
31
+ }
32
+
33
+ case build_data.build_type
34
+ when :pull_request
35
+ env["GIT_PULL_REQUEST"] = build_data.pull_request.to_s
36
+ command += Config.pull_request_build_script
37
+ when :master
38
+ command += Config.master_build_script
39
+ when :release
40
+ env["GIT_BRANCH"] = build_data.build_version
41
+ command += Config.release_build_script
42
+ else
43
+ raise "Unknown build type"
44
+ end
45
+
46
+ build_log_filename = File.join(Config.build_log_dir, "build_#{build_data.build_type.to_s}_#{Time.now.utc.strftime('%Y%m%d%H%M%S')}.log")
47
+ build_data.build_log_filename = build_log_filename
48
+
49
+ command += " >#{build_log_filename} 2>&1"
50
+
51
+ Bundler.with_clean_env do
52
+ @pid = Process.spawn(env, command)
53
+ end
54
+ info "Running '#{command}' (process #{@pid})"
55
+
56
+ if @watcher
57
+ @watcher.terminate
58
+ end
59
+
60
+ @watcher = Watcher.new(@pid)
61
+ @watcher.async.watch_pid
62
+ end
63
+
64
+ def process_done(status)
65
+ @build_data.termination_type = (status.signaled? ? :killed : :exited)
66
+ @build_data.exit_code = (status.exited? ? status.exitstatus : -1)
67
+ info "Process #{status.pid} #{@build_data.termination_type == :killed ? 'was terminated' : "exited (#{@build_data.exit_code})"}"
68
+ Celluloid::Actor[:server].async.on_build_completed(@build_data)
69
+ @watcher.terminate
70
+ @watcher = nil
71
+ end
72
+
73
+ def stop_build
74
+ if @pid
75
+ info "Killing pid #{@pid}"
76
+ Process.kill(:SIGABRT, @pid)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,28 @@
1
+ module BuildBuddy
2
+ module Config
3
+ extend self
4
+
5
+ attr_accessor :github_webhook_secret_token
6
+ attr_accessor :github_webhook_repo_full_name
7
+ attr_accessor :github_api_token
8
+ attr_accessor :slack_api_token
9
+ attr_accessor :slack_build_channel
10
+ attr_accessor :slack_builders
11
+ attr_accessor :xcode_workspace
12
+ attr_accessor :xcode_test_scheme
13
+ attr_accessor :build_log_dir
14
+ attr_accessor :pull_request_build_script
15
+ attr_accessor :master_build_script
16
+ attr_accessor :release_build_script
17
+ end
18
+
19
+ class << self
20
+ def configure
21
+ block_given? ? yield(Config) : Config
22
+ end
23
+
24
+ def config
25
+ Config
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,288 @@
1
+ require 'rubygems'
2
+ require 'celluloid/current'
3
+ require 'reel'
4
+ require 'slack-ruby-client'
5
+ require 'json'
6
+ require 'ostruct'
7
+ require 'octokit'
8
+ require 'thread'
9
+ require 'timers'
10
+ require 'rack'
11
+ require_relative './builder.rb'
12
+
13
+ module BuildBuddy
14
+ class Server < Reel::Server::HTTP
15
+ include Celluloid::Internals::Logger
16
+
17
+ def initialize(host = "127.0.0.1", port = 4567)
18
+ super(host, port, &method(:on_connection))
19
+ @gh_client ||= Octokit::Client.new(:access_token => Config.github_api_token)
20
+ @rt_client = Slack::RealTime::Client.new
21
+ @rt_client.on :hello do
22
+ self.on_slack_hello()
23
+ end
24
+ @rt_client.on :message do |data|
25
+ self.on_slack_data(data)
26
+ end
27
+ @rt_client.on :error do |error|
28
+ self.on_slack_error(error)
29
+ end
30
+ @rt_client.start_async
31
+ @active_build = nil
32
+ @build_queue = Queue.new
33
+ @done_queue = Queue.new
34
+ @notify_slack_channel = nil
35
+ @reverse_user_map = nil
36
+ end
37
+
38
+ def on_slack_error(error)
39
+ sub_error = error['error']
40
+ error "Whoops! Slack error #{sub_error['code']} - #{sub_error['msg']}}"
41
+ end
42
+
43
+ def on_slack_hello
44
+ user_id = @rt_client.self['id']
45
+ user_map = @rt_client.users.map {|user| [user['name'], user['id']]}.to_h
46
+ @reverse_user_map = user_map.invert
47
+ info "Connected to Slack as user id #{user_id} (@#{@reverse_user_map[user_id]})"
48
+
49
+ channel_map = @rt_client.channels.map {|channel| [channel['name'], channel['id']]}.to_h
50
+ group_map = @rt_client.groups.map {|group| [group['name'], group['id']]}.to_h
51
+ channel = Config.slack_build_channel
52
+ is_channel = (channel[0] == '#')
53
+
54
+ @notify_slack_channel = (is_channel ? channel_map[channel[1..-1]] : group_map[channel])
55
+ if @notify_slack_channel.nil?
56
+ error "Unable to identify the slack channel #{channel}"
57
+ else
58
+ info "Slack notification channel is #{@notify_slack_channel} (#{channel})"
59
+ end
60
+ end
61
+
62
+ def on_slack_data(data)
63
+ message = data['text']
64
+
65
+ # If no message, then there's nothing to do
66
+ if message.nil?
67
+ return
68
+ end
69
+
70
+ sending_user_id = data['user']
71
+ sending_user_name = @reverse_user_map[sending_user_id]
72
+
73
+ # Don't respond if _we_ sent the message!
74
+ if sending_user_id == @rt_client.self['id']
75
+ return
76
+ end
77
+
78
+ sender_is_a_builder = (Config.slack_builders.nil? ? true : Config.slack_builders.include?('@' + sending_user_name))
79
+
80
+ c = data['channel'][0]
81
+ in_channel = (c == 'C' || c == 'G')
82
+
83
+ # Don't respond if the message is to a channel and our name is not in the message
84
+ if in_channel and !message.match(@rt_client.self['id'])
85
+ return
86
+ end
87
+
88
+ case message
89
+ when /build/i
90
+ unless sender_is_a_builder
91
+ if in_channel
92
+ response = "I'm sorry @#{sending_user_name} you are not on my list of allowed builders."
93
+ else
94
+ response = "I'm sorry but you are not on my list of allowed builders."
95
+ end
96
+ else
97
+ case message
98
+ when /master/i
99
+ response = "OK, I've queued a build of the `master` branch."
100
+ queue_a_build(OpenStruct.new(
101
+ :build_type => :master,
102
+ :repo_full_name => Config.github_webhook_repo_full_name))
103
+ when /(?<version>v\d+\.\d+)/
104
+ version = $~[:version]
105
+ response = "OK, I've queued a build of `#{version}` branch."
106
+ queue_a_build(OpenStruct.new(
107
+ :build_type => :release,
108
+ :build_version => version,
109
+ :repo_full_name => Config.github_webhook_repo_full_name))
110
+ when /stop/i
111
+ build_data = @active_build
112
+ if build_data.nil?
113
+ response = "There is no build running to stop"
114
+ else
115
+ # TODO: We need some more checks here to avoid accidental stoppage
116
+ response = "OK, I'm trying to *stop* the currently running build..."
117
+ Celluloid::Actor[:builder].async.stop_build
118
+ end
119
+ else
120
+ response = "Sorry#{in_channel ? " <@#{data['user']}>" : ""}, I'm not sure if you want do an internal *master*, external *M.m* build, or maybe *stop* any running build?"
121
+ end
122
+ end
123
+ when /status/i
124
+ build_data = @active_build
125
+ queue_length = @build_queue.length
126
+ if build_data == nil
127
+ response = "There is currently no build running"
128
+ if queue_length == 0
129
+ response += " and no builds in the queue."
130
+ else
131
+ response += " and #{queue_length} in the queue."
132
+ end
133
+ else
134
+ case build_data.build_type
135
+ when :pull_request
136
+ response = "There is a pull request build in progress for https://github.com/#{build_data.repo_full_name}/pull/#{build_data.pull_request}."
137
+ when :master
138
+ response = "There is a build of the `master` branch of https://github.com/#{build_data.repo_full_name} in progress."
139
+ when :release
140
+ response = "There is a build of the `#{build_data.build_version}` branch of https://github.com/#{build_data.repo_full_name} in progress."
141
+ end
142
+ if queue_length == 1
143
+ response += " There is one build in the queue."
144
+ elsif queue_length > 1
145
+ response += " There are #{queue_length} builds in the queue."
146
+ end
147
+ end
148
+ when /help/i, /what can/i
149
+ # TODO: The repository should be a link to GitHub
150
+ response = %Q(Hello#{in_channel ? " <@#{data['user']}>" : ""}, I'm the *@#{@rt_client.self['name']}* build bot! I look after 3 types of build: pull request, master and release.
151
+
152
+ A pull request *build* happens when you make a pull request to the *#{Config.github_webhook_repo_full_name}* GitHub repository. I can stop those builds if you ask me too through Slack, but you have to start them with a pull request.
153
+
154
+ I can run builds of the *master* branch when you ask me, as well as doing builds of a release branch, e.g. *v1.0*, *v2.3*, etc..
155
+
156
+ You can also ask me about the *status* of builds and I'll tell you if anything is currently happening.
157
+
158
+ I am configured to let the *\##{Config.slack_build_channel}* channel know if master or release builds fail. Note the words I have highlighted in bold. These are the keywords that I'll look for to understand what you are asking me.
159
+ )
160
+ else
161
+ response = "Sorry#{in_channel ? " <@#{data['user']}>" : ""}, I'm not sure how to respond."
162
+ end
163
+ @rt_client.message channel: data['channel'], text: response
164
+ info "Slack message '#{message}' from #{data['channel']} handled"
165
+ end
166
+
167
+ def on_connection(connection)
168
+ connection.each_request do |request|
169
+ case request.method
170
+ when 'POST'
171
+ case request.path
172
+ when '/webhook'
173
+ case request.headers["X-GitHub-Event"]
174
+ when 'pull_request'
175
+ payload_text = request.body.to_s
176
+ # TODO: Also need to validate that it's the github_webhook_repo_full_name
177
+ if !verify_signature(payload_text, request.headers["X-Hub-Signature"])
178
+ request.respond 500, "Signatures didn't match!"
179
+ else
180
+ payload = JSON.parse(payload_text)
181
+ pull_request = payload['pull_request']
182
+ build_data = OpenStruct.new(
183
+ :build_type => :pull_request,
184
+ :pull_request => pull_request['number'],
185
+ :repo_sha => pull_request['head']['sha'],
186
+ :repo_full_name => pull_request['base']['repo']['full_name'])
187
+ info "Got pull request #{build_data[:pull_request]} from GitHub"
188
+ queue_a_build(build_data)
189
+ request.respond 200
190
+ end
191
+ when 'ping'
192
+ request.respond 200, "Running"
193
+ else
194
+ request.respond 404, "Path not found"
195
+ end
196
+ end
197
+ else
198
+ request.respond 404, "Method not supported"
199
+ end
200
+ end
201
+ end
202
+
203
+ def queue_a_build(build_data)
204
+ @build_queue.push(build_data)
205
+
206
+ case build_data.build_type
207
+ when :pull_request
208
+ @gh_client.create_status(
209
+ build_data.repo_full_name, build_data.repo_sha, 'pending',
210
+ { :description => "This build is in the queue" })
211
+ info "Pull request build queued"
212
+ when :master
213
+ info "Internal build queued"
214
+ when :release
215
+ info "External build queued"
216
+ end
217
+
218
+ if @build_timer.nil?
219
+ @build_timer = every(5) { on_build_interval }
220
+ info "Build timer started"
221
+ end
222
+ end
223
+
224
+ def on_build_interval
225
+ if @active_build.nil?
226
+ if @build_queue.length > 0
227
+ build_data = @build_queue.pop()
228
+ @active_build = build_data
229
+ # TODO: Add timing information into the build_data
230
+ if build_data.build_type == :pull_request
231
+ @gh_client.create_status(
232
+ build_data.repo_full_name, build_data.repo_sha, 'pending',
233
+ { :description => "This build has started" })
234
+ end
235
+ Celluloid::Actor[:builder].async.start_build(build_data)
236
+ elsif @done_queue.length > 0
237
+ # TODO: Should pop everything in the done queue
238
+ build_data = @done_queue.pop
239
+ term_msg = (build_data.termination_type == :killed ? "was stopped" : "completed")
240
+ if build_data.termination_type == :exited
241
+ if build_data.exit_code != 0
242
+ term_msg += " with errors (exit code #{build_data.exit_code}). See log file `#{build_data.build_log_filename}` for more details."
243
+ else
244
+ term_msg += " successfully"
245
+ end
246
+ end
247
+ if build_data.build_type == :pull_request
248
+ description = "The buddy build #{term_msg}"
249
+ @gh_client.create_status(
250
+ build_data.repo_full_name, build_data.repo_sha,
251
+ build_data.termination_type == :killed ? 'failure' : build_data.exit_code != 0 ? 'error' : 'success',
252
+ { :description => description })
253
+ info "Pull request build #{term_msg}"
254
+ else
255
+ case build_data.build_type
256
+ when :master
257
+ message = "A build of the `master` branch #{term_msg}."
258
+ info "Internal build #{term_msg}"
259
+ when :release
260
+ message = "A build of the `#{build_data.build_version}` branch #{term_msg}."
261
+ info "External build #{term_msg}"
262
+ end
263
+ unless @notify_slack_channel.nil?
264
+ @rt_client.message(channel: @notify_slack_channel, text: message)
265
+ end
266
+ end
267
+ else
268
+ @build_timer.cancel
269
+ @build_timer = nil
270
+ info "Build timer stopped"
271
+ end
272
+ else
273
+ # TODO: Make sure that the build has not run too long and kill if necessary
274
+ end
275
+ end
276
+
277
+ def on_build_completed(build_data)
278
+ @active_build = nil
279
+ @done_queue.push(build_data)
280
+ end
281
+
282
+ def verify_signature(payload_body, gh_signature)
283
+ signature = 'sha1=' + OpenSSL::HMAC.hexdigest(
284
+ OpenSSL::Digest.new('sha1'), Config.github_webhook_secret_token, payload_body)
285
+ Rack::Utils.secure_compare(signature, gh_signature)
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'celluloid'
3
+
4
+ module BuildBuddy
5
+ class Watcher
6
+ include Celluloid
7
+
8
+ def initialize(pid)
9
+ @pid = pid
10
+ end
11
+
12
+ def watch_pid
13
+ Process.waitpid2(@pid)
14
+ Celluloid::Actor[:builder].async.process_done($?)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,204 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: build-buddy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.6
5
+ platform: ruby
6
+ authors:
7
+ - John Lyon-smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: timers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: celluloid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.17.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.17.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: celluloid-supervision
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.20.5
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.20.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: methadone
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.9'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: slack-ruby-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.5.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.5.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: json
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.8'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: http
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: reel
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.6.0.pre3
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 0.6.0.pre3
125
+ - !ruby/object:Gem::Dependency
126
+ name: octokit
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '4.2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '4.2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rack
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.6'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.6'
153
+ - !ruby/object:Gem::Dependency
154
+ name: code-tools
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '5.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '5.0'
167
+ description: A build buddy bot with GitHub and Slack integration.
168
+ email: john@jamoki.com
169
+ executables:
170
+ - build-buddy
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - bin/build-buddy
175
+ - lib/build_buddy.rb
176
+ - lib/build_buddy/builder.rb
177
+ - lib/build_buddy/config.rb
178
+ - lib/build_buddy/server.rb
179
+ - lib/build_buddy/watcher.rb
180
+ homepage: http://rubygems.org/gems/build-buddy
181
+ licenses:
182
+ - MIT
183
+ metadata: {}
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - "~>"
191
+ - !ruby/object:Gem::Version
192
+ version: '2.2'
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubyforge_project:
200
+ rubygems_version: 2.4.5
201
+ signing_key:
202
+ specification_version: 4
203
+ summary: An automated build buddy
204
+ test_files: []