mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 05:53:38 +08:00
f6fadd7129
* FEATURE: bring plugin:create to core from `gem exec create-discourse-plugin` * DEV: remove plugin_rake spec and updated plugin rake creation with `begin/rescue` block
419 lines
13 KiB
Ruby
419 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
directory "plugins"
|
|
|
|
desc "install all official plugins (use GIT_WRITE=1 to pull with write access)"
|
|
task "plugin:install_all_official" do
|
|
skip = Set.new(%w[customer-flair poll])
|
|
|
|
STDERR.puts "Allowing write to all repos!" if ENV["GIT_WRITE"]
|
|
|
|
promises = []
|
|
failures = []
|
|
|
|
Plugin::Metadata::OFFICIAL_PLUGINS.each do |name|
|
|
next if skip.include? name
|
|
|
|
repo = "https://github.com/discourse/#{name}"
|
|
dir = repo.split("/").last
|
|
path = File.expand_path("plugins/" + dir)
|
|
|
|
if Dir.exist? path
|
|
STDOUT.puts "Skipping #{dir} cause it already exists!"
|
|
next
|
|
end
|
|
|
|
if ENV["GIT_WRITE"]
|
|
repo = repo.gsub("https://github.com/", "git@github.com:")
|
|
repo += ".git"
|
|
end
|
|
|
|
promises << Concurrent::Promise.execute do
|
|
attempts ||= 1
|
|
STDOUT.puts("Cloning '#{repo}' to '#{path}'...")
|
|
system("git clone --quiet #{repo} #{path}", exception: true)
|
|
rescue StandardError
|
|
if attempts == 3
|
|
failures << repo
|
|
abort
|
|
end
|
|
|
|
STDOUT.puts "Failed to clone #{repo}... trying again..."
|
|
attempts += 1
|
|
retry
|
|
end
|
|
end
|
|
|
|
Concurrent::Promise.zip(*promises).value!
|
|
failures.each { |repo| STDOUT.puts "Failed to clone #{repo}" } if failures.present?
|
|
end
|
|
|
|
desc "install plugin"
|
|
task "plugin:install", :repo do |t, args|
|
|
repo = ENV["REPO"] || ENV["repo"] || args[:repo]
|
|
name = ENV["NAME"] || ENV["name"] || File.basename(repo, ".git")
|
|
|
|
plugin_path = File.expand_path("plugins/" + name)
|
|
if File.directory?(File.expand_path(plugin_path))
|
|
abort("Plugin directory, " + plugin_path + ", already exists.")
|
|
end
|
|
|
|
clone_status = system("git clone " + repo + " " + plugin_path)
|
|
unless clone_status
|
|
FileUtils.rm_rf(plugin_path)
|
|
abort("Unable to clone plugin")
|
|
end
|
|
end
|
|
|
|
def update_plugin(plugin)
|
|
plugin = ENV["PLUGIN"] || ENV["plugin"] || 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
|
|
|
|
`git -C '#{plugin_path}' fetch origin --tags --force`
|
|
|
|
upstream_branch =
|
|
`git -C '#{plugin_path}' for-each-ref --format='%(upstream:short)' $(git -C '#{plugin_path}' symbolic-ref -q HEAD)`.strip
|
|
has_origin_main = `git -C '#{plugin_path}' branch -a`.match?(%r{remotes/origin/main\z})
|
|
has_local_main = `git -C '#{plugin_path}' show-ref refs/heads/main`.present?
|
|
|
|
if upstream_branch == "origin/master" && has_origin_main
|
|
puts "Branch has changed to `origin/main`"
|
|
|
|
if has_local_main
|
|
update_status = system("git -C '#{plugin_path}' checkout main")
|
|
abort("Unable to pull latest version of plugin #{plugin_path}") unless update_status
|
|
else
|
|
`git -C '#{plugin_path}' branch -m master main`
|
|
end
|
|
|
|
`git -C '#{plugin_path}' branch -u origin/main main`
|
|
end
|
|
|
|
update_status = system("git -C '#{plugin_path}' pull --quiet --no-rebase")
|
|
abort("Unable to pull latest version of plugin #{plugin_path}") unless update_status
|
|
end
|
|
|
|
desc "update all plugins"
|
|
task "plugin:update_all" do |t|
|
|
# Loop through each directory
|
|
plugins =
|
|
Dir
|
|
.glob(File.expand_path("plugins/*"))
|
|
.select { |f| File.directory?(f) && File.directory?("#{f}/.git") }
|
|
|
|
# run plugin:update
|
|
promises = plugins.map { |plugin| Concurrent::Promise.execute { update_plugin(plugin) } }
|
|
Concurrent::Promise.zip(*promises).value!
|
|
|
|
Rake::Task["plugin:versions"].invoke
|
|
end
|
|
|
|
desc "update a plugin"
|
|
task "plugin:update", :plugin do |t, args|
|
|
update_plugin(args[:plugin])
|
|
end
|
|
|
|
desc "pull compatible plugin versions for all plugins"
|
|
task "plugin:pull_compatible_all" do |t|
|
|
STDERR.puts <<~TEXT if GlobalSetting.load_plugins?
|
|
WARNING: Plugins were activated before running `rake plugin:pull_compatible_all`
|
|
You should prefix this command with LOAD_PLUGINS=0
|
|
TEXT
|
|
|
|
# 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}' cat-file -e #{checkout_version} || git -C '#{plugin_path}' fetch --depth 1 $(git -C '#{plugin_path}' rev-parse --symbolic-full-name @{upstream} | awk -F '/' '{print $3}') #{checkout_version}; 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|
|
|
# Left intentionally blank.
|
|
# When the app is being loaded, all missing gems are installed
|
|
# See: lib/plugin_gem.rb
|
|
puts "Done"
|
|
end
|
|
|
|
desc "install plugin gems"
|
|
task "plugin:install_gems", :plugin do |t, args|
|
|
# Left intentionally blank.
|
|
# When the app is being loaded, all missing gems are installed
|
|
# See: lib/plugin_gem.rb
|
|
puts "Done"
|
|
end
|
|
|
|
def spec(plugin, parallel: false, argv: nil)
|
|
params = []
|
|
params << "--profile" if !parallel
|
|
params << "--fail-fast" if ENV["RSPEC_FAILFAST"]
|
|
params << "--seed #{ENV["RSPEC_SEED"]}" if Integer(ENV["RSPEC_SEED"], exception: false)
|
|
params << argv if argv
|
|
|
|
# reject system specs as they are slow and need dedicated setup
|
|
files =
|
|
Dir.glob("./plugins/#{plugin}/spec/**/*_spec.rb").reject { |f| f.include?("spec/system/") }.sort
|
|
|
|
if files.length > 0
|
|
cmd = parallel ? "bin/turbo_rspec" : "bin/rspec"
|
|
|
|
Rake::FileUtilsExt.verbose(!parallel) do
|
|
sh("LOAD_PLUGINS=1 #{cmd} #{files.join(" ")} #{params.join(" ")}") do |ok, status|
|
|
fail "Spec command failed with status (#{status.exitstatus})" if !ok
|
|
end
|
|
end
|
|
else
|
|
abort "No specs found."
|
|
end
|
|
end
|
|
|
|
desc "run plugin specs"
|
|
task "plugin:spec", %i[plugin argv] do |_, args|
|
|
args.with_defaults(plugin: "*")
|
|
spec(args[:plugin], argv: args[:argv])
|
|
end
|
|
|
|
desc "run plugin specs in parallel"
|
|
task "plugin:turbo_spec", %i[plugin argv] do |_, args|
|
|
args.with_defaults(plugin: "*")
|
|
spec(args[:plugin], parallel: true, argv: args[:argv])
|
|
end
|
|
|
|
desc "run plugin qunit tests"
|
|
task "plugin:qunit", %i[plugin timeout] do |t, args|
|
|
args.with_defaults(plugin: "*")
|
|
|
|
rake = "#{Rails.root}/bin/rake"
|
|
|
|
cmd = "LOAD_PLUGINS=1 "
|
|
|
|
target =
|
|
if args[:plugin] == "*"
|
|
puts "Running qunit tests for all plugins"
|
|
"plugins"
|
|
else
|
|
puts "Running qunit tests for #{args[:plugin]}"
|
|
args[:plugin]
|
|
end
|
|
|
|
cmd += "TARGET='#{target}' "
|
|
|
|
cmd += "#{rake} qunit:test"
|
|
cmd += "[#{args[:timeout]}]" if args[:timeout]
|
|
|
|
system cmd
|
|
exit $?.exitstatus
|
|
end
|
|
|
|
desc "run all migrations of a plugin"
|
|
namespace "plugin:migrate" do
|
|
def list_migrations(plugin_name)
|
|
plugin_root = File.join(Rails.root, "plugins", plugin_name)
|
|
migrations_root = File.join(plugin_root, "db", "{post_migrate,migrate}", "*.rb")
|
|
Dir[migrations_root]
|
|
.map { |migration_filename| File.basename(migration_filename)[/(^.*?)_/, 1].to_i }
|
|
.sort
|
|
end
|
|
|
|
def cmd(operation, migration_number)
|
|
"rails db:migrate:#{operation} LOAD_PLUGINS=1 VERSION=#{migration_number}"
|
|
end
|
|
|
|
task :down, [:plugin] do |t, args|
|
|
list_migrations(args[:plugin]).reverse.each do |migration_number|
|
|
sh cmd(:down, migration_number)
|
|
end
|
|
end
|
|
|
|
task :up, [:plugin] do |t, args|
|
|
list_migrations(args[:plugin]).each { |migration_number| sh cmd(:up, migration_number) }
|
|
end
|
|
end
|
|
|
|
desc "display all plugin versions"
|
|
task "plugin:versions" do |t, args|
|
|
versions =
|
|
Dir
|
|
.glob("*", base: "plugins")
|
|
.map { |plugin| [plugin, "plugins/#{plugin}", "plugins/#{plugin}/.git"] }
|
|
.select do |plugin, plugin_dir, plugin_git_dir|
|
|
File.directory?(plugin_dir) && File.exist?(plugin_git_dir)
|
|
end
|
|
.sort_by { |plugin, _, _| plugin }
|
|
.map do |plugin, _, plugin_git_dir|
|
|
version = `git --git-dir \"#{plugin_git_dir}\" rev-parse HEAD`
|
|
abort("unable to get #{plugin} version") unless version
|
|
[plugin, version.strip[0...8]]
|
|
end
|
|
.to_h
|
|
|
|
puts JSON.pretty_generate(versions)
|
|
end
|
|
|
|
desc "create a new plugin based on template"
|
|
task "plugin:create", [:name] do |t, args|
|
|
class StringHelpers
|
|
def self.to_snake_case(string)
|
|
return string if string.match?(/\A[a-z0-9_]+\z/)
|
|
string.dup.gsub!("-", "_")
|
|
end
|
|
|
|
def self.to_pascal_case(string)
|
|
return string if string.match?(/\A[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*\z/)
|
|
string.dup.split("-").map(&:capitalize).join
|
|
end
|
|
|
|
def self.to_pascal_spaced_case(string)
|
|
return string if string.match?(/\A[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*\z/)
|
|
string.dup.split("-").map(&:capitalize).join(" ")
|
|
end
|
|
|
|
def self.is_in_kebab_case?(string)
|
|
string.match?(/\A[a-z0-9]+(-[a-z0-9]+)*\z/)
|
|
end
|
|
end
|
|
|
|
plugin_name = args[:name]
|
|
|
|
abort("Supply a name for the plugin") if plugin_name.blank?
|
|
abort("Name must be in kebab-case") unless StringHelpers.is_in_kebab_case?(plugin_name)
|
|
|
|
plugin_path = File.expand_path("plugins/" + plugin_name)
|
|
|
|
abort("Plugin directory, " + plugin_path + ", already exists.") if File.directory?(plugin_path)
|
|
|
|
failures = []
|
|
repo = "https://github.com/discourse/discourse-plugin-skeleton"
|
|
begin
|
|
attempts ||= 1
|
|
STDOUT.puts("Cloning '#{repo}' to '#{plugin_path}'...")
|
|
system("git clone --quiet #{repo} #{plugin_path}", exception: true)
|
|
rescue StandardError
|
|
if attempts == 3
|
|
failures << repo
|
|
abort
|
|
end
|
|
|
|
STDOUT.puts "Failed to clone #{repo}... trying again..."
|
|
attempts += 1
|
|
retry
|
|
end
|
|
failures.each { |repo| STDOUT.puts "Failed to clone #{repo}" } if failures.present?
|
|
|
|
Dir.chdir(plugin_path) do # rubocop:disable Discourse/NoChdir
|
|
puts "Initializing git repository..."
|
|
|
|
FileUtils.rm_rf("#{plugin_path}/.git")
|
|
FileUtils.rm_rf(Dir.glob("#{plugin_path}/**/.gitkeep"))
|
|
system "git", "init", exception: true
|
|
system "git", "symbolic-ref", "HEAD", "refs/heads/main", exception: true
|
|
root_files = Dir.glob("*").select { |f| File.file?(f) }
|
|
system "git", "add", *root_files, exception: true
|
|
system "git", "add", ".", exception: true
|
|
system "git", "commit", "-m", "Initial commit", "--quiet", exception: true
|
|
end
|
|
|
|
puts "🚂 Renaming directories..."
|
|
|
|
File.rename File.expand_path("plugins/#{plugin_name}/lib/my_plugin_module"),
|
|
File.expand_path(
|
|
"plugins/#{plugin_name}/lib/#{StringHelpers.to_snake_case(plugin_name)}",
|
|
)
|
|
|
|
File.rename File.expand_path("plugins/#{plugin_name}/app/controllers/my_plugin_module"),
|
|
File.expand_path(
|
|
"plugins/#{plugin_name}/app/controllers/#{StringHelpers.to_snake_case(plugin_name)}",
|
|
)
|
|
|
|
to_update_files = # assume all start with ./#{plugin_name}/
|
|
[
|
|
"app/controllers/#{StringHelpers.to_snake_case(plugin_name)}/examples_controller.rb",
|
|
"config/locales/client.en.yml",
|
|
"config/routes.rb",
|
|
"config/settings.yml",
|
|
"lib/#{StringHelpers.to_snake_case(plugin_name)}/engine.rb",
|
|
"plugin.rb",
|
|
"README.md",
|
|
]
|
|
|
|
to_update_files.each do |file|
|
|
puts "🚂 Updating #{file}..."
|
|
|
|
updated_file = []
|
|
File.foreach("plugins/#{plugin_name}/#{file}") do |line|
|
|
updated_file << line
|
|
.gsub("MyPluginModule", StringHelpers.to_pascal_case(plugin_name))
|
|
.gsub("my_plugin_module", StringHelpers.to_snake_case(plugin_name))
|
|
.gsub("my-plugin", plugin_name)
|
|
.gsub("discourse-plugin-name", plugin_name)
|
|
.gsub("TODO_plugin_name", StringHelpers.to_snake_case(plugin_name))
|
|
.gsub("plugin_name_enabled", "#{StringHelpers.to_snake_case(plugin_name)}_enabled")
|
|
.gsub("discourse_plugin_name", StringHelpers.to_snake_case(plugin_name))
|
|
.gsub("Plugin Name", StringHelpers.to_pascal_spaced_case(plugin_name))
|
|
.gsub(
|
|
"lib/my_plugin_module/engine",
|
|
"lib/#{StringHelpers.to_snake_case(plugin_name)}/engine",
|
|
)
|
|
end
|
|
|
|
File.open("plugins/#{plugin_name}/#{file}", "w") { |f| f.write(updated_file.join("")) }
|
|
end
|
|
|
|
Dir.chdir(plugin_path) do # rubocop:disable Discourse/NoChdir
|
|
puts "Committing changes..."
|
|
system "git", "add", ".", exception: true
|
|
system "git",
|
|
"commit",
|
|
"-m",
|
|
"Update plugin skeleton with plugin name",
|
|
"--quiet",
|
|
exception: true
|
|
end
|
|
|
|
puts "Done! 🎉"
|
|
|
|
puts "Do not forget to update the README.md and plugin.rb file with the plugin description and the url of the plugin."
|
|
puts "You are ready to start developing your plugin! 🚀"
|
|
end
|