hone.heroku.com

Guide to Heroku Plugins

categories: heroku

09 Jan 2010

As of heroku gem 1.5, heroku introduced a plugin system to their gem. The significance of this is that now you can push your own features to the gem without waiting for the heroku team to approve it and provide a way for others to get those changes. I had three features that didn’t make it into the heroku gem which are now pluginified and can be found here:

Usage

Installing a gem is quite simple. You just need to get the git url of the gem and run the install command. For example to install the colorize console plugin above, just do

heroku plugins:install git://github.com/hone/heroku_colorize_console.git

This will clone the git repo into your ~/.heroku/plugins directory and gets automatically loaded when running the heroku client now. There currently is no way to disable certain gems that are already installed. Also errors while loading/running the plugin are not cleanly handled by the heroku gem yet.

Rolling your own

Pedro based the heroku plugin system off of rails plugins. The file structure is similar:

|-- lib/
    |-- plugin files...
|-- README
|-- init.rb

The init.rb is the file that gets loaded when the heroku gem gets run. This file loads the rest of the plugin. For example in my colorize console plugin, my init.rb looks like this:

begin
  require 'wirble'
rescue LoadError
  raise "wirble gem is missing. Please install wirble: gem install wirble"
end
require File.dirname(__FILE__) + '/lib/heroku_colorize_console'

When ovewriting an existing heroku gem method, tread carefully. Try not to copy and paste the existing method into your plugin and then edit it there. If possible use the alias_method_chain pattern from rails. This is how I handle ovewriting Heroku::Command::Base#display

def display_with_colorize(msg, newline=true)
  msg = Wirble::Colorize.colorize(msg) if @options and @options[:colorize]
  display_without_colorize(msg, newline)
end

alias_method :display_without_colorize, :display
alias_method :display, :display_with_colorize

New Commandments!

When adding new commands into the heroku gem, namespace your plugin and inherit from Heroku::Command::BaseWithApp:

module Heroku::Command
  class PluginName < BaseWithApp
    # insert code here...
  end
end

You can then go ahead and add in all the methods that correspond to your command. The call heroku plugins:install git://some_git_url.git would be defined something like this:

module Heroku::Command
  class Plugins < BaseWithApp
    def install
      process_git_url(@args[0])
    end

    private
    def process_git_url(url)
      # ...
    end
  end
end

The command line is parsed as follows: <namespace class>:<method> arg1, arg2, arg3.... Methods don’t take any arguments because the gem bundles them into the @args instance variable. You can treat it like ARGV when doing command line ruby stuff.

If you just want to do a single command, then you can define an index method in your class and call it as if it was defined in Heroku::Command::App. The switch command is called this way: heroku switch <some app name>. This is how it’s written:

module Heroku::Command
  class Switch < BaseWithApp
    DEFAULT_HEROKU_REMOTE_NAME = "heroku"
 
    # switches which heroku app to use (sets it as the default)
    def index
      unless @args.size > 0
        raise(CommandFailed, "Need to specify heroku app to switch to")
      end
 
      git_repo = ::Git.open(Dir.pwd)
      git_repo.remotes.each do |remote|
        if remote.url.split(':').first == default_git_remote_host
          begin
            remote.remove
            # remove command isn't working for ruby-git 1.2.5
          rescue Git::GitExecuteError
            shell("git remote rm #{remote.name}") end
        end
      end
 
      remote_location = default_git_remote_path(@args.first)
      git_repo.add_remote(DEFAULT_HEROKU_REMOTE_NAME, remote_location)
      display("Switched heroku remote to #{git_repo.remote(DEFAULT_HEROKU_REMOTE_NAME).url}")
    end
 
    def default_git_remote_path(name)
      "#{default_git_remote_host}:#{name}.git"
    end
 
    def default_git_remote_host
      "git@#{heroku.host}"
    end
  end
end

Finally, don’t forget to add an entry into the help system, so people know how to use your new fangled command. ddollar has rewritten the old heroku help system in heroku gem 1.5.3. It’s dead simple to add in help text. For example my switch command uses this:

Heroku::Command::Help.group('Switch Command') do |group|
  group.command('switch <appname>', 'set as the default heroku remote, to run commands against by default')
end

which returns this when running heroku help:

=== Switch Command

switch <appname>             # set as the default heroku remote, to run commands against by default

Caveats

Versioning and dependency issues are still yet to be sorted/handled by the heroku gem. I think using something like git tags in conjunction with versionomy or switching over to rubygems would be a good solution to those issues. Lastly, I need to bring over my tests into these plugins and layout a good way to test heroku plugins.


blog comments powered by Disqus