gitlab-ce/scripts/database/migrate.rb

158 lines
4.9 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
# This script is designed to manage database migrations for GitLab.
# It allows users to selectively run or revert migrations that have been changed in the current Git branch.
# This is especially useful for database reviewers and maintainers
# The script uses the 'fzf' command-line fuzzy finder for interactive selection of migration files.
#
# Examples:
#
# 1. Running migrations:
# $ ./scripts/database/migrate.rb
# This will show a list of changed migration files and allow you to select which ones to apply.
#
# 2. Reverting migrations:
# $ ./scripts/database/migrate.rb -t down
# This will show a list of changed migration files and allow you to select which ones to revert.
#
# 3. Dry run mode:
# $ ./scripts/database/migrate.rb -n
# This will show what commands would be executed without actually running them.
#
# 4. Debug mode:
# $ ruby scripts/database/migrate.rb --debug
# This will run the script with additional debug output for troubleshooting.
# 5. Custom base branch:
# $ BASE_REF=origin/master ruby scripts/database/migrate.rb
# This will run the script with origin/master as the base branch for migrations retrieval
#
# The script checks for changed migration files in both 'db/migrate' and 'db/post_migrate' directories,
# and executes the selected migrations for both the main and CI databases.
require 'optparse'
SCRIPT_NAME = File.basename($PROGRAM_NAME)
MIGRATIONS_DIR = 'db/migrate'
POST_DEPLOY_MIGRATIONS_DIR = 'db/post_migrate'
BRANCH_NAME = ENV.fetch('BASE_REF', 'master')
def require_commands!(*commands)
missing_commands = commands.reject { |command| system("command", "-v", command, out: File::NULL) }
abort("This script requires #{missing_commands.join(', ')} to be installed.") unless missing_commands.empty?
end
def parse_options
options = {
task: :up,
dry_run: false
}
OptionParser.new do |opts|
opts.banner = "Usage: #{SCRIPT_NAME} [options]"
opts.on('--debug', 'Enable debug mode') do |v|
options[:debug] = v
end
opts.on('-n', '--dry-run', 'Show commands without executing them') do |v|
options[:dry_run] = v
end
opts.on('-t', '--task [up|down]', [:up, :down], 'Set task - "up" to migrate forward, "down" to rollback') do |v|
options[:task] = v
end
end.parse!
options
end
def prompt(list, prompt:, multi: false, reverse: false)
arr = list.join("\n")
fzf_args = [].tap do |args|
args << '--layout="reverse"'
args << '--height=30%'
args << '--multi' if multi
args << '--tac' if reverse
end
output = IO.popen("echo \"#{arr}\" | fzf #{fzf_args.join(' ')} --prompt=\"#{prompt}\"", &:readlines)
return [] unless output
selection = output.join.strip
return selection unless multi
selection.split("\n")
end
def get_changed_files(branch_name:)
set = Set.new
[MIGRATIONS_DIR, POST_DEPLOY_MIGRATIONS_DIR].each do |dir|
set += `git diff --name-only --diff-filter=d $(git merge-base #{branch_name} HEAD)..HEAD #{dir}`
.split("\n").to_set
set += `git diff --diff-filter=d --merge-base --name-only #{branch_name} #{dir}`.split("\n")
end
set
end
# rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- we can skip it for this script
def execute
options = parse_options
puts "Options: #{options.inspect}" if options[:debug]
files = get_changed_files(branch_name: BRANCH_NAME)
puts "Files: #{files.inspect}" if options[:debug]
base_files = files.map { |path| File.basename(path) }
puts "Base files: #{base_files.inspect}" if options[:debug]
if base_files.empty?
puts 'No migration files found'
exit 1
end
selected_files = prompt(base_files, prompt: 'Select migrations (press tab to select multiple)> ', multi: true)
puts "Selected files: #{selected_files.inspect}" if options[:debug]
if selected_files.empty?
puts 'No files selected'
exit 1
end
sorted = selected_files.sort_by { |f| f.match(/^\d+/)[0].to_i }
puts "Sorted: #{sorted.inspect}" if options[:debug]
migrations = case options[:task]
when :up
sorted.map do |file|
version = file.match(/^\d+/)[0].to_i
"bin/rails db:migrate:up:main db:migrate:up:ci VERSION=#{version}"
end
when :down
sorted.reverse.map do |file|
version = file.match(/^\d+/)[0].to_i
"bin/rails db:migrate:down:main db:migrate:down:ci VERSION=#{version}"
end
else
puts 'Invalid task. Use --task=[up|down]'
exit 1
end
migrations.each do |cmd|
puts "$ #{cmd}"
if options[:dry_run]
puts "[dry-run] Skipping execution"
else
raise "Migration failed: #{cmd}" unless system(cmd)
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
require_commands!('fzf')
execute