engineyard-serverside 1.4.3.nodestack → 1.4.7.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/core-ext/README.md +3 -0
- data/lib/core-ext/string.rb +9 -0
- data/lib/engineyard-serverside.rb +3 -1
- data/lib/engineyard-serverside/cli.rb +3 -2
- data/lib/engineyard-serverside/configuration.rb +1 -1
- data/lib/engineyard-serverside/deploy.rb +117 -23
- data/lib/engineyard-serverside/lockfile_parser.rb +3 -3
- data/lib/engineyard-serverside/logged_output.rb +0 -5
- data/lib/engineyard-serverside/task.rb +1 -0
- data/lib/engineyard-serverside/version.rb +1 -1
- data/lib/vendor/ruby_1.8.6_openssl.patch +7 -0
- data/spec/custom_deploy_spec.rb +14 -7
- data/spec/fixtures/gemfiles/activerecord_jdbcmysql/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_jdbcmysql/Gemfile.lock +29 -0
- data/spec/fixtures/gemfiles/activerecord_jdbcpostgresql/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_jdbcpostgresql/Gemfile.lock +29 -0
- data/spec/fixtures/gemfiles/activerecord_mysql/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_mysql/Gemfile.lock +25 -0
- data/spec/fixtures/gemfiles/activerecord_mysql2/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_mysql2/Gemfile.lock +25 -0
- data/spec/fixtures/gemfiles/activerecord_pg/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_pg/Gemfile.lock +25 -0
- data/spec/fixtures/gemfiles/activerecord_sqlite3/Gemfile +5 -0
- data/spec/fixtures/gemfiles/activerecord_sqlite3/Gemfile.lock +25 -0
- data/spec/fixtures/gemfiles/diy_database_yml/Gemfile +5 -0
- data/spec/fixtures/gemfiles/diy_database_yml/Gemfile.lock +25 -0
- data/spec/fixtures/gemfiles/diy_database_yml/config/database.yml +7 -0
- data/spec/generate_configs_spec.rb +228 -0
- data/spec/lib/full_test_deploy.rb +86 -0
- data/spec/real_deploy_spec.rb +42 -121
- data/spec/spec_helper.rb +62 -1
- metadata +75 -9
- data/spec/fixtures/gitrepo/bar +0 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
# methods for String that aren't available in ruby 1.8.6 (used by ey_resin)
|
2
|
+
# versions here are just workarounds
|
3
|
+
# FIXME remove this module when ey_resin (and .rvmrc) updated to ruby 1.8.7 or 1.9.2
|
4
|
+
module ModernString
|
5
|
+
def start_with?(prefix)
|
6
|
+
self.index(prefix) == 0
|
7
|
+
end
|
8
|
+
end
|
9
|
+
String.send(:include, ModernString)
|
@@ -4,6 +4,8 @@ $LOAD_PATH.unshift File.expand_path('vendor/escape/lib', File.dirname(__FILE__))
|
|
4
4
|
$LOAD_PATH.unshift File.expand_path('vendor/json_pure/lib', File.dirname(__FILE__))
|
5
5
|
$LOAD_PATH.unshift File.expand_path('vendor/dataflow', File.dirname(__FILE__))
|
6
6
|
|
7
|
+
require 'core-ext/string' if RUBY_VERSION == '1.8.6'
|
8
|
+
|
7
9
|
require 'escape'
|
8
10
|
require 'json'
|
9
11
|
require 'dataflow'
|
@@ -34,7 +36,7 @@ module EY
|
|
34
36
|
{}.to_json
|
35
37
|
end
|
36
38
|
end
|
37
|
-
|
39
|
+
|
38
40
|
RemoteFailure = Class.new StandardError
|
39
41
|
|
40
42
|
private
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'thor'
|
2
2
|
require 'pathname'
|
3
|
+
require 'tmpdir'
|
3
4
|
|
4
5
|
module EY
|
5
6
|
module Serverside
|
@@ -214,9 +215,9 @@ module EY
|
|
214
215
|
def propagate
|
215
216
|
config = EY::Serverside::Deploy::Configuration.new
|
216
217
|
gem_filename = "engineyard-serverside-#{EY::Serverside::VERSION}.gem"
|
217
|
-
local_gem_file = File.join(Gem.dir, 'cache', gem_filename)
|
218
|
+
local_gem_file = File.join(::Gem.dir, 'cache', gem_filename)
|
218
219
|
remote_gem_file = File.join(Dir.tmpdir, gem_filename)
|
219
|
-
gem_binary = File.join(Gem.default_bindir, 'gem')
|
220
|
+
gem_binary = File.join(::Gem.default_bindir, 'gem')
|
220
221
|
|
221
222
|
barrier(*(EY::Serverside::Server.all.find_all do |server|
|
222
223
|
!server.local? # of course this machine has it
|
@@ -119,7 +119,7 @@ module EY
|
|
119
119
|
end
|
120
120
|
|
121
121
|
def framework_envs
|
122
|
-
"RAILS_ENV=#{environment} RACK_ENV=#{environment}
|
122
|
+
"RAILS_ENV=#{environment} RACK_ENV=#{environment} MERB_ENV=#{environment}"
|
123
123
|
end
|
124
124
|
|
125
125
|
def current_path
|
@@ -27,6 +27,7 @@ module EY
|
|
27
27
|
create_revision_file
|
28
28
|
run_with_callbacks(:bundle)
|
29
29
|
symlink_configs
|
30
|
+
generate_configs
|
30
31
|
conditionally_enable_maintenance_page
|
31
32
|
run_with_callbacks(:migrate)
|
32
33
|
callback(:before_symlink)
|
@@ -136,8 +137,9 @@ module EY
|
|
136
137
|
bundler_installer = if File.exist?(lockfile)
|
137
138
|
get_bundler_installer(lockfile)
|
138
139
|
else
|
139
|
-
|
140
|
-
|
140
|
+
missing_lock_version = EY::Serverside::LockfileParser::Parse10::DEFAULT
|
141
|
+
warn_about_missing_lockfile missing_lock_version
|
142
|
+
bundler_10_installer missing_lock_version
|
141
143
|
end
|
142
144
|
|
143
145
|
sudo "#{$0} _#{EY::Serverside::VERSION}_ install_bundler #{bundler_installer.version}"
|
@@ -164,15 +166,6 @@ module EY
|
|
164
166
|
|
165
167
|
run "mkdir -p #{bundled_gems_path} && ruby -v > #{ruby_version_file} && uname -m > #{system_version_file}"
|
166
168
|
end
|
167
|
-
if File.exist?("#{c.release_path}/package.json")
|
168
|
-
unless `which npm` =~ /npm/
|
169
|
-
error "~> package.json detected, BUT npm not installed"
|
170
|
-
else
|
171
|
-
info "~> package.json detected, installing npm packages"
|
172
|
-
|
173
|
-
run "cd #{c.release_path} && npm install"
|
174
|
-
end
|
175
|
-
end
|
176
169
|
end
|
177
170
|
|
178
171
|
# task
|
@@ -227,6 +220,7 @@ module EY
|
|
227
220
|
run create_revision_file_command
|
228
221
|
end
|
229
222
|
|
223
|
+
# symlink to shared path; may be overridden by #generate_configs
|
230
224
|
def symlink_configs(release_to_link=c.release_path)
|
231
225
|
info "~> Symlinking configs"
|
232
226
|
[ "chmod -R g+w #{release_to_link}",
|
@@ -237,8 +231,8 @@ module EY
|
|
237
231
|
"mkdir -p #{release_to_link}/config",
|
238
232
|
"ln -nfs #{c.shared_path}/system #{release_to_link}/public/system",
|
239
233
|
"ln -nfs #{c.shared_path}/pids #{release_to_link}/tmp/pids",
|
240
|
-
"find #{c.shared_path}/config -type f -exec ln -s {} #{release_to_link}/config \\;",
|
241
|
-
|
234
|
+
"find #{c.shared_path}/config ! -name 'database.yml*' -type f -exec ln -s {} #{release_to_link}/config \\;",
|
235
|
+
# database.yml generated or symlink created in #generate_database_yml
|
242
236
|
"ln -nfs #{c.shared_path}/config/mongrel_cluster.yml #{release_to_link}/config/mongrel_cluster.yml",
|
243
237
|
].each do |cmd|
|
244
238
|
run cmd
|
@@ -248,6 +242,84 @@ module EY
|
|
248
242
|
run "if [ -f \"#{c.shared_path}/config/newrelic.yml\" ]; then ln -nfs #{c.shared_path}/config/newrelic.yml #{release_to_link}/config/newrelic.yml; fi"
|
249
243
|
end
|
250
244
|
|
245
|
+
def generate_configs(release_to_link=c.release_path)
|
246
|
+
generate_database_yml(release_to_link)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Do nothing if there is no Gemfile.lock to determine what ORM gems are being used
|
250
|
+
# (falls back to using the symlinked shared/database.yml file)
|
251
|
+
def generate_database_yml(release_to_link)
|
252
|
+
return if keep_database_yml?(release_to_link)
|
253
|
+
if config["db_adapter"] || File.exist?("#{c.release_path}/Gemfile.lock")
|
254
|
+
info "~> Generating database.yml from Gemfile.lock"
|
255
|
+
database_yml = "#{release_to_link}/config/database.yml"
|
256
|
+
node = EY::Serverside.node
|
257
|
+
node_app = node["engineyard"]["environment"]["apps"].find { |app| app['name'] == c['app'] }
|
258
|
+
abort("Invalid application name for target environment: #{c['app']}") unless node_app
|
259
|
+
|
260
|
+
db_stack_name = node[:engineyard][:environment][:db_stack_name]
|
261
|
+
instances = node[:engineyard][:environment][:instances]
|
262
|
+
db_master = {"public_hostname" => "localhost"} # you know, just in case
|
263
|
+
db_slaves = []
|
264
|
+
instances.each do |i|
|
265
|
+
case i['role']
|
266
|
+
when 'db_master', 'solo'
|
267
|
+
db_master = i
|
268
|
+
when 'db_slave'
|
269
|
+
db_slaves << i
|
270
|
+
end
|
271
|
+
end
|
272
|
+
db_host = db_master["public_hostname"]
|
273
|
+
db_slaves_hosts = db_slaves.map {|slave| slave["public_hostname"]}
|
274
|
+
|
275
|
+
if config["db_adapter"]
|
276
|
+
adapter = config["db_adapter"]
|
277
|
+
elsif bundler_gems_include?("mysql2")
|
278
|
+
adapter = "mysql2"
|
279
|
+
elsif bundler_gems_include?("mysql")
|
280
|
+
adapter = "mysql"
|
281
|
+
elsif bundler_gems_include?("pg")
|
282
|
+
adapter = "postgresql"
|
283
|
+
elsif bundler_gems_include?("jdbc-mysql")
|
284
|
+
adapter = "mysql"
|
285
|
+
elsif bundler_gems_include?("jdbc-postgres")
|
286
|
+
adapter = "postgresql"
|
287
|
+
elsif db_stack_name && db_stack_name =~ /postgres/
|
288
|
+
adapter = "postgresql"
|
289
|
+
else
|
290
|
+
adapter = "mysql"
|
291
|
+
end
|
292
|
+
|
293
|
+
File.open(database_yml, "w") do |file|
|
294
|
+
contents = <<-EOS.gsub(/^\s{12}/, '')
|
295
|
+
#{node[:engineyard][:environment][:framework_env]}:
|
296
|
+
adapter: #{adapter}
|
297
|
+
database: #{node_app[:database_name]}
|
298
|
+
username: #{node[:engineyard][:environment][:ssh_username]}
|
299
|
+
password: #{node[:engineyard][:environment][:ssh_password]}
|
300
|
+
host: #{db_host}
|
301
|
+
reconnect: true
|
302
|
+
EOS
|
303
|
+
db_slaves_hosts.each_with_index do |host, n|
|
304
|
+
slave_name = n.zero? ? "slave" : "slave_#{n}"
|
305
|
+
contents << <<-EOS.gsub(/^\s{14}/, '')
|
306
|
+
#{slave_name}:
|
307
|
+
adapter: #{adapter}
|
308
|
+
database: #{node_app[:database_name]}
|
309
|
+
username: #{node[:engineyard][:environment][:ssh_username]}
|
310
|
+
password: #{node[:engineyard][:environment][:ssh_password]}
|
311
|
+
host: #{host}
|
312
|
+
reconnect: true
|
313
|
+
EOS
|
314
|
+
end
|
315
|
+
file << contents
|
316
|
+
end
|
317
|
+
else
|
318
|
+
info "~> Symlinking database.yml config"
|
319
|
+
run "ln -nfs #{c.shared_path}/config/database.yml #{release_to_link}/config/database.yml"
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
251
323
|
# task
|
252
324
|
def symlink(release_to_link=c.release_path)
|
253
325
|
info "~> Symlinking code"
|
@@ -319,7 +391,7 @@ module EY
|
|
319
391
|
raise
|
320
392
|
end
|
321
393
|
|
322
|
-
def warn_about_missing_lockfile
|
394
|
+
def warn_about_missing_lockfile(missing_lock_version)
|
323
395
|
info "!>"
|
324
396
|
info "!> WARNING: Gemfile.lock is missing!"
|
325
397
|
info "!> You can get different gems in production than what you tested with."
|
@@ -329,30 +401,52 @@ module EY
|
|
329
401
|
info "!> Fix this by running \"git add Gemfile.lock; git commit\" and deploying again."
|
330
402
|
info "!> If you don't have a Gemfile.lock, run \"bundle lock\" to create one."
|
331
403
|
info "!>"
|
332
|
-
info "!> This deployment will use bundler #{
|
404
|
+
info "!> This deployment will use bundler #{missing_lock_version} to run 'bundle install'."
|
333
405
|
info "!>"
|
334
406
|
end
|
335
407
|
|
336
|
-
def
|
408
|
+
def keep_database_yml?(release_to_link=c.release_path)
|
409
|
+
File.exists?(File.join(release_to_link, "config", "keep.database.yml"))
|
410
|
+
end
|
411
|
+
|
412
|
+
# returns true if "bundle list" includes all gems requested
|
413
|
+
def bundler_gems_include?(*gems)
|
414
|
+
lockfile = File.join(c.release_path, "Gemfile.lock")
|
415
|
+
@lockfile_contents ||= File.read(lockfile)
|
416
|
+
|
417
|
+
# Parsing Gemfile.lock which looks like
|
418
|
+
# GEM
|
419
|
+
# remote: http://rubygems.org/
|
420
|
+
# specs:
|
421
|
+
# activemodel (3.0.10)
|
422
|
+
# activesupport (= 3.0.10)
|
423
|
+
# builder (~> 2.1.2)
|
424
|
+
# i18n (~> 0.5.0)
|
425
|
+
#
|
426
|
+
gems.inject(true) {|found_all, gem| found_all && (@lockfile_contents =~ / #{gem} \(/)}
|
427
|
+
end
|
428
|
+
|
429
|
+
def get_bundler_installer(lockfile, options = '')
|
337
430
|
parser = LockfileParser.new(File.read(lockfile))
|
338
431
|
case parser.lockfile_version
|
339
432
|
when :bundler09
|
340
|
-
bundler_09_installer(parser.bundler_version)
|
433
|
+
bundler_09_installer(parser.bundler_version, options)
|
341
434
|
when :bundler10
|
342
|
-
bundler_10_installer(parser.bundler_version)
|
435
|
+
bundler_10_installer(parser.bundler_version, options)
|
343
436
|
else
|
344
437
|
raise "Unknown lockfile version #{parser.lockfile_version}"
|
345
438
|
end
|
346
439
|
end
|
347
440
|
public :get_bundler_installer
|
348
441
|
|
349
|
-
def bundler_09_installer(version)
|
350
|
-
|
442
|
+
def bundler_09_installer(version, options = '')
|
443
|
+
default_options = '--without=development --without=test'
|
444
|
+
BundleInstaller.new(version, default_options + options)
|
351
445
|
end
|
352
446
|
|
353
|
-
def bundler_10_installer(version)
|
354
|
-
|
355
|
-
|
447
|
+
def bundler_10_installer(version, options = '')
|
448
|
+
default_options = "--deployment --path #{c.shared_path}/bundled_gems --binstubs #{c.binstubs_path} --without development test"
|
449
|
+
BundleInstaller.new(version, default_options + options)
|
356
450
|
end
|
357
451
|
end # DeployBase
|
358
452
|
|
@@ -79,11 +79,11 @@ module EY
|
|
79
79
|
when '='
|
80
80
|
bundler_version
|
81
81
|
when '>='
|
82
|
-
Gem::Version.new(bundler_version) > Gem::Version.new(DEFAULT) ? bundler_version : DEFAULT
|
82
|
+
::Gem::Version.new(bundler_version) > ::Gem::Version.new(DEFAULT) ? bundler_version : DEFAULT
|
83
83
|
when '~>'
|
84
|
-
bundler_gem_version = Gem::Version.new(bundler_version)
|
84
|
+
bundler_gem_version = ::Gem::Version.new(bundler_version)
|
85
85
|
recommendation = bundler_gem_version.spermy_recommendation.gsub(/~>\s*(.+)$/, '\1.')
|
86
|
-
DEFAULT.start_with?(recommendation) && Gem::Version.new(DEFAULT) > bundler_gem_version ? DEFAULT : bundler_version
|
86
|
+
DEFAULT.start_with?(recommendation) && ::Gem::Version.new(DEFAULT) > bundler_gem_version ? DEFAULT : bundler_version
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
@@ -54,6 +54,7 @@ module EY
|
|
54
54
|
need_later { server.run(Escape.shell_command(wrapper + [to_run])) }
|
55
55
|
end
|
56
56
|
barrier *results
|
57
|
+
|
57
58
|
# MRI's truthiness check is an internal C thing that does not call
|
58
59
|
# any methods... so Dataflow cannot proxy it & we must "x == true"
|
59
60
|
# Rubinius, wherefore art thou!?
|
@@ -0,0 +1,7 @@
|
|
1
|
+
diff --git a/ext/openssl/openssl_missing.h b/ext/openssl/openssl_missing.h
|
2
|
+
index 2ebf61a..8a0202a 100644
|
3
|
+
--- a/ext/openssl/openssl_missing.h
|
4
|
+
+++ b/ext/openssl/openssl_missing.h
|
5
|
+
@@ -123,2 +122,0 @@ int BN_mod_sub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, const BIGNUM *m, BN_
|
6
|
+
-int BN_rand_range(BIGNUM *r, BIGNUM *range);
|
7
|
+
-int BN_pseudo_rand_range(BIGNUM *r, BIGNUM *range);
|
data/spec/custom_deploy_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'fileutils'
|
2
3
|
|
3
4
|
describe "the EY::Serverside::Deploy API" do
|
4
5
|
it "calls tasks in the right order" do
|
@@ -29,9 +30,12 @@ describe "the EY::Serverside::Deploy API" do
|
|
29
30
|
def cleanup_old_releases() @call_order << 'cleanup_old_releases' end
|
30
31
|
def conditionally_enable_maintenance_page() @call_order << 'conditionally_enable_maintenance_page' end
|
31
32
|
def disable_maintenance_page() @call_order << 'disable_maintenance_page' end
|
33
|
+
def generate_database_yml(path) @call_order << 'generate_database_yml' end
|
32
34
|
end
|
33
35
|
|
34
|
-
|
36
|
+
setup_dna_json
|
37
|
+
|
38
|
+
td = TestDeploy.new(EY::Serverside::Deploy::Configuration.new('app' => 'myfirstapp'))
|
35
39
|
td.deploy
|
36
40
|
td.call_order.should == %w(
|
37
41
|
push_code
|
@@ -39,6 +43,7 @@ describe "the EY::Serverside::Deploy API" do
|
|
39
43
|
create_revision_file
|
40
44
|
bundle
|
41
45
|
symlink_configs
|
46
|
+
generate_database_yml
|
42
47
|
conditionally_enable_maintenance_page
|
43
48
|
migrate
|
44
49
|
symlink
|
@@ -53,15 +58,17 @@ describe "the EY::Serverside::Deploy API" do
|
|
53
58
|
end
|
54
59
|
|
55
60
|
before(:each) do
|
56
|
-
@tempdir =
|
57
|
-
@config
|
58
|
-
@deploy
|
61
|
+
@tempdir = File.join(Dir.tmpdir, "serverside-deploy-#{Time.now.to_i}-#{$$}")
|
62
|
+
@config = EY::Serverside::Deploy::Configuration.new('repository_cache' => @tempdir)
|
63
|
+
@deploy = TestQuietDeploy.new(@config)
|
64
|
+
end
|
65
|
+
|
66
|
+
after do
|
67
|
+
FileUtils.rm_rf(@tempdir)
|
59
68
|
end
|
60
69
|
|
61
70
|
def write_eydeploy(relative_path, contents = "def got_new_methods() 'from the file on disk' end")
|
62
|
-
FileUtils.mkdir_p(File.join(
|
63
|
-
@tempdir,
|
64
|
-
File.dirname(relative_path)))
|
71
|
+
FileUtils.mkdir_p(File.join(@tempdir, File.dirname(relative_path)))
|
65
72
|
|
66
73
|
File.open(File.join(@tempdir, relative_path), 'w') do |f|
|
67
74
|
f.write contents
|
@@ -0,0 +1,29 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.10)
|
5
|
+
activesupport (= 3.0.10)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.5.0)
|
8
|
+
activerecord (3.0.10)
|
9
|
+
activemodel (= 3.0.10)
|
10
|
+
activesupport (= 3.0.10)
|
11
|
+
arel (~> 2.0.10)
|
12
|
+
tzinfo (~> 0.3.23)
|
13
|
+
activerecord-jdbc-adapter (1.1.3)
|
14
|
+
activerecord-jdbcmysql-adapter (1.1.3)
|
15
|
+
activerecord-jdbc-adapter (= 1.1.3)
|
16
|
+
jdbc-mysql (~> 5.1.0)
|
17
|
+
activesupport (3.0.10)
|
18
|
+
arel (2.0.10)
|
19
|
+
builder (2.1.2)
|
20
|
+
i18n (0.5.0)
|
21
|
+
jdbc-mysql (5.1.13)
|
22
|
+
tzinfo (0.3.29)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
activerecord
|
29
|
+
activerecord-jdbcmysql-adapter
|
@@ -0,0 +1,29 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.10)
|
5
|
+
activesupport (= 3.0.10)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.5.0)
|
8
|
+
activerecord (3.0.10)
|
9
|
+
activemodel (= 3.0.10)
|
10
|
+
activesupport (= 3.0.10)
|
11
|
+
arel (~> 2.0.10)
|
12
|
+
tzinfo (~> 0.3.23)
|
13
|
+
activerecord-jdbc-adapter (1.1.3)
|
14
|
+
activerecord-jdbcpostgresql-adapter (1.1.3)
|
15
|
+
activerecord-jdbc-adapter (= 1.1.3)
|
16
|
+
jdbc-postgres (~> 9.0.0)
|
17
|
+
activesupport (3.0.10)
|
18
|
+
arel (2.0.10)
|
19
|
+
builder (2.1.2)
|
20
|
+
i18n (0.5.0)
|
21
|
+
jdbc-postgres (9.0.801)
|
22
|
+
tzinfo (0.3.29)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
activerecord
|
29
|
+
activerecord-jdbcpostgresql-adapter
|