From 23452d6390d2ff59240f71966409850a7646320f Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Sat, 27 Oct 2012 14:38:49 +1300 Subject: [PATCH] Added support for rolling back assets, and removing expired assets --- .../configuration/actions/invocation.rb | 2 +- lib/capistrano/recipes/deploy/assets.rb | 105 +++++++++++++++++- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/capistrano/configuration/actions/invocation.rb b/lib/capistrano/configuration/actions/invocation.rb index d026139c8..9bd5bba99 100644 --- a/lib/capistrano/configuration/actions/invocation.rb +++ b/lib/capistrano/configuration/actions/invocation.rb @@ -160,7 +160,7 @@ def run(cmd, options={}, &block) # or #invoke_command. def run_tree(tree, options={}) #:nodoc: if tree.branches.empty? && tree.fallback - logger.debug "executing #{tree.fallback}" + logger.debug "executing #{tree.fallback}" unless options[:silent] elsif tree.branches.any? logger.debug "executing multiple commands in parallel" tree.each do |branch| diff --git a/lib/capistrano/recipes/deploy/assets.rb b/lib/capistrano/recipes/deploy/assets.rb index 4d3f93fb1..a47e61c8c 100644 --- a/lib/capistrano/recipes/deploy/assets.rb +++ b/lib/capistrano/recipes/deploy/assets.rb @@ -3,11 +3,15 @@ _cset :asset_env, "RAILS_GROUPS=assets" _cset :assets_prefix, "assets" _cset :assets_role, [:web] +_cset :expire_assets_after, (3600 * 24 * 7) _cset :normalize_asset_timestamps, false -before 'deploy:finalize_update', 'deploy:assets:symlink' -after 'deploy:update_code', 'deploy:assets:precompile' +before 'deploy:finalize_update', 'deploy:assets:symlink' +after 'deploy:update_code', 'deploy:assets:precompile' +before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes' +after 'deploy:cleanup', 'deploy:assets:clean_expired' +after 'deploy:rollback:revision', 'deploy:assets:rollback' namespace :deploy do namespace :assets do @@ -20,7 +24,7 @@ :assets_prefix variable to match. DESC task :symlink, :roles => assets_role, :except => { :no_release => true } do - run <<-CMD + run <<-CMD.compact rm -rf #{latest_release}/public/#{assets_prefix} && mkdir -p #{latest_release}/public && mkdir -p #{shared_path}/assets && @@ -39,7 +43,29 @@ set :asset_env, "RAILS_GROUPS=assets" DESC task :precompile, :roles => assets_role, :except => { :no_release => true } do - run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:precompile" + run <<-CMD.compact + cd -- #{latest_release.shellescape} && + #{rake} RAILS_ENV=#{rails_env.shellescape} #{asset_env.shellescape} assets:precompile && + cp -- #{shared_path.shellescape}/assets/manifest.yml #{current_release.shellescape}/assets_manifest.yml + CMD + end + + desc <<-DESC + [internal] Updates the mtimes for assets that are required by the current release. + This task runs before assets:precompile. + DESC + task :update_asset_mtimes, :roles => assets_role, :except => { :no_release => true } do + # Fetch assets/manifest.yml contents. + manifest_path = "#{shared_path}/assets/manifest.yml" + manifest_yml = capture("[ -e #{manifest_path.shellescape} ] && cat #{manifest_path.shellescape} || echo").strip + + if manifest_yml != "" + manifest = YAML.load(manifest_yml) + current_assets = manifest.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten + logger.info "Updating mtimes for ~#{current_assets.count} assets..." + command = "touch -cm -- " << current_assets.map {|a| "#{shared_path}/assets/#{a}".shellescape }.join(' ') + run command, :silent => true + end end desc <<-DESC @@ -56,5 +82,76 @@ task :clean, :roles => assets_role, :except => { :no_release => true } do run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean" end + + desc <<-DESC + Clean up any assets that haven't been deployed for more than :expire_assets_after seconds. + Default time to keep old assets is one week. Set the :expire_assets_after variable + to change the assets expiry time. Assets will only be deleted if they are not required by + an existing release. + DESC + task :clean_expired, :roles => assets_role, :except => { :no_release => true } do + # Fetch all assets_manifest.yml contents. + manifests_output = capture <<-CMD.compact + for manifest in #{releases_path.shellescape}/*/assets_manifest.yml; do + cat -- "$manifest" 2> /dev/null && printf ':::' || true; + done + CMD + manifests = manifests_output.split(':::') + + if manifests.empty? + logger.info "No manifests in #{releases_path}/*/assets_manifest.yml" + else + logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.yml" + current_assets = Set.new + manifests.each do |yaml| + manifest = YAML.load(yaml) + current_assets += manifest.to_a.flatten.flat_map do |file| + [file, "#{file}.gz"] + end + end + current_assets += %w(manifest.yml sources_manifest.yml) + + # Write the list of required assets to server. + # Files must be sorted in dictionary order using Linux sort + logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..." + escaped_assets = current_assets.to_a.join("\\n").gsub("\"", "\\\"") + run "printf -- \"#{escaped_assets}\" | sort > #{deploy_to.shellescape}/REQUIRED_ASSETS", :silent => true + + # Finds all files older than X minutes, then removes them if they are not referenced + # in REQUIRED_ASSETS. + expire_after_mins = (expire_assets_after.to_f / 60.0).to_i + logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..." + run <<-CMD.compact + cd -- #{shared_path.shellescape}/assets/ && + for f in $( + find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | sort | + comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS + ); do + echo "Removing unneeded asset: $f"; + rm -f -- "$f"; + done; + rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS + CMD + end + end + + desc <<-DESC + Rolls back assets to the previous release by symlinking the release's manifest + to shared/assets/manifest.yml, and finally recompiling or regenerating nondigest assets. + DESC + task :rollback, :roles => assets_role, :except => { :no_release => true } do + previous_manifest = "#{previous_release}/assets_manifest.yml" + if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true' + puts "#{previous_manifest} is missing! Cannot roll back assets. " << + "Please run deploy:assets:precompile to update your assets when the rollback is finished." + return false + else + run <<-CMD.compact + cd -- #{previous_release.shellescape} && + cp -f -- #{previous_manifest.shellescape} #{shared_path.shellescape}/assets/manifest.yml && + #{rake} RAILS_ENV=#{rails_env.shellescape} #{asset_env.shellescape} assets:precompile:nondigest + CMD + end + end end end