build-buddy 1.0.6

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.
@@ -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: []