rain 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE.md +20 -0
  2. data/README.md +128 -0
  3. data/Rakefile +38 -0
  4. data/bin/rain +11 -0
  5. data/lib/generators/rain/install/USAGE +10 -0
  6. data/lib/generators/rain/install/install_generator.rb +24 -0
  7. data/lib/generators/rain/install/templates/versions.yml +3 -0
  8. data/lib/rain.rb +17 -0
  9. data/lib/rain/capistrano.rb +25 -0
  10. data/lib/rain/config.rb +21 -0
  11. data/lib/rain/deployer.rb +54 -0
  12. data/lib/rain/git_tools.rb +109 -0
  13. data/lib/rain/git_tools/release_tag.rb +46 -0
  14. data/lib/rain/semantic_version.rb +20 -0
  15. data/lib/rain/version.rb +3 -0
  16. data/lib/tasks/rain_tasks.rake +5 -0
  17. data/test/dummy/README.rdoc +261 -0
  18. data/test/dummy/Rakefile +7 -0
  19. data/test/dummy/app/assets/javascripts/application.js +15 -0
  20. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  21. data/test/dummy/app/controllers/application_controller.rb +3 -0
  22. data/test/dummy/app/helpers/application_helper.rb +2 -0
  23. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  24. data/test/dummy/config.ru +4 -0
  25. data/test/dummy/config/application.rb +59 -0
  26. data/test/dummy/config/boot.rb +10 -0
  27. data/test/dummy/config/database.yml +20 -0
  28. data/test/dummy/config/environment.rb +5 -0
  29. data/test/dummy/config/environments/development.rb +37 -0
  30. data/test/dummy/config/environments/production.rb +67 -0
  31. data/test/dummy/config/environments/test.rb +37 -0
  32. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  33. data/test/dummy/config/initializers/inflections.rb +15 -0
  34. data/test/dummy/config/initializers/mime_types.rb +5 -0
  35. data/test/dummy/config/initializers/secret_token.rb +7 -0
  36. data/test/dummy/config/initializers/session_store.rb +8 -0
  37. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/test/dummy/config/locales/en.yml +5 -0
  39. data/test/dummy/config/routes.rb +58 -0
  40. data/test/dummy/config/versions.yml +4 -0
  41. data/test/dummy/public/404.html +26 -0
  42. data/test/dummy/public/422.html +26 -0
  43. data/test/dummy/public/500.html +25 -0
  44. data/test/dummy/public/favicon.ico +0 -0
  45. data/test/dummy/script/rails +6 -0
  46. data/test/rain/config_test.rb +13 -0
  47. data/test/rain/deployer_test.rb +49 -0
  48. data/test/rain/git_tools/release_tag_test.rb +21 -0
  49. data/test/rain/git_tools_test.rb +101 -0
  50. data/test/rain_test.rb +7 -0
  51. data/test/test_helper.rb +35 -0
  52. metadata +304 -0
@@ -0,0 +1,20 @@
1
+ Copyright 2012 eLocal USA, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,128 @@
1
+ # Rain
2
+
3
+ Rain is a deployment engine for Rails applications. It leverages
4
+ Capistrano and Git to cleanly deploy new versions across multiple
5
+ servers. Essentially a command-line interface to both of these tools,
6
+ Rain emphasizes convention over configuration and attempts to make
7
+ continuous deployment between teams more organized and therefore, more
8
+ effective.
9
+
10
+ Designed for [eLocal](http://elocal.com), it is
11
+ currently in production use on several of our large applications.
12
+
13
+ ## Installation
14
+
15
+ Add to Gemfile
16
+
17
+ ```ruby
18
+ gem 'rain'
19
+ ```
20
+
21
+ Generate configuration
22
+
23
+ ```bash
24
+ $ rake rain:config
25
+ ```
26
+
27
+ This file tells Rain which versions are currently deployed. To start on
28
+ a different version, edit the file.
29
+
30
+ ## Usage
31
+
32
+ Rain is basically CLI glue for Capistrano and Git. In essence, it
33
+ creates a new Git tag with the version number as the name, pushes that
34
+ tag to `origin/master` (or whatever your default remote is), then
35
+ commits the name of that tag to a `versions.yml` file (editing the one
36
+ you created) and pushes that to your origin as well. When all is said
37
+ and done, you'll have a commit in your master branch marking when the
38
+ release occurred, as well as a tag on origin (and locally) with the
39
+ exact commit that was deployed with Capistrano. It allows you to easily
40
+ roll back changes without keeping a bunch of directories on the server
41
+ filled with ancient app code, as well as see at-a-glance when new
42
+ deployments occurred.
43
+
44
+ Meant for deployment to two environments, it deploys the latest `HEAD`
45
+ from Git's master branch to stage and the last released tag on stage to
46
+ production.
47
+
48
+ ### Configuration
49
+
50
+ Rain implies that you deploy to two environments, a **stage** for
51
+ testing new changes on a real server, with real API calls, and the
52
+ **production** environment which is where your application can be
53
+ interacted with by its userbase.
54
+
55
+ You need a `to_stage` and `to_production` task in your
56
+ **config/deploy.rb** for whatever environment you wish to deploy. The Rails environment, task and environment in the versions.yml file must be the same name. This task will tell Capistrano which servers to deploy to before it runs its `deploy` task. It basically configures Capistrano on a per-environment basis.
57
+
58
+ Here's what one of your `to_env` tasks might like:
59
+
60
+ ```ruby
61
+ desc "Let's gooooooooooooo"
62
+ task :to_production do
63
+ set :user, "bruce"
64
+ set :rails_env, "production"
65
+
66
+ role :web, "www.elocal.com"
67
+ role :app, "dyno.elocal.com"
68
+ role :db, "production.db.elocal.com", primary: true
69
+ end
70
+ ```
71
+
72
+ ### Deploying
73
+
74
+ Run the following command in your Terminal to deploy your app to every
75
+ server listed in the `to_production` task:
76
+
77
+ ```bash
78
+ $ rain on production
79
+ ```
80
+
81
+ Rain uses [Semantic Versioning](http://semver.org), so it is possible to
82
+ programatically increment the major or minor version instead of the
83
+ patch, which is the default (but can be explicitly set with `--patch`).
84
+
85
+ ```bash
86
+ $ rain on stage --minor
87
+ $ rain on production --major
88
+ ```
89
+
90
+ In a continuous deployment setting, it is best to establish rules
91
+ for when minor or major versions are bumped. You can see the "best
92
+ practice" rules established by semver.org if you run `rain help on`.
93
+
94
+ ## In The Wild
95
+
96
+ We use this deployment script at [eLocal](http://elocal.com) to deploy a
97
+ cluster of staging and production web app servers. Not only do we use it
98
+ for our main site, but the API application as well, which was the
99
+ primary motivation in developing this gem: the ability to share its code
100
+ across all of our applications.
101
+
102
+ ## Contributors
103
+
104
+ - [Tom Scott](http://github.com/tubbo)
105
+ - [Rob Di Marco](http://github.com/robdimarco)
106
+
107
+ ## License
108
+
109
+ Copyright 2012 eLocal USA, LLC
110
+
111
+ Permission is hereby granted, free of charge, to any person obtaining
112
+ a copy of this software and associated documentation files (the
113
+ "Software"), to deal in the Software without restriction, including
114
+ without limitation the rights to use, copy, modify, merge, publish,
115
+ distribute, sublicense, and/or sell copies of the Software, and to
116
+ permit persons to whom the Software is furnished to do so, subject to
117
+ the following conditions:
118
+
119
+ The above copyright notice and this permission notice shall be
120
+ included in all copies or substantial portions of the Software.
121
+
122
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
123
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
124
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
125
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
126
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
127
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
128
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Rain'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # = Rain
4
+ #
5
+ # Usage: `rain on {RAILS_ENV}`
6
+
7
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
8
+
9
+ require 'rain'
10
+
11
+ Rain::Deployer.start
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Generates the configuration file for Rain
3
+ and instructs the user on how to install
4
+ the Capistrano tasks.
5
+
6
+ Example:
7
+ rails generate rain:install
8
+
9
+ This will create:
10
+ config/version.yml
@@ -0,0 +1,24 @@
1
+ module Rain
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def copy_config_file
6
+ copy_file "versions.yml", "config/versions.yml"
7
+ end
8
+
9
+ def show_capistrano_instructions
10
+ say <<-TEXT
11
+
12
+ Please add `require 'rain/capistrano'` to Capfile and
13
+ define your :to_stage and :to_production tasks in config/deploy.rb.
14
+
15
+ Then, all you have to do to deploy to all of your servers is
16
+
17
+ rain on {environment}
18
+
19
+ Consult http://github.com/eLocal/rain/wiki for more information.
20
+
21
+ TEXT
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ ---
2
+ stage: rel_0.0.1
3
+ production: rel_0.0.1
@@ -0,0 +1,17 @@
1
+ require 'bundler'
2
+ Bundler.require :default
3
+
4
+ require 'rain/semantic_version'
5
+ require 'rain/config'
6
+ require 'rain/git_tools'
7
+ require 'rain/deployer'
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath)
11
+
12
+ module Rain
13
+ def self.version
14
+ config = Rain::Config.new(Dir.pwd)
15
+ config.versions
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # = Capistrano tasks
2
+ #
3
+ # A simple addon to Capistrano that defines some callbacks
4
+ # and deploy tasks.
5
+ #
6
+ # The to_latest_tag task abstracts setting the branch of each
7
+ # deployment to the current ReleaseTag.
8
+
9
+ require 'capistrano'
10
+ require 'rain/git_tools'
11
+
12
+ module Rain
13
+ Capistrano::Configuration.instance(:must_exist).load do
14
+ after "to_stage", "to_latest_tag"
15
+ after "to_production", "to_latest_tag"
16
+
17
+ task :to_latest_tag do
18
+ if rails_env == 'stage'
19
+ set :branch, GitTools::ReleaseTag.current(rails_env)
20
+ else
21
+ set :branch, GitTools::ReleaseTag.latest
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support/all'
2
+
3
+ module Rain
4
+ # Holds the configuration options for Rain from versions.yml in a
5
+ # HashWithIndifferentAccess.
6
+ class Config
7
+ attr_reader :yaml_file
8
+
9
+ def initialize(root)
10
+ @yaml_file = YAML::load_file "#{root}/config/versions.yml"
11
+ end
12
+
13
+ def versions
14
+ @versions ||= if @yaml_file.present?
15
+ ActiveSupport::HashWithIndifferentAccess.new @yaml_file
16
+ else
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,54 @@
1
+ require 'thor'
2
+
3
+ module Rain
4
+ class Deployer < Thor
5
+ include Thor::Actions
6
+ include GitTools
7
+
8
+ desc "on ENVIRONMENT", "Tag current HEAD and push it to the chosen environment. (default: 'production')"
9
+
10
+ method_option :force, type: :boolean, desc: "Force a release, do not prompt", aliases: "-f"
11
+ method_option :"keep-current-version", type: :boolean, desc: "Reuse the previous tag", aliases: "-k"
12
+ method_option :patch, default: true, desc: SemanticVersion::PATCH
13
+ method_option :minor, default: false, desc: SemanticVersion::MINOR
14
+ method_option :major, default: false, desc: SemanticVersion::MAJOR
15
+ method_option :smoke, default: false, desc: "Run tests after deployment"
16
+
17
+ def on environment="production"
18
+ say "Making it rain on #{environment}..."
19
+
20
+ return unless working_directory_copasetic?(options)
21
+
22
+ unless options[:"keep-current-version"] or environment == 'production'
23
+ update_release_tag(environment, tag.to_s)
24
+ run_cmd("git tag #{tag.to_s}")
25
+ push_tag(tag)
26
+ else
27
+ say "Deploying existing tag #{GitTools::ReleaseTag.current("stage")} to '#{environment}'."
28
+ end
29
+
30
+ run_cmd "bundle exec cap to_#{environment} deploy"
31
+
32
+ say "Got a handful of stacks better grab an umbrella."
33
+ end
34
+
35
+ default_task :on
36
+
37
+ private
38
+ no_tasks do
39
+ def run_smoke_tests
40
+ #run Rain.config.smoke_test_command
41
+ end
42
+
43
+ def tag
44
+ @tag ||= case true
45
+ when options[:minor] then increment_minor_version
46
+ when options[:major] then increment_major_version
47
+ else
48
+ say "No version type detected, assuming you meant '--patch' (as it is default)"
49
+ increment_patch_version
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,109 @@
1
+ require 'thor'
2
+ require_relative 'git_tools/release_tag'
3
+
4
+ # A collection of methods and a class, +ReleaseTag+, for querying the
5
+ # local Git repository. Originally part of the Thor binary, it was
6
+ # extracted to its own module after +Rain::Deployer+ got too large.
7
+ module GitTools
8
+ include Thor::Actions unless ENV['RAILS_ENV'] == 'test' # this is most likely temporary
9
+
10
+ # A short user prompt if there are uncommitted changes in the repo, in
11
+ # case the user forgets before they deploy. Naturally, one may cancel
12
+ # this process effectively cancelling the entire deploy cleanly. This
13
+ # occurs before any hard changes (e.g., writing changes, pushes,
14
+ # command execution, etc.) are made.
15
+ def working_directory_copasetic?(options={})
16
+ return true if options[:force]
17
+ return false unless no_changes_pending? || yes?("There are currently uncommitted changes. Are you sure you want to continue? [y/N]")
18
+ return false unless on_master? || yes?("You are not currently on the master branch. Are you sure you want to continue? [y/N]")
19
+ true
20
+ end
21
+
22
+ # Test whether we are currently using the master branch. All
23
+ # deployment activity should take place on the master branch.
24
+ def on_master?
25
+ out = %x(git symbolic-ref -q HEAD)
26
+ out.strip == "refs/heads/master"
27
+ end
28
+
29
+ # Test whether there are any uncommitted changes in the working
30
+ # directory.
31
+ def no_changes_pending?
32
+ %x(git status --porcelain).split("\n").length == 0
33
+ end
34
+
35
+ # Moved code to ReleaseTag. Basically returns a ReleaseTag with a version
36
+ # equal to the latest Git tag.
37
+ def last_release_tag
38
+ ReleaseTag.latest
39
+ end
40
+
41
+ # Display name as set in +~/.gitconfig+
42
+ def git_name
43
+ %x(git config --get user.name).split("\n")[0]
44
+ end
45
+ alias deployer git_name
46
+
47
+ # Compares the latest Git tag with the latest version name in YAML. If
48
+ # both of those are equal, this returns +true+, because the Git tags
49
+ # are in sync with versions.yml
50
+ def tagged_latest_version?
51
+ ReleaseTag.latest == ReleaseTag.current
52
+ end
53
+
54
+ %w(major minor patch).each do |f|
55
+ define_method :"increment_#{f}_version" do
56
+ tag = last_release_tag
57
+ case f
58
+ when "major"
59
+ tag.major += 1
60
+ tag.minor = 0
61
+ tag.patch = 0
62
+ tag.other = nil
63
+ when "minor"
64
+ tag.minor += 1
65
+ tag.patch = 0
66
+ tag.other = nil
67
+ when "patch"
68
+ tag.patch += 1
69
+ tag.other = nil
70
+ end
71
+ tag
72
+ end
73
+ end
74
+
75
+ # Push a given +ReleaseTag+ to +origin+. You must have a Git
76
+ # remote set up for this to work.
77
+ def push_tag(tag)
78
+ unless tag.nil?
79
+ run_cmd "git push origin #{tag}"
80
+ end
81
+ end
82
+
83
+ # Execute any shell commnad, unless we're testing. But always print
84
+ # what is/what would have been executed onto stdout.
85
+ def run_cmd(cmd)
86
+ puts "executing... #{cmd}"
87
+ %x(#{cmd}) unless ENV['RAILS_ENV'] == "test"
88
+ end
89
+
90
+ # Full path of the versions.yml file in the Rails app.
91
+ def versions_path
92
+ File.expand_path "./config/versions.yml"
93
+ end
94
+
95
+ # A YAML-parsed Hash of the versions.yml file.
96
+ def versions_hash
97
+ YAML.load_file(versions_path)
98
+ end
99
+
100
+ # Write the newest ReleaseTag's version number to versions.yml, save
101
+ # it, and commit/push it to +origin/master+.
102
+ def update_release_tag(environment, tag)
103
+ hsh = versions_hash
104
+ hsh[environment] = tag
105
+ File.write(versions_path, hsh.to_yaml.to_s)
106
+ run_cmd "git commit -m '[RELEASE][#{environment}] Update release tag for #{environment} to #{tag}' #{versions_path}"
107
+ run_cmd "git push origin master"
108
+ end
109
+ end