recap 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|