Last Updated: September 30, 2022
·
17.17K
· athal7

Only precompile assets when necessary (Rails 4, Capistrano 3)

The problem

rake assets:precompile can take up a large chunk of time of Rails application deploys with Capistano, even on deploys where the assets haven't changed!

Other solutions to skip unnecessary asset precompilation seem to rely on Capistrano 2.

The solution

Make these changes in your deploy.rb file:

Step 1: Set the locations to look for relevant changes to trigger a precompilation

set :assets_dependencies, %w(app/assets lib/assets vendor/assets Gemfile.lock config/routes.rb)

Step 2: Clear the previous deploy precompile task

Rake::Task["deploy:assets:precompile"].clear_actions

Step 3: Define the new deploy precompile task

3a: Define a custom error that we can use to have multiple exit points to for an asset precompile

class PrecompileRequired < StandardError; end

3b: Precompile if it is the first deploy

latest_release = capture(:ls, '-xr', releases_path).split[1]
raise PrecompileRequired unless latest_release

3c: Precompile if the previous deploy failed to finish asset precompilation

latest_release_path = releases_path.join(latest_release)
execute(:ls, latest_release_path.join('assets_manifest_backup')) rescue raise(PrecompileRequired)

3d: Precompile if there is a diff in the asset_dependencies set in step 1 as compared to the previous deploy

fetch(:assets_dependencies).each do |dep|
   execute(:diff, '-Naur', release_path.join(dep), latest_release_path.join(dep)) rescue raise(PrecompileRequired)
end

3e: If none of the above conditions are met, copy assets from the previous deploy

execute(:cp, '-r', latest_release_path.join('public', fetch(:assets_prefix)), release_path.join('public', fetch(:assets_prefix)))

3f: If a precompile is required, do it!

rescue PrecompileRequired
  execute(:rake, "assets:precompile") 
end

All together now:

# set the locations that we will look for changed assets to determine whether to precompile
set :assets_dependencies, %w(app/assets lib/assets vendor/assets Gemfile.lock config/routes.rb)

# clear the previous precompile task
Rake::Task["deploy:assets:precompile"].clear_actions
class PrecompileRequired < StandardError; end

namespace :deploy do
  namespace :assets do
    desc "Precompile assets"
    task :precompile do
      on roles(fetch(:assets_roles)) do
        within release_path do
          with rails_env: fetch(:rails_env) do
            begin
              # find the most recent release
              latest_release = capture(:ls, '-xr', releases_path).split[1]

              # precompile if this is the first deploy
              raise PrecompileRequired unless latest_release

              latest_release_path = releases_path.join(latest_release)

              # precompile if the previous deploy failed to finish precompiling
              execute(:ls, latest_release_path.join('assets_manifest_backup')) rescue raise(PrecompileRequired)

              fetch(:assets_dependencies).each do |dep|
                # execute raises if there is a diff
                execute(:diff, '-Naur', release_path.join(dep), latest_release_path.join(dep)) rescue raise(PrecompileRequired)
              end

              info("Skipping asset precompile, no asset diff found")

              # copy over all of the assets from the last release
              execute(:cp, '-r', latest_release_path.join('public', fetch(:assets_prefix)), release_path.join('public', fetch(:assets_prefix)))
            rescue PrecompileRequired
              execute(:rake, "assets:precompile") 
            end
          end
        end
      end
    end
  end
end

10 Responses
Add your response

Great post. This has really improved the speed of my deployment scripts, thank you. Hopefully this will be added to the capistrano rails gem as a default behaviour.

over 1 year ago ·

Would you mind if I turned this into a gem? I'm one of the contributors to https://github.com/capistrano-plugins/

over 1 year ago ·

@rhomeister not at all! please do send the link once you've done so so I can use it :)

over 1 year ago ·

thank you!

over 1 year ago ·

Thanks for @athal7 's great post and @rhomeister 's work, them resolve my problem!

over 1 year ago ·

Great JObs. @rhomeister and @athal7

over 1 year ago ·

Cool, @athal7 will this work with capistrano version 3.0.1 ? I see in @rhomeister's gem that it works only from 3.1.0 version

over 1 year ago ·

@yozzz sorry for the delay, had some coderwall issues. i would defer to @rhomeister's determination, I think he has spent more time with it.

over 1 year ago ·

hi @yozzz. Not sure if it would work with Capistrano 3.0.1. I'm using 3.2 at the moment. You could fork the gem and give it a try.

over 1 year ago ·