recap 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +4 -0
- data/README.md +12 -3
- data/Rakefile +8 -0
- data/Vagrantfile +61 -0
- data/doc/index.html +45 -26
- data/doc/lib/recap/bootstrap.html +42 -0
- data/doc/lib/recap/bundler.html +36 -19
- data/doc/lib/recap/capistrano_extensions.html +28 -23
- data/doc/lib/recap/cli.html +3 -0
- data/doc/lib/recap/compatibility.html +6 -3
- data/doc/lib/recap/deploy.html +41 -86
- data/doc/lib/recap/env.html +6 -1
- data/doc/lib/recap/foreman.html +3 -0
- data/doc/lib/recap/namespace.html +42 -0
- data/doc/lib/recap/preflight.html +12 -7
- data/doc/lib/recap/rails.html +3 -0
- data/doc/lib/recap/version.html +3 -0
- data/doc/lib/recap.html +42 -0
- data/features/bundling-gems.feature +18 -0
- data/features/deploying-projects.feature +21 -0
- data/features/managing-processes.feature +17 -0
- data/features/setting-environment-variables.feature +21 -0
- data/features/steps/capistrano_steps.rb +98 -0
- data/features/support/project.rb +211 -0
- data/features/support/server.rb +53 -0
- data/features/templates/gem/binary.erb +43 -0
- data/features/templates/gem/gemspec.erb +11 -0
- data/features/templates/project/Capfile +21 -0
- data/features/templates/project/Capfile.erb +21 -0
- data/features/templates/project/Gemfile.erb +7 -0
- data/features/templates/project/Procfile.erb +1 -0
- data/index.rb +26 -17
- data/lib/recap/bootstrap.rb +47 -0
- data/lib/recap/bundler.rb +31 -21
- data/lib/recap/capistrano_extensions.rb +11 -9
- data/lib/recap/cli.rb +1 -1
- data/lib/recap/compatibility.rb +3 -3
- data/lib/recap/deploy.rb +45 -57
- data/lib/recap/env.rb +30 -26
- data/lib/recap/environment.rb +54 -0
- data/lib/recap/foreman.rb +28 -9
- data/lib/recap/namespace.rb +37 -0
- data/lib/recap/preflight.rb +10 -8
- data/lib/recap/rails.rb +6 -4
- data/lib/recap/ruby.rb +3 -0
- data/lib/recap/static.rb +1 -0
- data/lib/recap/version.rb +1 -1
- data/lib/recap.rb +12 -0
- data/recap.gemspec +8 -4
- data/spec/models/environment_spec.rb +143 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/tasks/bootstrap_spec.rb +34 -0
- data/spec/tasks/bundler_spec.rb +126 -0
- data/spec/tasks/deploy_spec.rb +209 -0
- data/spec/tasks/env_spec.rb +38 -0
- data/spec/tasks/foreman_spec.rb +154 -0
- data/test-vm/manifests/base.pp +17 -0
- data/test-vm/share/.gitkeep +0 -0
- metadata +138 -19
- /data/bin/{tomafro-deploy → recap} +0 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'webrick'
|
3
|
+
|
4
|
+
GEM_VERSION = "<%= version %>"
|
5
|
+
|
6
|
+
class Server
|
7
|
+
class Action < WEBrick::HTTPServlet::AbstractServlet
|
8
|
+
def do_GET(request, response)
|
9
|
+
response.status = 200
|
10
|
+
response['Content-Type'] = "text/plain"
|
11
|
+
response.body = body
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Version < Action
|
16
|
+
def body
|
17
|
+
GEM_VERSION
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Env < Action
|
22
|
+
def body
|
23
|
+
ENV.keys.sort.map {|k| "#{k}=#{ENV[k]}" }.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.start
|
28
|
+
server = WEBrick::HTTPServer.new(:Port => 3500)
|
29
|
+
server.mount "/env", Env
|
30
|
+
server.mount "/version", Version
|
31
|
+
server.start
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
trap("SIGINT") { exit! }
|
36
|
+
trap("TERM") { exit! }
|
37
|
+
|
38
|
+
case ARGV[0]
|
39
|
+
when "--version" then puts GEM_VERSION
|
40
|
+
when "--env" then ENV.keys.sort.each {|k| puts "#{k}=#{ENV[k]}"}
|
41
|
+
when "--server" then Server.start
|
42
|
+
else puts "Unknown option"
|
43
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Tom Ward"]
|
5
|
+
gem.email = ["tom@popdog.net"]
|
6
|
+
gem.description = %q{<%= gem %> <%= version %>}
|
7
|
+
gem.summary = %q{<%= gem %> <%= version %>}
|
8
|
+
gem.executables = ['<%= gem %>']
|
9
|
+
gem.name = "<%= gem %>"
|
10
|
+
gem.version = "<%= version %>"
|
11
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require '<%= recap_require %>'
|
2
|
+
|
3
|
+
# To connect to the vagrant VM we need to set up a few non-standard parameters, including the
|
4
|
+
# vagrant SSH port and private key
|
5
|
+
|
6
|
+
set :user, 'vagrant'
|
7
|
+
|
8
|
+
ssh_options[:port] = 2222
|
9
|
+
ssh_options[:keys] = ['<%= project.private_key_path %>']
|
10
|
+
|
11
|
+
server '127.0.0.1', :web
|
12
|
+
|
13
|
+
# Each project has its own location shared between the host machine and the VM
|
14
|
+
|
15
|
+
set :application, '<%= project.name %>'
|
16
|
+
set :repository, '/recap/share/<%= project.name %>'
|
17
|
+
|
18
|
+
# Finally, to ensure tests don't fail if deployments are made within a second of each other
|
19
|
+
# which they can do when automated like this, we use a finer-grained release tag
|
20
|
+
|
21
|
+
set(:release_tag) { Time.now.utc.strftime("%Y%m%d%H%M%S%L") }
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require '<%= recap_require %>'
|
2
|
+
|
3
|
+
# To connect to the vagrant VM we need to set up a few non-standard parameters, including the
|
4
|
+
# vagrant SSH port and private key
|
5
|
+
|
6
|
+
set :user, 'vagrant'
|
7
|
+
|
8
|
+
ssh_options[:port] = 2222
|
9
|
+
ssh_options[:keys] = ['<%= project.private_key_path %>']
|
10
|
+
|
11
|
+
server '127.0.0.1', :web
|
12
|
+
|
13
|
+
# Each project has its own location shared between the host machine and the VM
|
14
|
+
|
15
|
+
set :application, '<%= project.name %>'
|
16
|
+
set :repository, '/recap/share/projects/<%= project.name %>'
|
17
|
+
|
18
|
+
# Finally, to ensure tests don't fail if deployments are made within a second of each other
|
19
|
+
# which they can do when automated like this, we use a finer-grained release tag
|
20
|
+
|
21
|
+
set(:release_tag) { Time.now.utc.strftime("%Y%m%d%H%M%S%L") }
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= name %>: <%= command %>
|
data/index.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# This is the annotated source code and documentation for
|
2
2
|
# [recap](http://github.com/freerange/recap), a simple, opinionated set of capistrano
|
3
|
-
# deployment recipes.
|
3
|
+
# deployment recipes.
|
4
|
+
|
5
|
+
# Inspired in part by
|
4
6
|
# [this blog post](https://github.com/blog/470-deployment-script-spring-cleaning), these recipes use
|
5
7
|
# git's strengths to deploy applications in a faster, simpler manner than a standard capistrano
|
6
8
|
# deployment. Using git to manage release versions means apps can be deployed to a single directory.
|
@@ -10,19 +12,19 @@
|
|
10
12
|
|
11
13
|
# These deployment recipes try to do the following:
|
12
14
|
|
13
|
-
#
|
14
|
-
# exceptions are `git` commands (which often rely on SSH agent forwarding for authentication), and
|
15
|
-
# that requires `sudo`.
|
15
|
+
# Where possible run commands as the `application_user`, loading the full user environment. The only
|
16
|
+
# exceptions are `git` commands (which often rely on SSH agent forwarding for authentication), and
|
17
|
+
# anything that requires `sudo`.
|
16
18
|
#
|
17
19
|
|
18
20
|
# Use `git` to avoid unecessary work. If the `Gemfile.lock` hasn't changed, there's no need to run
|
19
|
-
# `bundle install`. Similarly if there are no new migrations, why do `rake db:migrate
|
20
|
-
# mean more frequent deploys
|
21
|
+
# `bundle install`. Similarly if there are no new migrations, why do `rake db:migrate`? Faster
|
22
|
+
# deploys mean more frequent deploys.
|
21
23
|
#
|
22
24
|
|
23
|
-
# Avoid the use of `sudo` (other than to change to the `application_user`). As much as possible,
|
24
|
-
# is only used to `su` to the `application_user` before running a command. To avoid
|
25
|
-
# to perform the majority of deployment tasks,
|
25
|
+
# Avoid the use of `sudo` (other than to change to the `application_user`). As much as possible,
|
26
|
+
# `sudo` is only used to `su` to the `application_user` before running a command. To avoid having to
|
27
|
+
# type a password to perform the majority of deployment tasks, these lines can be added to
|
26
28
|
# `/etc/sudoers.d/application` (change `application` to the name of your app).
|
27
29
|
|
28
30
|
%application ALL=NOPASSWD: /sbin/start application*
|
@@ -31,23 +33,30 @@
|
|
31
33
|
%application ALL=NOPASSWD: /bin/su - application*
|
32
34
|
%application ALL=NOPASSWD: /bin/su application*
|
33
35
|
|
36
|
+
# Use environment variables for configuration. Rather than setting `rails_env` in the `Capfile`,
|
37
|
+
# `RAILS_ENV` (or `RACK_ENV`) variables should be set for the `application_user`. The `env:set` and
|
38
|
+
# `env:edit` tasks help do this.
|
39
|
+
|
34
40
|
# ### Code layout ###
|
35
41
|
|
36
|
-
# The main deployment tasks are defined in [recap.rb](lib/recap.html). Automatic
|
42
|
+
# The main deployment tasks are defined in [recap/deploy.rb](lib/recap/deploy.html). Automatic
|
37
43
|
# checks to ensure servers are correctly setup are in
|
38
|
-
# [recap/preflight.rb](lib/recap/preflight.html)
|
44
|
+
# [recap/preflight.rb](lib/recap/preflight.html), while tasks for environment variables are in
|
45
|
+
# [recap/env.rb](lib/recap/env.html)
|
39
46
|
|
40
|
-
# In addition, there are extensions for [bundler](lib/recap/bundler.html)
|
41
|
-
# [foreman](lib/recap/foreman.html).
|
47
|
+
# In addition, there are extensions for [bundler](lib/recap/bundler.html),
|
48
|
+
# [foreman](lib/recap/foreman.html) and [rails](lib/recap/rails.html)
|
42
49
|
|
43
|
-
# For
|
44
|
-
# [compatibility](lib/recap/compatibility.html)
|
50
|
+
# For limited compatability with other existing recipes, see
|
51
|
+
# [compatibility](lib/recap/compatibility.html).
|
45
52
|
|
46
53
|
# ### Deployment target ###
|
47
54
|
|
48
|
-
# These recipes have been
|
55
|
+
# These recipes have been developed and tested using Ubuntu 11.04, though they may work well with
|
56
|
+
# other flavours of unix.
|
49
57
|
|
50
|
-
# The application should be run as the application user; if using Apache and Passenger, you should
|
58
|
+
# The application should be run as the application user; if using Apache and Passenger, you should
|
59
|
+
# set the `PassengerDefaultUser` directive to be the same as the `application_user`.
|
51
60
|
|
52
61
|
# The code is available [on github](http://github.com/freerange/recap) and released under the
|
53
62
|
# [MIT License](https://github.com/freerange/recap/blob/master/LICENSE)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Recap::Bootstrap
|
2
|
+
extend Recap::Namespace
|
3
|
+
|
4
|
+
namespace :bootstrap do
|
5
|
+
set(:remote_username) { capture('whoami').strip }
|
6
|
+
set(:application_home) { "/home/#{application_user}"}
|
7
|
+
|
8
|
+
task :default do
|
9
|
+
application
|
10
|
+
user
|
11
|
+
end
|
12
|
+
|
13
|
+
task :application do
|
14
|
+
if exit_code("id #{application_user}").strip != "0"
|
15
|
+
sudo "useradd #{application_user} -d #{application_home}"
|
16
|
+
end
|
17
|
+
sudo "mkdir -p #{application_home}"
|
18
|
+
sudo "chown #{application_user}:#{application_group} #{application_home}"
|
19
|
+
sudo "chmod 755 #{application_home}"
|
20
|
+
|
21
|
+
put_as_app %{
|
22
|
+
if [ -s "$HOME/.env" ]; then
|
23
|
+
rm -rf $HOME/.recap-env-export
|
24
|
+
touch $HOME/.recap-env-export
|
25
|
+
while read line
|
26
|
+
do echo "export $line" >> $HOME/.recap-env-export;
|
27
|
+
done < $HOME/.env
|
28
|
+
. $HOME/.recap-env-export
|
29
|
+
fi
|
30
|
+
}, "#{application_home}/.recap"
|
31
|
+
|
32
|
+
as_app "touch .profile", "~"
|
33
|
+
|
34
|
+
if exit_code(%{grep '\\. \\$HOME\\/.recap' .profile}) != "0"
|
35
|
+
as_app %{echo >> .profile && echo ". \\$HOME/.recap" >> .profile}, "~"
|
36
|
+
end
|
37
|
+
|
38
|
+
as_app "mkdir -p #{deploy_to}", "~"
|
39
|
+
end
|
40
|
+
|
41
|
+
task :user do
|
42
|
+
run "git config --global user.name '#{`git config user.name`.strip}'"
|
43
|
+
run "git config --global user.email '#{`git config user.email`.strip}'"
|
44
|
+
sudo "usermod --append -G #{application_group} #{remote_username}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/recap/bundler.rb
CHANGED
@@ -1,26 +1,32 @@
|
|
1
1
|
# The bundler recipe ensures that the application bundle is installed whenever the code is updated.
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
set(:bundle_gemfile) { "#{deploy_to}/Gemfile" }
|
3
|
+
module Recap::Bundler
|
4
|
+
extend Recap::Namespace
|
6
5
|
|
7
|
-
|
8
|
-
|
6
|
+
namespace :bundle do
|
7
|
+
# Each bundle is declared in a `Gemfile`, by default in the root of the application directory
|
8
|
+
set(:bundle_gemfile) { "#{deploy_to}/Gemfile" }
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
set(:bundle_dir) { "#{deploy_to}/.bundle/gems" }
|
10
|
+
# As well as a `Gemfile`, application repositories should also contain a `Gemfile.lock`.
|
11
|
+
set(:bundle_gemfile_lock) { "#{bundle_gemfile}.lock" }
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
# An application's gems are installed within the application directory. By default they are
|
14
|
+
# placed under `vendor/gems`.
|
15
|
+
set(:bundle_path) { "#{deploy_to}/vendor/gems" }
|
16
|
+
|
17
|
+
# Not all gems are needed for production environments, so by default the `development`, `test` and
|
18
|
+
# `assets` groups are skipped.
|
19
|
+
set(:bundle_without) { "development test assets" }
|
20
|
+
|
21
|
+
# The main bundle install command uses all the settings above, together with the `--deployment`,
|
22
|
+
# `--binstubs` and `--quiet` flags
|
23
|
+
set(:bundle_install_command) { "bundle install --gemfile #{bundle_gemfile} --path #{bundle_path} --deployment --quiet --binstubs --without #{bundle_without}" }
|
17
24
|
|
18
|
-
namespace :bundle do
|
19
25
|
namespace :install do
|
20
|
-
# After cloning or updating the code, we only install the bundle if the `Gemfile`
|
21
|
-
desc "Install the latest gem bundle only if Gemfile.lock
|
26
|
+
# After cloning or updating the code, we only install the bundle if the `Gemfile` or `Gemfile.lock` have changed.
|
27
|
+
desc "Install the latest gem bundle only if Gemfile or Gemfile.lock have changed"
|
22
28
|
task :if_changed do
|
23
|
-
if deployed_file_changed?(bundle_gemfile_lock)
|
29
|
+
if deployed_file_changed?(bundle_gemfile) || deployed_file_changed?(bundle_gemfile_lock)
|
24
30
|
top.bundle.install.default
|
25
31
|
end
|
26
32
|
end
|
@@ -30,16 +36,20 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
30
36
|
desc "Install the latest gem bundle"
|
31
37
|
task :default do
|
32
38
|
if deployed_file_exists?(bundle_gemfile)
|
33
|
-
|
39
|
+
if deployed_file_exists?(bundle_gemfile_lock)
|
40
|
+
as_app bundle_install_command
|
41
|
+
else
|
42
|
+
abort 'Gemfile found without Gemfile.lock. The Gemfile.lock should be committed to the project repository'
|
43
|
+
end
|
34
44
|
else
|
35
45
|
puts "Skipping bundle:install as no Gemfile found"
|
36
46
|
end
|
37
47
|
end
|
38
48
|
end
|
39
|
-
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
50
|
+
# To install the bundle automatically each time the code is updated or cloned, hooks are added to
|
51
|
+
# the `deploy:clone_code` and `deploy:update_code` tasks.
|
52
|
+
after 'deploy:clone_code', 'bundle:install:if_changed'
|
53
|
+
after 'deploy:update_code', 'bundle:install:if_changed'
|
54
|
+
end
|
45
55
|
end
|
@@ -19,10 +19,11 @@ module Recap
|
|
19
19
|
|
20
20
|
# Put a string into a file as the application user
|
21
21
|
def put_as_app(string, path)
|
22
|
-
|
23
|
-
put
|
22
|
+
put string, "/tmp/recap-put-as-app"
|
23
|
+
as_app "cp /tmp/recap-put-as-app #{path} && chmod g+rw #{path}", "/"
|
24
24
|
end
|
25
25
|
|
26
|
+
# Edit a file on the remote server, using a local editor
|
26
27
|
def edit_file(path)
|
27
28
|
if editor = ENV['DEPLOY_EDITOR'] || ENV['EDITOR']
|
28
29
|
as_app "touch #{path} && chmod g+rw #{path}"
|
@@ -37,17 +38,16 @@ module Recap
|
|
37
38
|
|
38
39
|
# Run a git command in the `deploy_to` directory
|
39
40
|
def git(command)
|
40
|
-
run "cd #{deploy_to} && git #{command}"
|
41
|
+
run "cd #{deploy_to} && umask 002 && sg #{application_group} -c \"git #{command}\""
|
41
42
|
end
|
42
43
|
|
43
44
|
# Capture the result of a git command run within the `deploy_to` directory
|
44
45
|
def capture_git(command)
|
45
|
-
capture "cd #{deploy_to} && git #{command}"
|
46
|
+
capture "cd #{deploy_to} && umask 002 && sg #{application_group} -c 'git #{command}'"
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
as_app "bundle #{command}"
|
49
|
+
def exit_code(command)
|
50
|
+
capture("#{command} > /dev/null 2>&1; echo $?").strip
|
51
51
|
end
|
52
52
|
|
53
53
|
# Find the latest tag from the repository. As `git tag` returns tags in order, and our release
|
@@ -59,14 +59,16 @@ module Recap
|
|
59
59
|
|
60
60
|
# Does the given file exist within the deployment directory?
|
61
61
|
def deployed_file_exists?(path)
|
62
|
-
|
62
|
+
exit_code("cd #{deploy_to} && [ -f #{path} ]") == "0"
|
63
63
|
end
|
64
64
|
|
65
65
|
# Has the given path been created or changed since the previous deployment? During the first
|
66
66
|
# successful deployment this will always return true.
|
67
67
|
def deployed_file_changed?(path)
|
68
68
|
return true unless latest_tag
|
69
|
-
|
69
|
+
exit_code("cd #{deploy_to} && git diff --exit-code #{latest_tag} origin/#{branch} #{path}") == "1"
|
70
70
|
end
|
71
|
+
|
72
|
+
Capistrano::Configuration.send :include, self
|
71
73
|
end
|
72
74
|
end
|
data/lib/recap/cli.rb
CHANGED
data/lib/recap/compatibility.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# `recap` isn't intended to be compatible with tasks (such as those within the `bundler`
|
2
2
|
# or `whenever` projects) that are built on the original capistrano deployment recipes. At times
|
3
|
-
# though there are tasks that would work, but for some missing (and redundant) settings.
|
3
|
+
# though there are tasks that would work, but for some missing (and redundant) settings.
|
4
4
|
#
|
5
5
|
# Including this recipe adds these legacy settings, but provides no guarantee that original tasks
|
6
6
|
# will work. Many are based on assumptions about the deployment layout that no longer hold true.
|
7
7
|
|
8
|
-
|
9
|
-
extend Recap::
|
8
|
+
module Recap::Compatibility
|
9
|
+
extend Recap::Namespace
|
10
10
|
|
11
11
|
# As `git` to manages releases, all deployments are placed directly in the `deploy_to` folder. The
|
12
12
|
# `current_path` is always this directory (no symlinking required).
|
data/lib/recap/deploy.rb
CHANGED
@@ -1,49 +1,52 @@
|
|
1
|
+
require 'recap'
|
1
2
|
require 'recap/capistrano_extensions'
|
2
|
-
|
3
|
+
|
3
4
|
require 'recap/preflight'
|
5
|
+
require 'recap/bootstrap'
|
6
|
+
require 'recap/env'
|
4
7
|
|
5
|
-
|
6
|
-
extend Recap::
|
8
|
+
module Recap::Deploy
|
9
|
+
extend Recap::Namespace
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
+
namespace :deploy do
|
12
|
+
# To use this recipe, both the application's name and its git repository are required.
|
13
|
+
set(:application) { abort "You must set the name of your application in your Capfile, e.g.: set :application, 'tomafro.net'" }
|
14
|
+
set(:repository) { abort "You must set the git respository location in your Capfile, e.g.: set :respository, 'git@github.com/tomafro/tomafro.net'"}
|
11
15
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
# The recipe assumes that the application code will be run as a dedicated user. Any any user who
|
17
|
+
# can deploy the application should be added as a member of the application's group. By default,
|
18
|
+
# both the application user and group take the same name as the application.
|
19
|
+
set(:application_user) { application }
|
20
|
+
set(:application_group) { application_user }
|
17
21
|
|
18
|
-
|
19
|
-
|
22
|
+
# Deployments can be made from any branch. `master` is used by default.
|
23
|
+
set(:branch, 'master')
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
# Unlike a standard capistrano deployment, all releases are stored directly in the `deploy_to`
|
26
|
+
# directory. The default is `/home/#{application_user}/apps/#{application}`.
|
27
|
+
set(:deploy_to) { "/home/#{application_user}/apps/#{application}" }
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
# Each release is marked by a unique tag, generated with the current timestamp. While this can be
|
30
|
+
# changed, it's not recommended, as the sort order of the tag names is important; later tags must
|
31
|
+
# be listed after earlier tags.
|
32
|
+
set(:release_tag) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
|
29
33
|
|
30
|
-
|
31
|
-
|
32
|
-
|
34
|
+
# On tagging a release, a message is also recorded alongside the tag. This message can contain
|
35
|
+
# anything useful - its contents are not important for the recipe.
|
36
|
+
set(:release_message, "Deployed at #{Time.now}")
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
# Some tasks need to know the `latest_tag` - the most recent successful deployment. If no
|
39
|
+
# deployments have been made, this will be `nil`.
|
40
|
+
set(:latest_tag) { latest_tag_from_repository }
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
# To authenticate with github or other git servers, it is easier (and cleaner) to forward the
|
43
|
+
# deploying user's ssh key than manage keys on deployment servers.
|
44
|
+
ssh_options[:forward_agent] = true
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
|
46
|
+
# If key forwarding isn't possible, git may show a password prompt which stalls capistrano unless
|
47
|
+
# `:pty` is set to `true`.
|
48
|
+
default_run_options[:pty] = true
|
45
49
|
|
46
|
-
namespace :deploy do
|
47
50
|
# The `deploy:setup` task prepares all the servers for the deployment.
|
48
51
|
desc "Prepare servers for deployment"
|
49
52
|
task :setup, :except => {:no_release => true} do
|
@@ -54,25 +57,11 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
54
57
|
|
55
58
|
# Clone the repository into the deployment directory.
|
56
59
|
task :clone_code, :except => {:no_release => true} do
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# permission to write in the base deployment folder.
|
63
|
-
run "git clone #{repository} $HOME/#{application}.tmp"
|
64
|
-
# Again using `sudo`, move the temporary clone to its final destination.
|
65
|
-
sudo "mv $HOME/#{application}.tmp #{deploy_to}"
|
66
|
-
# Finally ensure that members of the `application_group` can read and write all files.
|
67
|
-
top.deploy.change_ownership
|
68
|
-
end
|
69
|
-
|
70
|
-
# Any files that have been created or updated by our user need to have thier permissions changed to
|
71
|
-
# ensure they can be read and written by and member of the `application_group` (deploying users and
|
72
|
-
# the application itself).
|
73
|
-
task :change_ownership, :except => {:no_release => true} do
|
74
|
-
run "find #{deploy_to} -user `whoami` ! -group #{application_group} -exec chown :#{application_group} {} \\;"
|
75
|
-
run "find #{deploy_to} -user `whoami` -exec chmod g+rw {} \\;"
|
60
|
+
# Before cloning, the directory needs to exist and be both readable and writable by the application group
|
61
|
+
as_app "mkdir -p #{deploy_to}", "~"
|
62
|
+
as_app "chmod g+rw #{deploy_to}"
|
63
|
+
# Then clone the code
|
64
|
+
git "clone #{repository} ."
|
76
65
|
end
|
77
66
|
|
78
67
|
# The main deployment task (called with `cap deploy`) deploys the latest application code to all
|
@@ -91,19 +80,16 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
91
80
|
on_rollback { git "reset --hard #{latest_tag}" if latest_tag }
|
92
81
|
git "fetch"
|
93
82
|
git "reset --hard origin/#{branch}"
|
94
|
-
# Finally ensure that the members of the `application_group` can read and write all files.
|
95
|
-
top.deploy.change_ownership
|
96
83
|
end
|
97
84
|
|
98
85
|
# Tag `HEAD` with the release tag and message
|
99
86
|
task :tag, :except => {:no_release => true} do
|
100
87
|
on_rollback { git "tag -d #{release_tag}" }
|
101
88
|
git "tag #{release_tag} -m '#{release_message}'"
|
102
|
-
top.deploy.change_ownership
|
103
89
|
end
|
104
90
|
|
105
91
|
# After a successful deployment, the app is restarted. In the most basic deployments this does
|
106
|
-
# nothing, but other recipes may override it, or attach tasks
|
92
|
+
# nothing, but other recipes may override it, or attach tasks to its before or after hooks.
|
107
93
|
desc "Restart the application following a deploy"
|
108
94
|
task :restart do
|
109
95
|
end
|
@@ -118,8 +104,10 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
118
104
|
if previous_tag = latest_tag_from_repository
|
119
105
|
git "reset --hard #{previous_tag}"
|
120
106
|
end
|
107
|
+
restart
|
108
|
+
else
|
109
|
+
abort "This app is not currently deployed"
|
121
110
|
end
|
122
|
-
restart
|
123
111
|
end
|
124
112
|
end
|
125
113
|
|
data/lib/recap/env.rb
CHANGED
@@ -1,54 +1,58 @@
|
|
1
|
-
#
|
1
|
+
# Environment variables are a useful way to set application configuration, such as database passwords
|
2
|
+
# or S3 keys and secrets. [recap](http://github.com/freerange/recap) stores these extra variables in
|
3
|
+
# a special file, usually stored at `$HOME/.env`. This file is loaded each time the shell starts by
|
4
|
+
# adding the following to the user's `.profile`:
|
2
5
|
#
|
3
|
-
#
|
6
|
+
# . $HOME/.recap
|
4
7
|
#
|
5
|
-
#
|
8
|
+
# The `.recap` script is automatically generated in the bootstrap process.
|
9
|
+
|
10
|
+
module Recap::Env
|
11
|
+
extend Recap::Namespace
|
6
12
|
|
7
|
-
Capistrano::Configuration.instance(:must_exist).load do
|
8
13
|
namespace :env do
|
14
|
+
# Environment
|
9
15
|
set(:environment_file) { "/home/#{application_user}/.env" }
|
10
16
|
|
11
|
-
def extract_environment(declarations)
|
12
|
-
declarations.inject({}) do |env, line|
|
13
|
-
if line =~ /\A([A-Za-z_]+)=(.*)\z/
|
14
|
-
env[$1] = $2.strip
|
15
|
-
end
|
16
|
-
env
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
17
|
def current_environment
|
21
18
|
@current_environment ||= begin
|
22
19
|
if deployed_file_exists?(environment_file)
|
23
|
-
|
20
|
+
Recap::Environment.from_string(capture("cat #{environment_file}"))
|
24
21
|
else
|
25
|
-
|
22
|
+
Recap::Environment.new
|
26
23
|
end
|
27
24
|
end
|
28
25
|
end
|
29
26
|
|
30
|
-
def write_environment(env)
|
31
|
-
env.keys.sort.collect do |v|
|
32
|
-
"#{v}=#{env[v]}" unless env[v].nil? || env[v].empty?
|
33
|
-
end.compact.join("\n")
|
34
|
-
end
|
35
|
-
|
36
27
|
task :default do
|
37
|
-
|
28
|
+
if current_environment.empty?
|
29
|
+
puts "There are no config variables set"
|
30
|
+
else
|
31
|
+
puts "The config variables are:"
|
32
|
+
puts
|
33
|
+
puts current_environment
|
34
|
+
end
|
38
35
|
end
|
39
36
|
|
40
37
|
task :set do
|
41
|
-
|
42
|
-
|
38
|
+
env = ARGV[1..-1].inject(current_environment) do |env, string|
|
39
|
+
env.set_string(string)
|
40
|
+
logger.debug "Setting #{string}"
|
41
|
+
logger.debug "Env is now: #{env}"
|
42
|
+
env
|
43
|
+
end
|
44
|
+
|
43
45
|
if env.empty?
|
44
|
-
as_app "rm -f #{environment_file}"
|
46
|
+
as_app "rm -f #{environment_file}", "~"
|
45
47
|
else
|
46
|
-
put_as_app env, environment_file
|
48
|
+
put_as_app env.to_s, environment_file
|
47
49
|
end
|
50
|
+
default
|
48
51
|
end
|
49
52
|
|
50
53
|
task :edit do
|
51
54
|
edit_file environment_file
|
55
|
+
default
|
52
56
|
end
|
53
57
|
end
|
54
58
|
end
|