capistrano/capistrano — pull request #281 by ndbroadbent (2012-09-29 16:04:03+00:00)
Title: Added support for rolling back assets, and removing expired assets

## Solution for assets rollback and invalidation

Following on from the discussions at #227 and #210, I decided it would be fun to have a proper go at solving this problem. Here are the changes I've made:

The `deploy:assets:symlink` task ensures the presence of a `asset_releases` directory. This directory now holds all of the `manifest.yml` files for each release, under the names `{release_name}.yml`. At the end of the `deploy:assets:precompile` task, we move `#{shared_path}/assets/manifest.yml` to `asset_releases/#{release_name}.yml`, and symlink it back to `#{shared_path}/assets/manifest.yml`. Before precompile, we make sure to replace the symlink with a copy of the manifest, so that the task doesn't write to the previous manifest through the symlink.

We chain a `deploy:assets:update_previous_manifest` task after `deploy:create_symlink`, so that after each deploy, the previous release's `manifest.yml` is updated with a new `:last_deployed_at` time:

```yaml
:last_deployed_at: #{Time.now.to_s}
```

This records the time when the assets were first retired from deployment.

### Cleaning up expired assets

The `deploy:assets:cleanup_expired` task is chained after `deploy:cleanup`.

It works like this:

Load all manifests from `asset_releases/*.yml`, and iterate over each set of assets. If the corresponding release has been deleted, and `last_deployed_at` is more than `:expire_assets_after` seconds old (defaults to 1 week), then those assets are added to the `expired_assets` set. Otherwise, if the release still exists or the assets have been deployed recently, add the files to the `preserved_assets` set. Remove all `preserved assets` from `expired assets` at the end of the loop, and you have a list of assets that are safe to delete.

Now, to clean up the manifest files in `asset_releases`, we delete a manifest file only if it's release has been deleted, and it doesn't contain any unique, unexpired assets.

In order to run this clean up task efficiently, I've written a Ruby script that is gsubbed with params, uploaded to the server's shared directory, and then executed remotely. (Wasn't sure if there was a way to reliably add rake tasks to the parent app from the capistrano gem, especially if developers only add the gem to the `:development` group.)

### Rollbacks

There's now a `deploy:assets:rollback` task, which is chained after `deploy:rollback:revision`.
It symlinks `asset_releases/#{previous_release}.yml` to `shared/assets/manifest.yml`, and then runs `rake assets:precompile:nondigest` in order to recompile the nondigest assets.
Note that if you are using my `turbo-sprockets-rails3` gem, this will only take a couple of seconds since nondigest assets will be generated from the precompiled digest assets.

Please let me know if this all sounds reasonable. I haven't written tests yet, but can definitely add some before it's merged.