DEV: Introduce script/promote_migrations tool

DEV: Introduce script/promote_migrations tool

Post-deploy migrations exist to allow for seamless Discourse upgrades. By design, they cause migrations to run out of numerical order. This has the potential to cause some unexpected edge cases. To reduce the likelihood of these edge cases, we will promote historical post_deploy migrations to regular migrations after a full Discourse stable release cycle.

This script is intended to be run at least during every Discourse release cycle.

This means that truly seamless upgrades will not be possible between non-consecutive Discourse versions. (Upgrades will still work, but may cause some server errors for users during the upgrade)

diff --git a/script/promote_migrations b/script/promote_migrations
new file mode 100755
index 0000000..a76e751
--- /dev/null
+++ b/script/promote_migrations
@@ -0,0 +1,97 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# This script will promote post_migrate files
+# which have existed for more than one Discourse
+# stable version cycle.
+#
+# Renames will be staged in git, but not committed
+#
+# Usage:
+#   script/promote_migrations [--dry-run] [--plugins]
+
+require 'open3'
+require 'fileutils'
+
+VERSION_REGEX = %r{\/(\d+)_}
+DRY_RUN = ARGV.include? '--dry-run'
+PLUGINS = ARGV.include? '--plugins'
+
+def run(*args, capture: true)
+  out, s = Open3.capture2(*args)
+  if s.exitstatus != 0
+    STDERR.puts "Command failed: '#{args.join(' ')}'"
+    exit 1
+  end
+  out.strip
+end
+
+current_version = run 'git describe --abbrev=0 --match "v*"'
+puts "Current version is #{current_version}"
+
+run 'git fetch'
+current_stable_version =
+  run 'git describe --abbrev=0 --match "v*" origin/stable'
+puts "Current stable version is #{current_stable_version}"
+
+minor = current_stable_version[/^(v\d+\.\d+)\./, 1]
+
+previous_stable_version =
+  run "git describe --abbrev=0 --match 'v*' --exclude '#{minor}*' origin/stable"
+puts "Previous stable version is #{previous_stable_version}"
+
+stable_post_migrate_filenames =
+  run(
+    'git',
+    'ls-tree',
+    '--name-only',
+    '-r',
+    previous_stable_version,
+    'db/post_migrate'
+  ).split("\n")
+
+stable_post_migrate_filenames.sort!
+latest_stable_post_migration = stable_post_migrate_filenames.last
+
+puts "The latest core post_migrate file in #{previous_stable_version} is #{latest_stable_post_migration}"
+puts 'Promoting this, and all earlier post_migrates, to regular migrations'
+
+promote_threshold = latest_stable_post_migration[VERSION_REGEX, 1].to_i
+current_post_migrations =
+  if PLUGINS
+    puts 'Looking in plugins...'
+    Dir.glob('plugins/*/db/post_migrate/*')
+  else
+    Dir.glob('db/post_migrate/*')
+  end
+
+if current_post_migrations.length == 0
+  puts 'No post_migrate files found. All done'
+end
+
+current_post_migrations.each do |path|
+  version = path[VERSION_REGEX, 1].to_i
+  file = File.basename(path)
+  dir = File.dirname(path)
+
+  if version <= promote_threshold
+    print "Promoting #{path}..."
+    if DRY_RUN
+      puts ' (dry run)'
+    else
+      run 'mkdir', '-p', "#{dir}/../migrate"
+      run 'git', '-C', dir, 'mv', file, "../migrate/#{file}"
+      puts ' (done)'
+    end
+  end
+end
+
+puts 'Done! File moves are staged and ready for commit.'
+puts 'Suggested commit message:'
+puts '-' * 20
+puts <<~MESSAGE
+DEV: Promote historic post_deploy migrations
+
+This commit promotes all post_deploy migrations which existed in Discourse #{previous_stable_version} (timestamp <= #{promote_threshold})
+MESSAGE
+puts '-' * 20

GitHub sha: 49f39434c48c6f05ab2037f0f31c9da521401cfb

This commit appears in #13496 which was approved by CvX. It was merged by davidtaylorhq.