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