310 lines
8.1 KiB
Ruby
Executable File
310 lines
8.1 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
# frozen_string_literal: true
|
|
|
|
ENV['RAILS_ENV'] = 'test'
|
|
|
|
require 'optparse'
|
|
require 'open3'
|
|
require 'fileutils'
|
|
require 'uri'
|
|
|
|
class SchemaRegenerator
|
|
##
|
|
# Filename of the schema
|
|
#
|
|
# This file is being regenerated by this script.
|
|
FILENAME = 'db/structure.sql'
|
|
|
|
##
|
|
# Directories where migrations are stored
|
|
#
|
|
# The methods +hide_migrations+ and +unhide_migrations+ will rename
|
|
# these to disable/enable migrations.
|
|
MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
|
|
|
|
##
|
|
# Directory where we store schema versions
|
|
#
|
|
# The remove_schema_migration_files removes files added in this
|
|
# directory when it runs.
|
|
SCHEMA_MIGRATIONS_DIR = 'db/schema_migrations/'
|
|
|
|
def initialize(options)
|
|
@rollback_testing = options.delete(:rollback_testing)
|
|
@init_schema_loading = options.delete(:init_schema_loading)
|
|
end
|
|
|
|
def execute
|
|
Dir.chdir(File.expand_path('..', __dir__)) do
|
|
# Note: `db:drop` must run prior to hiding migrations.
|
|
#
|
|
# Executing a Rails DB command e.g., `reset`, `drop`, etc. triggers running the initializers.
|
|
# During the initialization, the default values for `application_settings` need to be set.
|
|
# Depending on the presence of migrations, the default values are either faked or inserted.
|
|
#
|
|
# 1. If no migration is detected, all the necessary columns are in place from `db/structure.sql`.
|
|
# The default values can be inserted into `application_settings` table.
|
|
#
|
|
# 2. If a migration is detected, at least one column may be missing from `db/structure.sql`
|
|
# and needs to be added through the detected migration. In this case, the default values are faked.
|
|
# If not, an error would be raised e.g., "NoMethodError: undefined method `some_setting`"
|
|
#
|
|
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135085#note_1628210334 for more info.
|
|
#
|
|
load_tasks
|
|
drop_db
|
|
checkout_ref
|
|
checkout_clean_schema
|
|
hide_migrations
|
|
remove_schema_migration_files
|
|
stop_spring
|
|
setup_db
|
|
unhide_migrations
|
|
migrate
|
|
dump_schema
|
|
rollback if @rollback_testing
|
|
ensure
|
|
unhide_migrations
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def load_tasks
|
|
require_relative '../config/environment'
|
|
Gitlab::Application.load_tasks
|
|
end
|
|
|
|
##
|
|
# Git checkout +CI_COMMIT_SHA+.
|
|
#
|
|
# When running from CI, checkout the clean commit,
|
|
# not the merged result.
|
|
def checkout_ref
|
|
return unless ci?
|
|
|
|
run %(git checkout #{source_ref})
|
|
run %q(git clean -f -- db)
|
|
end
|
|
|
|
##
|
|
# Checkout the clean schema from the target branch
|
|
def checkout_clean_schema
|
|
remote_checkout_clean_schema || local_checkout_clean_schema
|
|
end
|
|
|
|
##
|
|
# Get clean schema from remote servers
|
|
#
|
|
# This script might run in CI, using a shallow clone, so to checkout
|
|
# the file, fetch the target branch from the server.
|
|
def remote_checkout_clean_schema
|
|
return false unless project_url
|
|
return false unless target_project_url
|
|
|
|
run %(git remote add target_project #{target_project_url}.git)
|
|
run %(git fetch target_project #{target_branch}:#{target_branch})
|
|
|
|
local_checkout_clean_schema
|
|
end
|
|
|
|
##
|
|
# Git checkout the schema from target branch.
|
|
#
|
|
# Ask git to checkout the schema from the target branch and reset
|
|
# the file to unstage the changes.
|
|
def local_checkout_clean_schema
|
|
run %(git checkout #{merge_base} -- #{FILENAME})
|
|
run %(git reset -- #{FILENAME})
|
|
end
|
|
|
|
##
|
|
# Move migrations to where Rails will not find them.
|
|
#
|
|
# To reset the database to clean schema defined in +FILENAME+, move
|
|
# the migrations to a path where Rails will not find them, otherwise
|
|
# +db:reset+ would abort. Later when the migrations should be
|
|
# applied, use +unhide_migrations+ to bring them back.
|
|
def hide_migrations
|
|
MIGRATION_DIRS.each do |dir|
|
|
File.rename(dir, "#{dir}__")
|
|
end
|
|
end
|
|
|
|
##
|
|
# Undo the effect of +hide_migrations+.
|
|
#
|
|
# Place back the migrations which might be moved by
|
|
# +hide_migrations+.
|
|
def unhide_migrations
|
|
error = nil
|
|
|
|
MIGRATION_DIRS.each do |dir|
|
|
File.rename("#{dir}__", dir)
|
|
rescue Errno::ENOENT
|
|
nil
|
|
rescue StandardError => e
|
|
# Save error for later, but continue with other dirs first
|
|
error = e
|
|
end
|
|
|
|
raise error if error
|
|
end
|
|
|
|
##
|
|
# Remove files added to db/schema_migrations
|
|
#
|
|
# In order to properly reset the database and re-run migrations
|
|
# the schema migrations for new migrations must be removed.
|
|
def remove_schema_migration_files
|
|
(untracked_schema_migrations + committed_schema_migrations).each do |schema_migration|
|
|
FileUtils.rm(schema_migration)
|
|
end
|
|
end
|
|
|
|
##
|
|
# List of untracked schema migrations
|
|
#
|
|
# Get a list of schema migrations that are not tracked so we can remove them
|
|
def untracked_schema_migrations
|
|
git_command = "git ls-files --others --exclude-standard -- #{SCHEMA_MIGRATIONS_DIR}"
|
|
run(git_command).chomp.split("\n")
|
|
end
|
|
|
|
##
|
|
# List of untracked schema migrations
|
|
#
|
|
# Get a list of schema migrations that have been committed since the last
|
|
def committed_schema_migrations
|
|
git_command = "git diff --name-only --diff-filter=A #{merge_base} -- #{SCHEMA_MIGRATIONS_DIR}"
|
|
run(git_command).chomp.split("\n")
|
|
end
|
|
|
|
##
|
|
# Stop spring before modifying the database
|
|
def stop_spring
|
|
run %q(bin/spring stop)
|
|
end
|
|
|
|
##
|
|
# Run rake task to drop the database.
|
|
def drop_db
|
|
run_rake_task 'db:drop'
|
|
end
|
|
|
|
##
|
|
# Run rake task to setup the database.
|
|
def setup_db
|
|
run_rake_task(@init_schema_loading ? 'db:create' : 'db:setup')
|
|
end
|
|
|
|
##
|
|
# Run rake task to run migrations.
|
|
def migrate
|
|
run_rake_task 'db:migrate'
|
|
end
|
|
|
|
##
|
|
# Run rake task to dump schema.
|
|
def dump_schema
|
|
run_rake_task 'db:schema:dump'
|
|
end
|
|
|
|
##
|
|
# Run rake task to rollback migrations.
|
|
def rollback
|
|
(untracked_schema_migrations + committed_schema_migrations).sort.reverse_each do |filename|
|
|
version = filename[/\d+\Z/]
|
|
run %(bin/rails db:rollback:main db:rollback:ci RAILS_ENV=test VERSION=#{version})
|
|
end
|
|
end
|
|
|
|
##
|
|
# Run the given +cmd+.
|
|
#
|
|
# The command is colored green, and the output of the command is
|
|
# colored gray.
|
|
# When the command failed an exception is raised.
|
|
def run(cmd)
|
|
puts "\e[32m$ #{cmd}\e[37m"
|
|
stdout_str, stderr_str, status = Open3.capture3(cmd)
|
|
puts "#{stdout_str}#{stderr_str}\e[0m"
|
|
raise("Command failed: #{stderr_str}") unless status.success?
|
|
|
|
stdout_str
|
|
end
|
|
|
|
def run_rake_task(*tasks, env: {})
|
|
Array.wrap(tasks).each do |task|
|
|
env.each { |k, v| ENV[k.to_s] = v.to_s }
|
|
|
|
puts "\e[32m$ bin/rails #{task} RAILS_ENV=test #{env.map { |m| m.join('=') }.join(' ')}\e[37m"
|
|
Rake::Task[task].invoke
|
|
end
|
|
end
|
|
|
|
##
|
|
# Return the base commit between source and target branch.
|
|
def merge_base
|
|
@merge_base ||= run("git merge-base #{target_branch} #{source_ref}").chomp
|
|
end
|
|
|
|
##
|
|
# Return the name of the target branch
|
|
#
|
|
# Get source ref from CI environment variable, or read the +TARGET+
|
|
# environment+ variable, or default to +HEAD+.
|
|
def target_branch
|
|
ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || ENV['CI_DEFAULT_BRANCH'] || 'master'
|
|
end
|
|
|
|
##
|
|
# Return the source ref
|
|
#
|
|
# Get source ref from CI environment variable, or default to +HEAD+.
|
|
def source_ref
|
|
ENV['CI_COMMIT_SHA'] || 'HEAD'
|
|
end
|
|
|
|
##
|
|
# Return the source project URL from CI environment variable.
|
|
def project_url
|
|
ENV['CI_PROJECT_URL']
|
|
end
|
|
|
|
##
|
|
# Return the target project URL from CI environment variable.
|
|
def target_project_url
|
|
ENV['CI_MERGE_REQUEST_PROJECT_URL']
|
|
end
|
|
|
|
##
|
|
# Return whether the script is running from CI
|
|
def ci?
|
|
ENV['CI']
|
|
end
|
|
end
|
|
|
|
if $PROGRAM_NAME == __FILE__
|
|
options = {}
|
|
|
|
OptionParser.new do |opts|
|
|
opts.on("-r", "--rollback-testing", String, "Enable rollback testing") do
|
|
options[:rollback_testing] = true
|
|
end
|
|
|
|
opts.on("-i", "--init-schema-loading", String,
|
|
"Enable clean migrations starting from the beginning - loading init_structure.sql") do
|
|
options[:init_schema_loading] = true
|
|
end
|
|
|
|
opts.on("-h", "--help", "Prints this help") do
|
|
puts opts
|
|
exit
|
|
end
|
|
end.parse!
|
|
|
|
SchemaRegenerator.new(options).execute
|
|
end
|