From dc59c248a72444a559f86a57d96d2188fd7f2ae4 Mon Sep 17 00:00:00 2001 From: Matt Brictson Date: Sun, 17 Jan 2016 16:46:53 -0800 Subject: [PATCH] Add a Plugin API The Plugin class is a simple set of conventions for defining Capistrano plugins. This is to support the eventual migration of SCM logic out of Capistrano itself and into separate gems as plugins. --- lib/capistrano/plugin.rb | 106 +++++++++++++++++++++++++++++ spec/lib/capistrano/plugin_spec.rb | 82 ++++++++++++++++++++++ spec/support/tasks/plugin.rake | 6 ++ 3 files changed, 194 insertions(+) create mode 100644 lib/capistrano/plugin.rb create mode 100644 spec/lib/capistrano/plugin_spec.rb create mode 100644 spec/support/tasks/plugin.rake diff --git a/lib/capistrano/plugin.rb b/lib/capistrano/plugin.rb new file mode 100644 index 00000000..ab275f0f --- /dev/null +++ b/lib/capistrano/plugin.rb @@ -0,0 +1,106 @@ +require "capistrano/all" +require "capistrano/ext/sshkit/backend/thread_local" +require "rake/tasklib" + +# Base class for Capistrano plugins. Makes building a Capistrano plugin as easy +# as writing a `Capistrano::Plugin` subclass and overriding any or all of its +# three template methods: +# +# * set_defaults +# * register_hooks +# * define_tasks +# +# Within the plugin you can use any methods of the Rake or Capistrano DSLs, like +# `fetch`, `invoke`, etc. In cases when you need to use SSHKit's backend outside +# of an `on` block, use the `backend` convenience method. E.g. `backend.test`, +# `backend.execute`, or `backend.capture`. +# +# Package up and distribute your plugin class as a gem and you're good to go! +# +# To use a plugin, all a user has to do is instantiate it in the Capfile, like +# this: +# +# # Capfile +# require "capistrano/superfancy" +# Capistrano::Superfancy.new +# +# Or, to install the plugin without its hooks: +# +# # Capfile +# require "capistrano/superfancy" +# Capistrano::Superfancy.new(hooks: false) +# +class Capistrano::Plugin < Rake::TaskLib + include Capistrano::DSL + + # Constructing a plugin "installs" it into Capistrano by loading its tasks, + # hooks, and defaults at the appropriate time. The hooks in particular can be + # skipped, if you want full control over when and how the plugin's tasks are + # executed. Simply pass `hooks:false` to opt out. + # + def initialize(hooks:true) + define_tasks + register_hooks if hooks + task "load:defaults" do + set_defaults + end + end + + private + + # Implemented by subclasses to provide default values for settings needed by + # this plugin. Typically done using the `set_if_empty` Capistrano DSL method. + # + # Example: + # + # def set_defaults + # set_if_empty :my_plugin_option, true + # end + # + def set_defaults; end + + # Implemented by subclasses to hook into Capistrano's deployment flow using + # using the `before` and `after` DSL methods. Note that `register_hooks` will + # not be called if the user has opted-out of hooks when installing the plugin. + # + # Example: + # + # def register_hooks + # after "deploy:updated", "my_plugin:do_something" + # end + # + def register_hooks; end + + # Implemented by subclasses to define Rake tasks. Typically a plugin will call + # `eval_rakefile` to load Rake tasks from a separate .rake file. + # + # Example: + # + # def define_tasks + # eval_rakefile File.expand_path("../tasks.rake", __FILE__) + # end + # + # For simple tasks, you can define them inline. No need for a separate file. + # + # def define_tasks + # desc "Do something fantastic." + # task "my_plugin:fantastic" do + # ... + # end + # end + # + def define_tasks; end + + # Read and eval a .rake file in such a way that `self` within the .rake file + # refers to this plugin instance. This gives the tasks in the file access to + # helper methods defined by the plugin. + def eval_rakefile(path) + contents = IO.read(path) + instance_eval(contents, path, 1) + end + + # Convenience to access the current SSHKit backend outside of an `on` block. + def backend + SSHKit::Backend.current + end +end diff --git a/spec/lib/capistrano/plugin_spec.rb b/spec/lib/capistrano/plugin_spec.rb new file mode 100644 index 00000000..e8a6362a --- /dev/null +++ b/spec/lib/capistrano/plugin_spec.rb @@ -0,0 +1,82 @@ +require "spec_helper" +require "capistrano/plugin" + +module Capistrano + describe Plugin do + include Rake::DSL + include Capistrano::DSL + + class DummyPlugin < Capistrano::Plugin + def define_tasks + task :hello do + end + end + + def register_hooks + before "deploy:published", "hello" + end + end + + class ExternalTasksPlugin < Capistrano::Plugin + def define_tasks + eval_rakefile( + File.expand_path("../../../support/tasks/plugin.rake", __FILE__) + ) + end + + # Called from plugin.rake to demonstrate that helper methods work + def hello + set :plugin_result, "hello" + end + end + + before do + # Define an example task to allow testing hooks + task "deploy:published" + end + + after do + # Clean up any tasks or variables we created during the tests + Rake::Task.clear + Capistrano::Configuration.reset! + end + + it "defines tasks when constructed" do + DummyPlugin.new + expect(Rake::Task["hello"]).not_to be_nil + end + + it "registers hooks when constructed" do + DummyPlugin.new + expect(Rake::Task["deploy:published"].prerequisites).to include("hello") + end + + it "skips registering hooks if :hooks => false" do + DummyPlugin.new(:hooks => false) + expect(Rake::Task["deploy:published"].prerequisites).to be_empty + end + + it "doesn't call set_defaults immediately" do + dummy = DummyPlugin.new + dummy.expects(:set_defaults).never + end + + it "calls set_defaults during load:defaults" do + dummy = DummyPlugin.new + dummy.expects(:set_defaults).once + Rake::Task["load:defaults"].invoke + end + + it "is able to load tasks from a .rake file" do + ExternalTasksPlugin.new + Rake::Task["plugin_test"].invoke + expect(fetch(:plugin_result)).to eq("hello") + end + + it "exposes the SSHKit backend to subclasses" do + SSHKit::Backend.expects(:current).returns(:backend) + plugin = DummyPlugin.new + expect(plugin.send(:backend)).to eq(:backend) + end + end +end diff --git a/spec/support/tasks/plugin.rake b/spec/support/tasks/plugin.rake new file mode 100644 index 00000000..3234ebcd --- /dev/null +++ b/spec/support/tasks/plugin.rake @@ -0,0 +1,6 @@ +# This rake file is used by plugin_spec.rb. + +task :plugin_test do + # Example of invoking a helper method provided by the plugin + hello +end