diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f7f2f8..cca34e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html ### Breaking changes +## 12.6.0 2025-09-22 + +### Compatible changes + +- `geordi dump`: Allow to forward the compression option to the underlying `dumple` command, e.g. `geordi dump --compress=zstd:3` (for PostgreSQL) or `geordi dump --compress` (for MySQL). +- `dumple`: Allow to specify a compression algorithm for PostgreSQL, e.g. `dumple --compress=zstd:3`. The already supported compression for MySQL `dumple --compress` is kept untouched. + + ## 12.5.0 2025-09-09 ### Compatible changes diff --git a/Gemfile.lock b/Gemfile.lock index 82c57dab..42a24ed0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - geordi (12.5.0) + geordi (12.6.0) highline thor (~> 1) diff --git a/README.md b/README.md index 296131d7..8fbf86bc 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ Example: `geordi chromedriver_update` This command will find and install the matching chromedriver for the currently installed Chrome. -Setting `auto_update_chromedriver` to `true` in your global Geordi config file -(`~/.config/geordi/global.yml`), will automatically update chromedriver before +Setting `auto_update_chromedriver` to `true` in your global Geordi config file +(`~/.config/geordi/global.yml`), will automatically update chromedriver before cucumber tests if a newer chromedriver version is available. @@ -98,8 +98,8 @@ servers. When passed a number, directly connects to the selected server. IRB flags can be given as `irb_flags: '...'` in the global or local Geordi config file (`~/.config/geordi/global.yml` / `./.geordi.yml`). If you define irb_flags in both files, the local config file will be used. For IRB >=1.2 in combination with Ruby <3 geordi automatically sets the `--nomultiline` flag, to prevent slow -pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags` -in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for +pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags` +in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for the irb_flags key. **Options** @@ -191,8 +191,8 @@ and offer to delete them. Excluded are databases that are whitelisted. This come in handy when you're keeping your currently active projects in the whitelist files and perform regular housekeeping with Geordi. -Per default, Geordi will try to connect to the databases as a local user without -password authorization. +Per default, Geordi will try to connect to the databases as a local user without +password authorization. Geordi will ask for confirmation before actually dropping databases and will offer to edit the whitelist instead. @@ -234,6 +234,7 @@ not match, please issue separate commands for dumping (`dump -d`) and sourcing **Options** - `-l, --load=[DUMP_FILE]`: Load a dump - `-d, --database=NAME`: Target database, if there are multiple databases +- `-c, --compress=[ALGORITHM]`: Compress the dump file (default for PSQL) ### `geordi help [COMMAND]` @@ -341,7 +342,7 @@ Run all employed tests. When running `geordi tests` without any arguments, all unit tests, rspec specs and cucumber features will be run. -When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`. +When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`. All rspec specs and cucumber features matching the given paths will be run. @@ -389,7 +390,9 @@ Stores a timestamped database dump for the given Rails environment in `~/dumps`: **Options** - `-i`: Print disk usage of `~/dumps` -- `--compress`: After dumping, run gzip to compress the dump in place +- `--fail-gently`: On error, do not crash but print a warning and exit(0) +- `--for-download`: Dump to `~/dumps/dump_for_download.dump` +- `--compress`: Compress the dump (default for PostgreSQL) and optionally set the compression algorithm (only available for PostgreSQL) Contributing diff --git a/exe/dumple b/exe/dumple index b6b87767..46debdcb 100755 --- a/exe/dumple +++ b/exe/dumple @@ -50,7 +50,7 @@ def cd_to_project_root(fail_gently) end end -def dump_command(dump_file, config) +def dump_command(dump_file, config, compress) host = config['host'] port = config['port'] @@ -79,6 +79,7 @@ def dump_command(dump_file, config) command << " pg_dump #{config['database']}" command << " --clean" command << " --format=custom" + command << " --compress=#{compress}" if compress.is_a?(String) command << " --file=#{dump_file}" command << " --username=\"#{config['username']}\"" command << " --host=#{host}" if host @@ -124,7 +125,7 @@ def prepare_dump_path(config) run "chmod 700 #{DUMPS_DIR}" end - if ARGV.include? '--for_download' + if ARGV.include?('--for_download') || ARGV.include?('--for-download') "#{DUMPS_DIR}/dump_for_download.dump" else "#{DUMPS_DIR}/#{config['database']}_#{Time.now.strftime("%Y%m%d_%H%M%S")}.dump" @@ -133,8 +134,14 @@ end begin fail_gently = ARGV.include?("--fail-gently") - compress = ARGV.include?("--compress") - environment, database = ARGV.reject { |arg| arg[0].chr == '-' } + compress = ARGV.find do |argument| + if argument == '--compress' + break true + elsif argument.start_with?('--compress=') + break argument.split('=').last + end + end + environment, database = ARGV.reject { |argument| argument.start_with?('-') } cd_to_project_root(fail_gently) config = find_database_config(DB_CONFIG_PATH, environment, database) @@ -142,12 +149,17 @@ begin # Dump! given_database = database ? %(#{database} ) : "" - command = dump_command(dump_path, config) + command = dump_command(dump_path, config, compress) puts "> Dumping #{given_database}database for \"#{environment}\" environment ..." run command or raise "x Creating the dump failed." run "chmod 600 #{dump_path}" - if compress + if config['adapter'] == 'mysql' && compress.is_a?(String) + puts "> Cannot compress a MySQL dump with #{compress}, falling back to gzip." + end + + # For PostgreSQL, #dump_command will do the compression. MySQL needs manual compression. + if config['adapter'] == 'mysql' && compress puts "> Compressing the dump ..." # gzip compresses in place compress_success = run "gzip #{dump_path}" diff --git a/features/dump.feature b/features/dump.feature index 61a32543..1f03d28d 100644 --- a/features/dump.feature +++ b/features/dump.feature @@ -17,9 +17,7 @@ Feature: The dump command Scenario: Creating a dump of a remote database Given a file named "Capfile" with "Capfile exists" - And a file named "config/deploy.rb" with: - """ - """ + And a file named "config/deploy.rb" with "deploy.rb exists" And a file named "config/deploy/staging.rb" with: """ set :rails_env, 'staging' @@ -32,7 +30,7 @@ Feature: The dump command When I run `geordi dump staging` Then the output should contain "# Dumping the database of staging" And the output should contain "> Connecting to www.example.com" - And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging --for_download'" + And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging --for-download'" And the output should contain "> Downloading remote dump to tmp/staging.dump" # Omitting the absolute path in this regex (.*) And the output should match: @@ -62,7 +60,7 @@ Feature: The dump command When I run `geordi dump staging --load` Then the output should contain "# Dumping the database of staging" - And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging --for_download'" + And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging --for-download'" And the output should contain "> Dumped the staging database to tmp/staging.dump" # Loading the dump @@ -85,7 +83,7 @@ Feature: The dump command When I run `geordi dump staging --database primary` Then the output should contain "# Dumping the database of staging (primary database)" - And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging primary --for_download'" + And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging primary --for-download'" And the output should contain "> Dumped the primary staging database to tmp/staging.dump" @@ -110,7 +108,7 @@ Feature: The dump command When I run `geordi dump staging --database primary --load` Then the output should contain "# Dumping the database of staging (primary database)" - And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging primary --for_download'" + And the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging primary --for-download'" And the output should contain "> Dumped the primary staging database to tmp/staging.dump" # Loading the dump @@ -176,6 +174,7 @@ Feature: The dump command And the output should contain "Util.run! dropdb --if-exists test-other && createdb test-other && pg_restore --no-owner --no-acl --dbname=test-other tmp/production.dump" But the output should not contain "Sourcing dump into the test-primary db" + Scenario: Sourcing a dump into one of multiple databases without specifying a db Given a file named "tmp/production.dump" with "some content" And a file named "config/database.yml" with: @@ -193,3 +192,32 @@ Feature: The dump command And the output should contain "Source file: tmp/production.dump" And the output should contain "Util.run! dropdb --if-exists test-primary && createdb test-primary && pg_restore --no-owner --no-acl --dbname=test-primary tmp/production.dump" But the output should not contain "Sourcing dump into the test-other db" + + + Scenario: Enforcing compression + When I run `geordi dump --compress` + Then the output should contain "Util.run! dumple development --compress" + And the output should contain "Successfully dumped the development database" + + + Scenario: Setting a custom compression algorithm + When I run `geordi dump --compress=zstd:3` + Then the output should contain "Util.run! dumple development --compress=zstd:3" + And the output should contain "Successfully dumped the development database" + + + Scenario: Setting a custom compression algorithm for a remote target + Given a file named "Capfile" with "Capfile exists" + And a file named "config/deploy.rb" with "deploy.rb exists" + And a file named "config/deploy/staging.rb" with: + """ + set :rails_env, 'staging' + set :deploy_to, '/var/www/example.com' + set :user, 'user' + + server 'www.example.com' + """ + + When I run `geordi dump staging --compress=zstd:3` + Then the output should contain "Util.run! ssh, user@www.example.com, -t, cd /var/www/example.com/current && bash --login -c 'dumple staging --compress=zstd:3 --for-download'" + And the output should contain "> Dumped the staging database to tmp/staging.dump" diff --git a/features/dumple.feature b/features/dumple.feature index e8640b82..0dbc6a41 100644 --- a/features/dumple.feature +++ b/features/dumple.feature @@ -1,6 +1,6 @@ Feature: Creating Rails database dumps with the "dumple" script - The --for_download option generates a deterministic file name. + The --for-download option generates a deterministic file name. Scenario: Execution outside of a Rails application When I run `dumple development` @@ -20,7 +20,7 @@ Feature: Creating Rails database dumps with the "dumple" script Since dumple won't actually dump in tests, we prepare the dump file in advance. """ - When I run `dumple development --for_download` + When I run `dumple development --for-download` Then the output should match /Dumping database for "development" environment/ And the output should contain "> Dumped to /home/" And the output should contain "/geordi/tmp/aruba/dumps/dump_for_download.dump (0 KB)" @@ -36,7 +36,7 @@ Feature: Creating Rails database dumps with the "dumple" script """ And a mocked home directory - When I run `dumple development --for_download` + When I run `dumple development --for-download` Then the output should match %r @@ -50,7 +50,7 @@ Feature: Creating Rails database dumps with the "dumple" script """ And a mocked home directory - When I run `dumple development --for_download` + When I run `dumple development --for-download` Then the output should match %r @@ -133,3 +133,60 @@ Feature: Creating Rails database dumps with the "dumple" script When I run `dumple development sub` Then the output should match /Could not select "sub" database in a single-db environment/ + + + Scenario: Instructing MySQL to compress the dump + Given a file named "config/database.yml" with: + """ + development: + adapter: mysql + database: geordi_development + """ + And a mocked home directory + And a file named "dumps/dump_for_download.dump.gz" with: + """ + Since dumple won't actually dump in tests, we prepare the dump file in advance. + """ + + When I run `dumple development --for-download --compress` + Then the output should match /Dumping database for "development" environment/ + And the output should contain "> Compressing the dump ..." + And the output should contain "system gzip" + And the output should contain "> Dumped to /home/" + And the output should contain "/geordi/tmp/aruba/dumps/dump_for_download.dump.gz (0 KB)" + + + Scenario: Instructing MySQL to compress the dump with a custom compression algorithm shows a warning + Given a file named "config/database.yml" with: + """ + development: + adapter: mysql + database: geordi_development + """ + And a mocked home directory + And a file named "dumps/dump_for_download.dump.gz" with: + """ + Since dumple won't actually dump in tests, we prepare the dump file in advance. + """ + + When I run `dumple development --for-download --compress=zstd:3` + Then the output should match /Dumping database for "development" environment/ + And the output should contain "> Cannot compress a MySQL dump with zstd:3, falling back to gzip." + And the output should contain "> Compressing the dump ..." + And the output should contain "system gzip" + And the output should contain "> Dumped to /home/" + And the output should contain "/geordi/tmp/aruba/dumps/dump_for_download.dump.gz (0 KB)" + + + Scenario: Setting a custom compression algorithm for PostgreSQL + Given a file named "config/database.yml" with: + """ + development: + adapter: postgres + username: user + password: password + """ + And a mocked home directory + + When I run `dumple development --for-download --compress=zstd:3` + Then the output should match %r diff --git a/lib/geordi/commands/dump.rb b/lib/geordi/commands/dump.rb index 27cf196b..e07256d3 100644 --- a/lib/geordi/commands/dump.rb +++ b/lib/geordi/commands/dump.rb @@ -26,11 +26,11 @@ option :load, aliases: '-l', type: :string, desc: 'Load a dump', banner: '[DUMP_FILE]' option :database, aliases: '-d', type: :string, desc: 'Target database, if there are multiple databases', banner: 'NAME' +option :compress, aliases: '-c', type: :string, desc: 'Compress the dump file (default for PSQL)', banner: '[ALGORITHM]' def dump(target = nil, *_args) require 'geordi/dump_loader' require 'geordi/remote' - database = options[:database] ? "#{options[:database]} " : '' if target.nil? # Local … if options.load # … dump loading @@ -46,14 +46,17 @@ def dump(target = nil, *_args) else # … dump creation Interaction.announce 'Dumping the development database' - Util.run!("dumple development #{database}") + Util.run!(Util.dumple_command('development', options)) + + database = "#{options[:database]} " if options[:database] Interaction.success "Successfully dumped the #{database}development database." end else # Remote dumping … - database_label = options[:database] ? " (#{database}database)" : "" + database_label = target.dup + database_label << " (#{options[:database]} database)" if options[:database] - Interaction.announce "Dumping the database of #{target}#{database_label}" + Interaction.announce "Dumping the database of #{database_label}" dump_path = Geordi::Remote.new(target).dump(options) if options.load # … and dump loading @@ -65,7 +68,7 @@ def dump(target = nil, *_args) Util.run! "rm #{dump_path}" Interaction.note "Dump file removed" - Interaction.success "Your #{loader.config['database']} database has now the data of #{target}#{database_label}." + Interaction.success "Your #{loader.config['database']} database has now the data of #{database_label}." end end diff --git a/lib/geordi/remote.rb b/lib/geordi/remote.rb index 47bc8576..2d379ad8 100644 --- a/lib/geordi/remote.rb +++ b/lib/geordi/remote.rb @@ -33,11 +33,9 @@ def select_server end def dump(options = {}) - database = options[:database] ? " #{options[:database]}" : '' # Generate dump on the server - shell options.merge({ - remote_command: "dumple #{@config.env}#{database} --for_download", - }) + dumple = Util.dumple_command(@config.env, options.merge(for_download: true)) + shell(options.merge(remote_command: dumple)) destination_directory = File.join(@config.root, 'tmp') FileUtils.mkdir_p destination_directory @@ -48,6 +46,7 @@ def dump(options = {}) server = @config.primary_server Util.run!("scp -C #{@config.user(server)}@#{server}:#{REMOTE_DUMP_PATH} #{destination_path}") + database = " #{options[:database]}" if options[:database] Interaction.success "Dumped the#{database} #{@stage} database to #{relative_destination}." destination_path diff --git a/lib/geordi/util.rb b/lib/geordi/util.rb index 6b622742..e48a4713 100644 --- a/lib/geordi/util.rb +++ b/lib/geordi/util.rb @@ -119,6 +119,21 @@ def server_command end end + def dumple_command(environment, options) + compress = if options[:compress] == 'compress' + '--compress' + elsif options[:compress] + "--compress=#{options[:compress]}" + end + + cmd = ['dumple'] + cmd << environment + cmd << options[:database] + cmd << compress + cmd << '--for-download' if options[:for_download] + + cmd.compact.join(' ') + end def deploy_targets Dir['config/deploy/*'].map do |f| @@ -205,6 +220,7 @@ def cucumber_path?(path) def rspec_path?(path) %r{(^|\/)spec|_spec\.rb($|:)}.match?(path) end + end end end diff --git a/lib/geordi/version.rb b/lib/geordi/version.rb index 4dac609e..64adc711 100644 --- a/lib/geordi/version.rb +++ b/lib/geordi/version.rb @@ -1,3 +1,3 @@ module Geordi - VERSION = '12.5.0'.freeze + VERSION = '12.6.0'.freeze end diff --git a/spec/util_spec.rb b/spec/util_spec.rb index b7be6bd9..a80bbd48 100644 --- a/spec/util_spec.rb +++ b/spec/util_spec.rb @@ -158,4 +158,5 @@ end end + end