109

I already have a deploy.rb that can deploy my app on my production server.

My app contains a custom rake task (a .rake file in the lib/tasks directory).

I'd like to create a cap task that will remotely run that rake task.

Richard Poirier
  • 4,878
  • 5
  • 26
  • 21
  • 2
    Can someone explain the pros/cons of using capistrano's own `#{rake}` variable? Seems it's not always the best option. – lulalala Nov 07 '12 at 03:13

17 Answers17

61

A little bit more explicit, in your \config\deploy.rb, add outside any task or namespace:

namespace :rake do  
  desc "Run a task on a remote server."  
  # run like: cap staging rake:invoke task=a_certain_task  
  task :invoke do  
    run("cd #{deploy_to}/current; /usr/bin/env rake #{ENV['task']} RAILS_ENV=#{rails_env}")  
  end  
end

Then, from /rails_root/, you can run:

cap staging rake:invoke task=rebuild_table_abc
Arturo Herrero
  • 12,772
  • 11
  • 42
  • 73
Coward
  • 619
  • 5
  • 2
48

Capistrano 3 Generic Version (run any rake task)

Building a generic version of Mirek Rusin's answer:

desc 'Invoke a rake command on the remote server'
task :invoke, [:command] => 'deploy:set_rails_env' do |task, args|
  on primary(:app) do
    within current_path do
      with :rails_env => fetch(:rails_env) do
        rake args[:command]
      end
    end
  end
end

Example usage: cap staging "invoke[db:migrate]"

Note that deploy:set_rails_env requires comes from the capistrano-rails gem

Abram
  • 39,950
  • 26
  • 134
  • 184
marinosb
  • 5,367
  • 2
  • 18
  • 10
  • 1
    This only supports a single argument, if you replace `rake args[:command]` with `execute :rake, "#{args.command}[#{args.extras.join(",")}]"` you can execute a task with multiple arguments like so: `cap production invoke["task","arg1","arg2"]` – Robin Clowers Sep 24 '14 at 01:19
  • 1
    @[Robin Clowers](http://stackoverflow.com/users/69047/robin-clowers) You can pass multiple arguments, e.g. `cap staging invoke['task[arg1\,arg2]']`. I prefer this approach to the one you mention because it mirrors the actual invocation of rake. With this approach you can also chain multiple tasks, which is often useful: `cap staging invoke['task1 task2[arg1] task3[arg2\,arg3]']`. Works for rake 10.2.0 or newer – marinosb Oct 15 '14 at 18:41
  • this is great - i'd like to note, you need to include :app as one of your server roles. – random-forest-cat Oct 31 '15 at 22:01
  • Apparently this needed to be "invoke[db:migrate]" ... Correction made. – Abram Dec 23 '15 at 06:18
  • @Abram with the command you suggested I get "Don't know how to build task 'invoke" – dc10 Dec 25 '15 at 11:09
  • You mean with quotes? So it works for you without quotes only @dc10 – Abram Dec 25 '15 at 14:41
  • @Abram it does not work for me at all like this cap production "invoke[load_data]" the task itself is declared inside the namespace deploy – dc10 Dec 25 '15 at 14:49
  • Gotcha. Well, check out my provided answer below, which worked for me. – Abram Dec 26 '15 at 01:04
  • Note that `current_path` won't work on first run as it won't have been created yet - use `release_path` instead if you plan to include that rake task in your first run – Paul Odeon Oct 10 '19 at 10:32
44

...couple of years later...

Have a look at capistrano's rails plugin, you can see at https://github.com/capistrano/rails/blob/master/lib/capistrano/tasks/migrations.rake#L5-L14 it can look something like:

desc 'Runs rake db:migrate if migrations are set'
task :migrate => [:set_rails_env] do
  on primary fetch(:migration_role) do
    within release_path do
      with rails_env: fetch(:rails_env) do
        execute :rake, "db:migrate"
      end
    end
  end
end
Mirek Rusin
  • 18,820
  • 3
  • 43
  • 36
41
run("cd #{deploy_to}/current && /usr/bin/env rake `<task_name>` RAILS_ENV=production")

Found it with Google -- http://ananelson.com/said/on/2007/12/30/remote-rake-tasks-with-capistrano/

The RAILS_ENV=production was a gotcha -- I didn't think of it at first and couldn't figure out why the task wasn't doing anything.

James Chen
  • 10,794
  • 1
  • 41
  • 38
Richard Poirier
  • 4,878
  • 5
  • 26
  • 21
  • 2
    A minor improvement: if you replace the semicolon with && then the second statement (running the rake task) will not run if the first statement (changing the directory) fails. – Teflon Ted May 12 '09 at 15:17
  • 2
    This won't work if you are deploying to multiple servers. It will run the rake task multiple times. – Mark Redding Jun 03 '11 at 20:36
  • 4
    one should really respect capistrano's rake setting `"cd #{deploy_to}/current && #{rake} RAILS_ENV=production"` – kares Jun 14 '11 at 11:13
  • @Mark Redding: Could you put one of the servers in its own role for rake tasks and restrict your capistrano task to only run on servers with that role? – mj1531 Jun 24 '11 at 15:25
  • I did something where I created a task in my deploy.rb. That task has a :roles => :db on it such that it will only execute on the same server which i defined as my primary for db:migrate. – Mark Redding Nov 26 '11 at 20:38
20

Use Capistrano-style rake invocations

There's a common way that'll "just work" with require 'bundler/capistrano' and other extensions that modify rake. This will also work with pre-production environments if you're using multistage. The gist? Use config vars if you can.

desc "Run the super-awesome rake task"
task :super_awesome do
  rake = fetch(:rake, 'rake')
  rails_env = fetch(:rails_env, 'production')

  run "cd '#{current_path}' && #{rake} super_awesome RAILS_ENV=#{rails_env}"
end
captainpete
  • 6,162
  • 3
  • 28
  • 26
  • 2
    This is the nicest solution, uses the capistrano values where available – loopj Jul 14 '12 at 23:28
  • 2
    Probably worth adding that if your task is namespaced (i.e. defined not in the top level namespace) you might have to use `top.run` instead of just `run` – Ev Dolzhenko Oct 02 '13 at 14:02
  • Thanks @dolzenko. Just found the [docs for the `top` method](https://github.com/capistrano/capistrano/wiki/2.x-DSL-Configuration-Tasks-Top). In the case where we've defined `run` in the same namespace, `top.run` is required, otherwise it should still find the top-level `run` even where the task is namespaced. Have I missed something? What happened in your case? – captainpete Oct 03 '13 at 01:34
  • 1
    I clearly didn't have any run method defined in the same namespace, so not sure why I needed that. In any case Capistrano 2.0 is a history and the next version is Rake based (making things more predictable hopefully) – Ev Dolzhenko Oct 30 '13 at 08:42
17

Use the capistrano-rake gem

Just install the gem without messing with custom capistrano recipes and execute desired rake tasks on remote servers like this:

cap production invoke:rake TASK=my:rake_task

Full Disclosure: I wrote it

Sheharyar
  • 73,588
  • 21
  • 168
  • 215
7

I personally use in production a helper method like this:

def run_rake(task, options={}, &block)
  command = "cd #{latest_release} && /usr/bin/env bundle exec rake #{task}"
  run(command, options, &block)
end

That allows to run rake task similar to using the run (command) method.


NOTE: It is similar to what Duke proposed, but I:

  • use latest_release instead of current_release - from my experience it is more what you expect when running a rake command;
  • follow the naming convention of Rake and Capistrano (instead of: cmd -> task and rake -> run_rake)
  • don't set RAILS_ENV=#{rails_env} because the right place to set it is the default_run_options variable. E.g default_run_options[:env] = {'RAILS_ENV' => 'production'} # -> DRY!
Community
  • 1
  • 1
Szymon Jeż
  • 8,273
  • 4
  • 42
  • 60
5

There's an interesting gem cape that makes your rake tasks available as Capistrano tasks, so you can run them remotely. cape is well documented, but here's a short overview on how to set i up.

After installing the gem, just add this to your config/deploy.rb file.

# config/deploy.rb
require 'cape'
Cape do
  # Create Capistrano recipes for all Rake tasks.
  mirror_rake_tasks
end

Now, you can run all you rake tasks locally or remotely through cap.

As an added bonus, cape lets you set how you want to run your rake task locally and remotely (no more bundle exec rake), just add this to your config/deploy.rb file:

# Configure Cape to execute Rake via Bundler, both locally and remotely.
Cape.local_rake_executable  = '/usr/bin/env bundle exec rake'
Cape.remote_rake_executable = '/usr/bin/env bundle exec rake'
Yacc
  • 1,068
  • 11
  • 16
3
namespace :rake_task do
  task :invoke do
    if ENV['COMMAND'].to_s.strip == ''
      puts "USAGE: cap rake_task:invoke COMMAND='db:migrate'" 
    else
      run "cd #{current_path} && RAILS_ENV=production rake #{ENV['COMMAND']}"
    end
  end                           
end 
Darme
  • 6,984
  • 5
  • 37
  • 52
  • 1
    Good. Changing it from `RAILS_ENV=production` to `RAILS_ENV=#{rails_env}` allows it to work on my staging server as well. – evanrmurphy Mar 20 '13 at 21:33
3

This worked for me:

task :invoke, :command do |task, args|
  on roles(:app) do
    within current_path do
      with rails_env: fetch(:rails_env) do
        execute :rake, args[:command]
      end
    end
  end
end

Then simply run cap production "invoke[task_name]"

Abram
  • 39,950
  • 26
  • 134
  • 184
2

Here's what I put in my deploy.rb to simplify running rake tasks. It's a simple wrapper around capistrano's run() method.

def rake(cmd, options={}, &block)
  command = "cd #{current_release} && /usr/bin/env bundle exec rake #{cmd} RAILS_ENV=#{rails_env}"
  run(command, options, &block)
end

Then I just run any rake task like so:

rake 'app:compile:jammit'
Duke
  • 7,070
  • 3
  • 38
  • 28
  • this conflicts as capistrano defines it's own rake variable (used to determine which rake to use) and thus breaks built in receipies for instance the one which precompiles assets – Michael Oct 17 '12 at 11:57
2

You can use this:

namespace :rails_staging_task do
  desc "Create custom role"
  task :create_custom_role do
    on roles(:app), in: :sequence, wait: 5 do
      within "#{deploy_to}/current" do
        with rails_env: :staging do
          rake "create_role:my_custom_role"
        end
      end
    end
  end

  # other task here
end

Documentation

yosefbennywidyo
  • 163
  • 4
  • 8
1

Most of it is from above answer with a minor enhancement to run any rake task from capistrano

Run any rake task from capistrano

$ cap rake -s rake_task=$rake_task

# Capfile     
task :rake do
  rake = fetch(:rake, 'rake')
  rails_env = fetch(:rails_env, 'production')

  run "cd '#{current_path}' && #{rake} #{rake_task} RAILS_ENV=#{rails_env}"
end
Community
  • 1
  • 1
Sairam
  • 2,708
  • 1
  • 25
  • 34
1

If you want to be able to pass multiple arguments try this (based on marinosbern's answer):

task :invoke, [:command] => 'deploy:set_rails_env' do |task, args|
  on primary(:app) do
    within current_path do
      with :rails_env => fetch(:rails_env) do
        execute :rake, "#{args.command}[#{args.extras.join(",")}]"
      end
    end
  end
end

Then you can run a task like so: cap production invoke["task","arg1","arg2"]

Robin Clowers
  • 2,150
  • 18
  • 28
1

This also works:

run("cd #{release_path}/current && /usr/bin/rake <rake_task_name>", :env => {'RAILS_ENV' => rails_env})

More info: Capistrano Run

acw
  • 1,093
  • 10
  • 14
1

Previous answers didn't help me and i found this: From http://kenglish.co/run-rake-tasks-on-the-server-with-capistrano-3-and-rbenv/

namespace :deploy do
  # ....
  # @example
  #   bundle exec cap uat deploy:invoke task=users:update_defaults
  desc 'Invoke rake task on the server'
  task :invoke do
    fail 'no task provided' unless ENV['task']

    on roles(:app) do
      within release_path do
        with rails_env: fetch(:rails_env) do
          execute :rake, ENV['task']
        end
      end
    end
  end

end

to run your task use

bundle exec cap uat deploy:invoke task=users:update_defaults

Maybe it will be useful for someone

0

So I have been working on this. it seams to work well. However you need a formater to really take advantage of the code.

If you don't want to use a formatter just set the log level to to debug mode. These semas to h

SSHKit.config.output_verbosity = Logger::DEBUG

Cap Stuff

namespace :invoke do
  desc 'Run a bash task on a remote server. cap environment invoke:bash[\'ls -la\'] '
  task :bash, :execute do |_task, args|
    on roles(:app), in: :sequence do
      SSHKit.config.format = :supersimple
      execute args[:execute]
    end
  end

  desc 'Run a rake task on a remote server. cap environment invoke:rake[\'db:migrate\'] '
  task :rake, :task do |_task, args|
    on primary :app do
      within current_path do
        with rails_env: fetch(:rails_env) do
          SSHKit.config.format = :supersimple
          rake args[:task]
        end
      end
    end
  end
end

This is the formatter I built to work with the code above. It is based off the :textsimple built into the sshkit but it is not a bad way to invoke custom tasks. Oh this many not works with the newest version of sshkit gem. I know it works with 1.7.1. I say this because the master branch has changed the SSHKit::Command methods that are available.

module SSHKit
  module Formatter
    class SuperSimple < SSHKit::Formatter::Abstract
      def write(obj)
        case obj
        when SSHKit::Command    then write_command(obj)
        when SSHKit::LogMessage then write_log_message(obj)
        end
      end
      alias :<< :write

      private

      def write_command(command)
        unless command.started? && SSHKit.config.output_verbosity == Logger::DEBUG
          original_output << "Running #{String(command)} #{command.host.user ? "as #{command.host.user}@" : "on "}#{command.host}\n"
          if SSHKit.config.output_verbosity == Logger::DEBUG
            original_output << "Command: #{command.to_command}" + "\n"
          end
        end

        unless command.stdout.empty?
          command.stdout.lines.each do |line|
            original_output << line
            original_output << "\n" unless line[-1] == "\n"
          end
        end

        unless command.stderr.empty?
          command.stderr.lines.each do |line|
            original_output << line
            original_output << "\n" unless line[-1] == "\n"
          end
        end

      end

      def write_log_message(log_message)
        original_output << log_message.to_s + "\n"
      end
    end
  end
end
User128848244
  • 3,250
  • 3
  • 24
  • 22