From 271d6319ce00c4430900f7eb1e06168ea964b7da Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Mon, 6 Jul 2020 11:48:00 -1000 Subject: [PATCH] Support plugin and Theme compatibility version manifests (#9995) Adds a new rake task `plugin:checkout_compatible_all` and `plugin:checkout_compatible[plugin-name]` that check out compatible plugin versions. Supports a .discourse-compatibility file in the root of plugins and themes that list out a plugin's compatibility with certain discourse versions: eg: .discourse-compatibility ``` 2.5.0.beta6: some-git-hash 2.4.4.beta4: some-git-tag 2.2.0: git-reference ``` This ensures older Discourse installs are able to find and install older versions of plugins without intervention, through the manifest only. It iterates through the versions in descending order. If the current Discourse version matches an item in the manifest, it checks out the listed plugin target. If the Discourse version is greater than an item in the manifest, it checks out the next highest version listed in the manifest. If no versions match, it makes no change. --- lib/tasks/plugin.rake | 39 ++++++++++++++++++++++ lib/theme_store/git_importer.rb | 6 ++++ lib/version.rb | 38 ++++++++++++++++++++++ spec/components/version_spec.rb | 57 +++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index 9da1f433506..a5d26368b21 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -88,6 +88,45 @@ task 'plugin:update', :plugin do |t, args| abort('Unable to pull latest version of plugin') unless update_status end +desc 'pull compatible plugin versions for all plugins' +task 'plugin:pull_compatible_all' do |t| + # Loop through each directory + plugins = Dir.glob(File.expand_path('plugins/*')).select { |f| File.directory? f } + # run plugin:pull_compatible + plugins.each do |plugin| + next unless File.directory?(plugin + "/.git") + Rake::Task['plugin:pull_compatible'].invoke(plugin) + Rake::Task['plugin:pull_compatible'].reenable + end +end + +desc 'pull a compatible plugin version' +task 'plugin:pull_compatible', :plugin do |t, args| + + plugin = ENV['PLUGIN'] || ENV['plugin'] || args[:plugin] + plugin_path = plugin + plugin = File.basename(plugin) + + unless File.directory?(plugin_path) + if File.directory?('plugins/' + plugin) + plugin_path = File.expand_path('plugins/' + plugin) + else + abort('Plugin ' + plugin + ' not found') + end + end + + checkout_version = Discourse.find_compatible_git_resource(plugin_path) + + # Checkout value of the version compat + if checkout_version + puts "checking out compatible #{plugin} version: #{checkout_version}" + update_status = system("git -C '#{plugin_path}' reset --hard #{checkout_version}") + abort('Unable to checkout a compatible plugin version') unless update_status + else + puts "#{plugin} is already at latest compatible version" + end +end + desc 'install all plugin gems' task 'plugin:install_all_gems' do |t| plugins = Dir.glob(File.expand_path('plugins/*')).select { |f| File.directory? f } diff --git a/lib/theme_store/git_importer.rb b/lib/theme_store/git_importer.rb index c73d44f8070..2419464db82 100644 --- a/lib/theme_store/git_importer.rb +++ b/lib/theme_store/git_importer.rb @@ -23,6 +23,12 @@ class ThemeStore::GitImporter else import_public! end + if version = Discourse.find_compatible_git_resource(@temp_folder) + Discourse::Utils.execute_command(chdir: @temp_folder) do |runner| + Rails.logger.warn "git reset --hard #{version}" + return runner.exec("git", "reset", "--hard", version) + end + end end def diff_local_changes(remote_theme_id) diff --git a/lib/version.rb b/lib/version.rb index c577055ff52..66bbd35ce57 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -3,6 +3,8 @@ module Discourse VERSION_REGEXP = /\A\d+\.\d+\.\d+(\.beta\d+)?\z/ unless defined? ::Discourse::VERSION_REGEXP + VERSION_COMPATIBILITY_FILENAME = ".discourse-compatibility" + # work around reloader unless defined? ::Discourse::VERSION module VERSION #:nodoc: @@ -18,4 +20,40 @@ module Discourse def self.has_needed_version?(current, needed) Gem::Version.new(current) >= Gem::Version.new(needed) end + + # lookup an external resource (theme/plugin)'s best compatible version + # compatible resource files are YAML, in the format: + # `discourse_version: plugin/theme git reference.` For example: + # 2.5.0.beta6: c4a6c17 + # 2.5.0.beta4: d1d2d3f + # 2.5.0.beta2: bbffee + # 2.4.4.beta6: some-other-branch-ref + # 2.4.2.beta1: v1-tag + def self.find_compatible_resource(version_list) + + return unless version_list + + version_list = YAML.load(version_list).sort_by { |version, pin| Gem::Version.new(version) }.reverse + + # If plugin compat version is listed as less than current Discourse version, take the version/hash listed before. + checkout_version = nil + version_list.each do |core_compat, target| + if Gem::Version.new(core_compat) == Gem::Version.new(::Discourse::VERSION::STRING) # Exact version match - return it + checkout_version = target + break + elsif Gem::Version.new(core_compat) < Gem::Version.new(::Discourse::VERSION::STRING) # Core is on a higher version than listed, use a later version + break + end + checkout_version = target + end + + checkout_version + end + + # Find a compatible resource from a git repo + def self.find_compatible_git_resource(path) + return unless File.directory?("#{path}/.git") + compat_resource, std_error, s = Open3.capture3("git -C '#{path}' show HEAD@{upstream}:#{Discourse::VERSION_COMPATIBILITY_FILENAME}") + Discourse.find_compatible_resource(compat_resource) if s.success? + end end diff --git a/spec/components/version_spec.rb b/spec/components/version_spec.rb index 255ef64c1b0..e2dc8a58e1e 100644 --- a/spec/components/version_spec.rb +++ b/spec/components/version_spec.rb @@ -46,4 +46,61 @@ describe Discourse::VERSION do end end + + context "compatible_resource" do + after do + # Cleanup versions + ::Discourse::VERSION::STRING = [::Discourse::VERSION::MAJOR, ::Discourse::VERSION::MINOR, ::Discourse::VERSION::TINY, ::Discourse::VERSION::PRE].compact.join('.') + end + + shared_examples "test compatible resource" do + it "returns nil when the current version is above all pinned versions" do + ::Discourse::VERSION::STRING = "2.6.0" + expect(Discourse.find_compatible_resource(version_list)).to be_nil + end + + it "returns the correct version if matches exactly" do + ::Discourse::VERSION::STRING = "2.5.0.beta4" + expect(Discourse.find_compatible_resource(version_list)).to eq("twofivebetafour") + end + + it "returns the closest matching version" do + ::Discourse::VERSION::STRING = "2.4.6.beta12" + expect(Discourse.find_compatible_resource(version_list)).to eq("twofivebetatwo") + end + + it "returns the lowest version possible when using an older version" do + ::Discourse::VERSION::STRING = "1.4.6.beta12" + expect(Discourse.find_compatible_resource(version_list)).to eq("twofourtwobetaone") + end + end + + it "returns nil when nil" do + expect(Discourse.find_compatible_resource(nil)).to be_nil + end + + context "with a regular compatible list" do + let(:version_list) { <<~VERSION_LIST + 2.5.0.beta6: twofivebetasix + 2.5.0.beta4: twofivebetafour + 2.5.0.beta2: twofivebetatwo + 2.4.4.beta6: twofourfourbetasix + 2.4.2.beta1: twofourtwobetaone + VERSION_LIST + } + include_examples "test compatible resource" + end + + context "handle a compatible resource out of order" do + let(:version_list) { <<~VERSION_LIST + 2.4.2.beta1: twofourtwobetaone + 2.5.0.beta4: twofivebetafour + 2.5.0.beta6: twofivebetasix + 2.5.0.beta2: twofivebetatwo + 2.4.4.beta6: twofourfourbetasix + VERSION_LIST + } + include_examples "test compatible resource" + end + end end