monkey_butler 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +122 -0
- data/Guardfile +8 -0
- data/LICENSE +202 -0
- data/README.md +221 -0
- data/Rakefile +16 -0
- data/bin/mb +6 -0
- data/lib/monkey_butler.rb +1 -0
- data/lib/monkey_butler/actions.rb +38 -0
- data/lib/monkey_butler/cli.rb +354 -0
- data/lib/monkey_butler/databases/abstract_database.rb +49 -0
- data/lib/monkey_butler/databases/cassandra_database.rb +69 -0
- data/lib/monkey_butler/databases/sqlite_database.rb +105 -0
- data/lib/monkey_butler/migrations.rb +52 -0
- data/lib/monkey_butler/project.rb +90 -0
- data/lib/monkey_butler/targets/base.rb +85 -0
- data/lib/monkey_butler/targets/cassandra/cassandra_target.rb +72 -0
- data/lib/monkey_butler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
- data/lib/monkey_butler/targets/cassandra/migration.cql.erb +0 -0
- data/lib/monkey_butler/targets/cocoapods/cocoapods_target.rb +43 -0
- data/lib/monkey_butler/targets/cocoapods/podspec.erb +12 -0
- data/lib/monkey_butler/targets/maven/maven_target.rb +60 -0
- data/lib/monkey_butler/targets/maven/project/.gitignore +6 -0
- data/lib/monkey_butler/targets/maven/project/MonkeyButler.iml +19 -0
- data/lib/monkey_butler/targets/maven/project/build.gradle +54 -0
- data/lib/monkey_butler/targets/sqlite/create_monkey_butler_tables.sql.erb +15 -0
- data/lib/monkey_butler/targets/sqlite/migration.sql.erb +11 -0
- data/lib/monkey_butler/targets/sqlite/sqlite_target.rb +91 -0
- data/lib/monkey_butler/templates/Gemfile.erb +4 -0
- data/lib/monkey_butler/templates/gitignore.erb +1 -0
- data/lib/monkey_butler/util.rb +71 -0
- data/lib/monkey_butler/version.rb +3 -0
- data/logo.jpg +0 -0
- data/monkey_butler.gemspec +33 -0
- data/spec/cli_spec.rb +700 -0
- data/spec/databases/cassandra_database_spec.rb +241 -0
- data/spec/databases/sqlite_database_spec.rb +181 -0
- data/spec/migrations_spec.rb +4 -0
- data/spec/project_spec.rb +128 -0
- data/spec/sandbox/cassandra/.gitignore +2 -0
- data/spec/sandbox/cassandra/.monkey_butler.yml +7 -0
- data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
- data/spec/sandbox/cassandra/sandbox.cql +0 -0
- data/spec/sandbox/sqlite/.gitignore +2 -0
- data/spec/sandbox/sqlite/.monkey_butler.yml +7 -0
- data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
- data/spec/sandbox/sqlite/sandbox.sql +0 -0
- data/spec/spec_helper.rb +103 -0
- data/spec/targets/cassandra_target_spec.rb +191 -0
- data/spec/targets/cocoapods_target_spec.rb +197 -0
- data/spec/targets/maven_target_spec.rb +156 -0
- data/spec/targets/sqlite_target_spec.rb +103 -0
- data/spec/util_spec.rb +13 -0
- metadata +260 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
desc 'Builds and installs the monkey_butler Gem'
|
8
|
+
task :install do
|
9
|
+
lib = File.expand_path('../lib', __FILE__)
|
10
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
11
|
+
require 'monkey_butler/version'
|
12
|
+
|
13
|
+
system("gem build monkey_butler.gemspec && gem install monkey_butler-#{MonkeyButler::VERSION}.gem")
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => :spec
|
data/bin/mb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'monkey_butler/cli'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MonkeyButler
|
2
|
+
module Actions
|
3
|
+
# Run a command in git.
|
4
|
+
#
|
5
|
+
# git :init
|
6
|
+
# git add: "this.file that.rb"
|
7
|
+
# git add: "onefile.rb", rm: "badfile.cxx"
|
8
|
+
def git(commands={})
|
9
|
+
if commands.is_a?(Symbol)
|
10
|
+
run "git #{commands}"
|
11
|
+
else
|
12
|
+
commands.each do |cmd, options|
|
13
|
+
run "git #{cmd} #{options}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def git_add(*paths)
|
19
|
+
inside(destination_root) do
|
20
|
+
git add: paths.flatten.join(' ')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def truncate_database
|
25
|
+
say_status :truncate, database.to_s, :yellow
|
26
|
+
database.drop
|
27
|
+
end
|
28
|
+
|
29
|
+
def bundle
|
30
|
+
inside(destination_root) { run "bundle" }
|
31
|
+
end
|
32
|
+
|
33
|
+
def unique_tag_for_version(version)
|
34
|
+
return version if options['pretend']
|
35
|
+
MonkeyButler::Util.unique_tag_for_version(version)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require "open3"
|
3
|
+
require 'monkey_butler/project'
|
4
|
+
require 'monkey_butler/actions'
|
5
|
+
require 'monkey_butler/databases/abstract_database'
|
6
|
+
require 'monkey_butler/migrations'
|
7
|
+
require 'monkey_butler/util'
|
8
|
+
require 'monkey_butler/targets/base'
|
9
|
+
|
10
|
+
module MonkeyButler
|
11
|
+
class CLI < Thor
|
12
|
+
include Thor::Actions
|
13
|
+
include MonkeyButler::Actions
|
14
|
+
|
15
|
+
add_runtime_options!
|
16
|
+
|
17
|
+
# Configures root path for resources (e.g. templates)
|
18
|
+
def self.source_root
|
19
|
+
File.dirname(__FILE__)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :project
|
23
|
+
|
24
|
+
desc 'init [PATH]', 'Initializes a new repository into PATH'
|
25
|
+
method_option :name, type: :string, aliases: '-n', desc: "Specify project name"
|
26
|
+
method_option :database, type: :string, aliases: '-d', desc: "Specify database path or URL."
|
27
|
+
method_option :targets, type: :array, aliases: '-g', default: [], desc: "Specify default code targets."
|
28
|
+
method_option :bundler, type: :boolean, aliases: '-b', default: false, desc: "Use Bundler to import MonkeyButler into project."
|
29
|
+
method_option :config, type: :hash, aliases: '-c', default: {}, desc: "Specify config variables."
|
30
|
+
def init(path)
|
31
|
+
if File.exists?(path)
|
32
|
+
raise Error, "Cannot create repository: regular file exists at path '#{path}'" unless File.directory?(path)
|
33
|
+
raise Error, "Cannot create repository into non-empty path '#{path}'" if File.directory?(path) && Dir.entries(path) != %w{. ..}
|
34
|
+
end
|
35
|
+
self.destination_root = File.expand_path(path)
|
36
|
+
empty_directory('.')
|
37
|
+
inside(destination_root) { git init: '-q' }
|
38
|
+
|
39
|
+
# hydrate the project
|
40
|
+
project_name = options['name'] || File.basename(path)
|
41
|
+
sanitized_options = options.reject { |k,v| %w{bundler pretend database}.include?(k) }
|
42
|
+
sanitized_options[:name] = project_name
|
43
|
+
sanitized_options[:database_url] = options[:database] || "sqlite:#{project_name}.sqlite"
|
44
|
+
@project = MonkeyButler::Project.set(sanitized_options)
|
45
|
+
|
46
|
+
# generate_gitignore
|
47
|
+
template('templates/gitignore.erb', ".gitignore")
|
48
|
+
git_add '.gitignore'
|
49
|
+
|
50
|
+
# generate_config
|
51
|
+
create_file '.monkey_butler.yml', YAML.dump(sanitized_options)
|
52
|
+
git_add '.monkey_butler.yml'
|
53
|
+
|
54
|
+
# generate_gemfile
|
55
|
+
if options['bundler']
|
56
|
+
template('templates/Gemfile.erb', "Gemfile")
|
57
|
+
end
|
58
|
+
|
59
|
+
# init_targets
|
60
|
+
project = MonkeyButler::Project.set(sanitized_options)
|
61
|
+
target_options = options.merge('name' => project_name)
|
62
|
+
MonkeyButler::Util.target_classes_named(options[:targets]) do |target_class|
|
63
|
+
say "Initializing target '#{target_class.name}'..."
|
64
|
+
invoke(target_class, :init, [], target_options)
|
65
|
+
end
|
66
|
+
project.save!(destination_root) unless options['pretend']
|
67
|
+
git_add '.monkey_butler.yml'
|
68
|
+
|
69
|
+
# Run after targets in case they modify the Gemfile
|
70
|
+
# run_bundler
|
71
|
+
if options['bundler']
|
72
|
+
git_add "Gemfile"
|
73
|
+
bundle
|
74
|
+
git_add "Gemfile.lock"
|
75
|
+
end
|
76
|
+
|
77
|
+
# touch_database
|
78
|
+
create_file(project.schema_path)
|
79
|
+
git_add project.schema_path
|
80
|
+
|
81
|
+
# init_database_adapter
|
82
|
+
empty_directory('migrations')
|
83
|
+
inside do
|
84
|
+
invoke(project.database_target_class, :init, [], target_options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "dump", "Dump project schema from a database"
|
89
|
+
def dump
|
90
|
+
@project = MonkeyButler::Project.load
|
91
|
+
invoke(project.database_target_class, :dump, [], options)
|
92
|
+
end
|
93
|
+
|
94
|
+
desc "load", "Load project schema into a database"
|
95
|
+
def load
|
96
|
+
@project = MonkeyButler::Project.load
|
97
|
+
invoke(project.database_target_class, :load, [], options)
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "drop", "Drop the schema currently loaded into a database"
|
101
|
+
def drop
|
102
|
+
@project = MonkeyButler::Project.load
|
103
|
+
invoke(project.database_target_class, :drop, [], options)
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "new NAME", "Create a new migration"
|
107
|
+
def new(name)
|
108
|
+
@project = MonkeyButler::Project.load
|
109
|
+
empty_directory('migrations')
|
110
|
+
invoke(project.database_target_class, :new, [name], options)
|
111
|
+
end
|
112
|
+
|
113
|
+
desc "status", "Display current schema version and any pending migrations"
|
114
|
+
method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
|
115
|
+
def status
|
116
|
+
project = MonkeyButler::Project.load
|
117
|
+
migrations = MonkeyButler::Migrations.new(project.migrations_path, database)
|
118
|
+
|
119
|
+
if database.migrations_table?
|
120
|
+
say "Current version: #{migrations.current_version}"
|
121
|
+
pending_count = migrations.pending.size
|
122
|
+
version = (pending_count == 1) ? "version" : "versions"
|
123
|
+
say "The database at '#{database}' is #{pending_count} #{version} behind #{migrations.latest_version}" unless migrations.up_to_date?
|
124
|
+
else
|
125
|
+
say "New database"
|
126
|
+
say "The database at '#{database}' does not have a 'schema_migrations' table."
|
127
|
+
end
|
128
|
+
|
129
|
+
if migrations.up_to_date?
|
130
|
+
say "Database is up to date."
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
say
|
135
|
+
say "Migrations to be applied"
|
136
|
+
with_padding do
|
137
|
+
say %q{(use "mb migrate" to apply)}
|
138
|
+
say
|
139
|
+
with_padding do
|
140
|
+
migrations.pending do |version, path|
|
141
|
+
say "pending migration: #{path}", :green
|
142
|
+
end
|
143
|
+
end
|
144
|
+
say
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
desc "migrate [VERSION]", "Apply pending migrations to a database"
|
149
|
+
method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
|
150
|
+
method_option :dump, type: :boolean, aliases: '-D', desc: "Dump schema after migrate"
|
151
|
+
def migrate(version = nil)
|
152
|
+
project = MonkeyButler::Project.load
|
153
|
+
|
154
|
+
if migrations.up_to_date?
|
155
|
+
say "Database is up to date."
|
156
|
+
return
|
157
|
+
end
|
158
|
+
|
159
|
+
target_version = version || migrations.latest_version
|
160
|
+
if database.migrations_table?
|
161
|
+
say "Migrating from #{database.current_version} to #{target_version}"
|
162
|
+
else
|
163
|
+
say "Migrating new database to #{target_version}"
|
164
|
+
end
|
165
|
+
say
|
166
|
+
|
167
|
+
with_padding do
|
168
|
+
say "Migrating database..."
|
169
|
+
say
|
170
|
+
with_padding do
|
171
|
+
migrations.pending do |version, path|
|
172
|
+
say "applying migration: #{path}", :green
|
173
|
+
begin
|
174
|
+
database.execute_migration(File.read(path))
|
175
|
+
database.insert_version(version)
|
176
|
+
rescue project.database_class.exception_class => exception
|
177
|
+
fail Error, "Failed loading migration: #{exception}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
say
|
182
|
+
end
|
183
|
+
|
184
|
+
say "Migration to version #{target_version} complete."
|
185
|
+
|
186
|
+
if options['dump']
|
187
|
+
say
|
188
|
+
invoke :dump, [], options
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
desc "validate", "Validate that schema loads and all migrations are linearly applicable"
|
193
|
+
def validate
|
194
|
+
project = MonkeyButler::Project.load
|
195
|
+
|
196
|
+
say "Validating project configuration..."
|
197
|
+
say_status :git, "configuration", (project.git_url.empty? ? :red : :green)
|
198
|
+
if project.git_url.empty?
|
199
|
+
fail Error, "Invalid configuration: git does not have a remote named 'origin'."
|
200
|
+
end
|
201
|
+
say
|
202
|
+
|
203
|
+
invoke(project.database_target_class, :validate, [], options)
|
204
|
+
|
205
|
+
say "Validating schema loads..."
|
206
|
+
truncate_database
|
207
|
+
load
|
208
|
+
say
|
209
|
+
|
210
|
+
say "Validating migrations apply..."
|
211
|
+
truncate_database
|
212
|
+
migrate
|
213
|
+
say
|
214
|
+
|
215
|
+
say "Validating targets..."
|
216
|
+
target_names = options['targets'] || project.targets
|
217
|
+
MonkeyButler::Util.target_classes_named(target_names) do |target_class|
|
218
|
+
with_padding do
|
219
|
+
say_status :validate, target_class.name
|
220
|
+
invoke_with_padding(target_class, :validate, [], options)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
say
|
224
|
+
|
225
|
+
say "Validation successful."
|
226
|
+
end
|
227
|
+
|
228
|
+
desc "generate", "Generate platform specific migration implementations"
|
229
|
+
method_option :targets, type: :array, aliases: '-t', desc: "Generate only the specified targets."
|
230
|
+
def generate
|
231
|
+
project = MonkeyButler::Project.load
|
232
|
+
invoke(project.database_target_class, :generate, [], options)
|
233
|
+
target_names = options['targets'] || project.targets
|
234
|
+
MonkeyButler::Util.target_classes_named(target_names) do |target_class|
|
235
|
+
say "Invoking target '#{target_class.name}'..."
|
236
|
+
invoke(target_class, :generate, [], options)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
desc "package", "Package a release by validating, generating, and tagging a version."
|
241
|
+
method_option :diff, type: :boolean, desc: "Show Git diff after packaging"
|
242
|
+
method_option :commit, type: :boolean, desc: "Commit package artifacts after build."
|
243
|
+
def package
|
244
|
+
validate
|
245
|
+
say
|
246
|
+
generate
|
247
|
+
say
|
248
|
+
|
249
|
+
git_add '.'
|
250
|
+
git :status unless options['quiet']
|
251
|
+
|
252
|
+
show_diff = options['diff'] != false && (options['diff'] || ask("Review package diff?", limited_to: %w{y n}) == 'y')
|
253
|
+
git diff: '--cached' if show_diff
|
254
|
+
|
255
|
+
commit = options['commit'] != false && (options['commit'] || ask("Commit package artifacts?", limited_to: %w{y n}) == 'y')
|
256
|
+
if commit
|
257
|
+
tag = unique_tag_for_version(migrations.latest_version)
|
258
|
+
git commit: "#{options['quiet'] && '-q '}-m 'Packaging release #{tag}' ."
|
259
|
+
git tag: "#{tag}"
|
260
|
+
else
|
261
|
+
say "Package artifacts were built but not committed. Re-run `mb package` when ready to complete build."
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
desc "push", "Push a release to Git, CocoaPods, Maven, etc."
|
266
|
+
def push
|
267
|
+
# Verify that the tag exists
|
268
|
+
git tag: "-l #{migrations.latest_version}"
|
269
|
+
unless $?.exitstatus.zero?
|
270
|
+
fail Error, "Could not find tag #{migrations.latest_version}. Did you forget to run `mb package`?"
|
271
|
+
end
|
272
|
+
push_options = []
|
273
|
+
push_options << '--force' if options['force']
|
274
|
+
branch_name = project.git_current_branch
|
275
|
+
run "git config branch.`git symbolic-ref --short HEAD`.merge", verbose: false
|
276
|
+
unless $?.exitstatus.zero?
|
277
|
+
say_status :git, "no merge branch detected: setting upstream during push", :yellow
|
278
|
+
push_options << "--set-upstream origin #{branch_name}"
|
279
|
+
end
|
280
|
+
push_options << "origin #{branch_name}"
|
281
|
+
push_options << "--tags"
|
282
|
+
|
283
|
+
git push: push_options.join(' ')
|
284
|
+
unless $?.exitstatus.zero?
|
285
|
+
fail Error, "git push failed."
|
286
|
+
end
|
287
|
+
|
288
|
+
# Give the targets a chance to push
|
289
|
+
target_names = options['targets'] || project.targets
|
290
|
+
MonkeyButler::Util.target_classes_named(target_names) do |target_class|
|
291
|
+
say "Invoking target '#{target_class.name}'..."
|
292
|
+
invoke(target_class, :push, [], options)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
desc "config", "Get and set configuration options."
|
297
|
+
def config(key = nil, value = nil)
|
298
|
+
if key && value
|
299
|
+
project.config[key] = value
|
300
|
+
project.save!(Dir.pwd)
|
301
|
+
elsif key
|
302
|
+
value = project.config[key]
|
303
|
+
if value
|
304
|
+
say "#{key}=#{value}"
|
305
|
+
else
|
306
|
+
say "No value for key '#{key}'"
|
307
|
+
end
|
308
|
+
else
|
309
|
+
project.config.each { |key, value| say "#{key}=#{value}" }
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Hook into the command execution for dynamic task configuration
|
314
|
+
def self.start(given_args = ARGV, config = {})
|
315
|
+
if File.exists?(Dir.pwd + '/.monkey_butler.yml')
|
316
|
+
project = MonkeyButler::Project.load
|
317
|
+
project.database_target_class.register_with_cli(self)
|
318
|
+
end
|
319
|
+
super
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
def unique_tag_for_version(version)
|
324
|
+
return version if options['pretend']
|
325
|
+
|
326
|
+
revision = nil
|
327
|
+
tag = nil
|
328
|
+
begin
|
329
|
+
tag = [version, revision].compact.join('.')
|
330
|
+
existing_tag = run "git tag -l #{tag}", capture: true
|
331
|
+
break if existing_tag == ""
|
332
|
+
revision = revision.to_i + 1
|
333
|
+
end while true
|
334
|
+
tag
|
335
|
+
end
|
336
|
+
|
337
|
+
private
|
338
|
+
def bundle
|
339
|
+
inside(destination_root) { run "bundle" }
|
340
|
+
end
|
341
|
+
|
342
|
+
def project
|
343
|
+
@project ||= MonkeyButler::Project.load
|
344
|
+
end
|
345
|
+
|
346
|
+
def database
|
347
|
+
@database ||= project.database_class.new((options[:database] && URI(options[:database])) || project.database_url)
|
348
|
+
end
|
349
|
+
|
350
|
+
def migrations
|
351
|
+
@migrations ||= MonkeyButler::Migrations.new(project.migrations_path, database)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module MonkeyButler
|
2
|
+
module Databases
|
3
|
+
class AbstractDatabase
|
4
|
+
class << self
|
5
|
+
def migration_ext
|
6
|
+
raise NotImplementedError, "Required method not implemented."
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def migrations_table?
|
11
|
+
raise NotImplementedError, "Required method not implemented."
|
12
|
+
end
|
13
|
+
|
14
|
+
def origin_version
|
15
|
+
raise NotImplementedError, "Required method not implemented."
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_version
|
19
|
+
raise NotImplementedError, "Required method not implemented."
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_versions
|
23
|
+
raise NotImplementedError, "Required method not implemented."
|
24
|
+
end
|
25
|
+
|
26
|
+
def insert_version(version)
|
27
|
+
raise NotImplementedError, "Required method not implemented."
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute_migration(content)
|
31
|
+
raise NotImplementedError, "Required method not implemented."
|
32
|
+
end
|
33
|
+
|
34
|
+
def drop
|
35
|
+
raise NotImplementedError, "Required method not implemented."
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :url
|
39
|
+
|
40
|
+
def initialize(url)
|
41
|
+
@url = url
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
url.to_s
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|