rain 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.md +20 -0
- data/README.md +128 -0
- data/Rakefile +38 -0
- data/bin/rain +11 -0
- data/lib/generators/rain/install/USAGE +10 -0
- data/lib/generators/rain/install/install_generator.rb +24 -0
- data/lib/generators/rain/install/templates/versions.yml +3 -0
- data/lib/rain.rb +17 -0
- data/lib/rain/capistrano.rb +25 -0
- data/lib/rain/config.rb +21 -0
- data/lib/rain/deployer.rb +54 -0
- data/lib/rain/git_tools.rb +109 -0
- data/lib/rain/git_tools/release_tag.rb +46 -0
- data/lib/rain/semantic_version.rb +20 -0
- data/lib/rain/version.rb +3 -0
- data/lib/tasks/rain_tasks.rake +5 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +20 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/config/versions.yml +4 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/rain/config_test.rb +13 -0
- data/test/rain/deployer_test.rb +49 -0
- data/test/rain/git_tools/release_tag_test.rb +21 -0
- data/test/rain/git_tools_test.rb +101 -0
- data/test/rain_test.rb +7 -0
- data/test/test_helper.rb +35 -0
- metadata +304 -0
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/rain
ADDED
@@ -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
|
data/lib/rain.rb
ADDED
@@ -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
|
data/lib/rain/config.rb
ADDED
@@ -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
|