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.
Files changed (61) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +4 -0
  3. data/README.md +12 -3
  4. data/Rakefile +8 -0
  5. data/Vagrantfile +61 -0
  6. data/doc/index.html +45 -26
  7. data/doc/lib/recap/bootstrap.html +42 -0
  8. data/doc/lib/recap/bundler.html +36 -19
  9. data/doc/lib/recap/capistrano_extensions.html +28 -23
  10. data/doc/lib/recap/cli.html +3 -0
  11. data/doc/lib/recap/compatibility.html +6 -3
  12. data/doc/lib/recap/deploy.html +41 -86
  13. data/doc/lib/recap/env.html +6 -1
  14. data/doc/lib/recap/foreman.html +3 -0
  15. data/doc/lib/recap/namespace.html +42 -0
  16. data/doc/lib/recap/preflight.html +12 -7
  17. data/doc/lib/recap/rails.html +3 -0
  18. data/doc/lib/recap/version.html +3 -0
  19. data/doc/lib/recap.html +42 -0
  20. data/features/bundling-gems.feature +18 -0
  21. data/features/deploying-projects.feature +21 -0
  22. data/features/managing-processes.feature +17 -0
  23. data/features/setting-environment-variables.feature +21 -0
  24. data/features/steps/capistrano_steps.rb +98 -0
  25. data/features/support/project.rb +211 -0
  26. data/features/support/server.rb +53 -0
  27. data/features/templates/gem/binary.erb +43 -0
  28. data/features/templates/gem/gemspec.erb +11 -0
  29. data/features/templates/project/Capfile +21 -0
  30. data/features/templates/project/Capfile.erb +21 -0
  31. data/features/templates/project/Gemfile.erb +7 -0
  32. data/features/templates/project/Procfile.erb +1 -0
  33. data/index.rb +26 -17
  34. data/lib/recap/bootstrap.rb +47 -0
  35. data/lib/recap/bundler.rb +31 -21
  36. data/lib/recap/capistrano_extensions.rb +11 -9
  37. data/lib/recap/cli.rb +1 -1
  38. data/lib/recap/compatibility.rb +3 -3
  39. data/lib/recap/deploy.rb +45 -57
  40. data/lib/recap/env.rb +30 -26
  41. data/lib/recap/environment.rb +54 -0
  42. data/lib/recap/foreman.rb +28 -9
  43. data/lib/recap/namespace.rb +37 -0
  44. data/lib/recap/preflight.rb +10 -8
  45. data/lib/recap/rails.rb +6 -4
  46. data/lib/recap/ruby.rb +3 -0
  47. data/lib/recap/static.rb +1 -0
  48. data/lib/recap/version.rb +1 -1
  49. data/lib/recap.rb +12 -0
  50. data/recap.gemspec +8 -4
  51. data/spec/models/environment_spec.rb +143 -0
  52. data/spec/spec_helper.rb +7 -0
  53. data/spec/tasks/bootstrap_spec.rb +34 -0
  54. data/spec/tasks/bundler_spec.rb +126 -0
  55. data/spec/tasks/deploy_spec.rb +209 -0
  56. data/spec/tasks/env_spec.rb +38 -0
  57. data/spec/tasks/foreman_spec.rb +154 -0
  58. data/test-vm/manifests/base.pp +17 -0
  59. data/test-vm/share/.gitkeep +0 -0
  60. metadata +138 -19
  61. /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,7 @@
1
+ source :rubygems
2
+ <% if foreman %>
3
+ gem 'foreman'
4
+ <% end %>
5
+ <% gems.each do |gem, version| %>
6
+ gem '<%= gem %>', :git => '/recap/share/gems/<%= gem %>', :tag => '<%= version %>'
7
+ <% end %>
@@ -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. Inspired by
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
- # Run all commands as the `application_user`, loading the full user environment. The only
14
- # exceptions are `git` commands (which often rely on SSH agent forwarding for authentication), and anything
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`. Faster deploys
20
- # mean more frequent deploys, which in our experience leads to better applications.
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, `sudo`
24
- # is only used to `su` to the `application_user` before running a command. To avoid typing a password
25
- # to perform the majority of deployment tasks, this code can be added to
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) and
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 (limited) compatability with other existing recipes, see
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 run successful against Ubuntu.
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 set the `PassengerDefaultUser` directive to be the same as the `application_user`.
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
- Capistrano::Configuration.instance(:must_exist).load do
4
- # Each bundle is declared in a `Gemfile`, by default in the root of the application directory
5
- set(:bundle_gemfile) { "#{deploy_to}/Gemfile" }
3
+ module Recap::Bundler
4
+ extend Recap::Namespace
6
5
 
7
- # As well as a `Gemfile`, application repositories should also contain a `Gemfile.lock`.
8
- set(:bundle_gemfile_lock) { "#{bundle_gemfile}.lock" }
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
- # An application's gems are installed within the application directory. By default they are
11
- # places under `.bundle/gems`.
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
- # Not all gems are needed for production environments, so by default the `development`, `test` and
15
- # `assets` groups are skipped.
16
- set(:bundle_without) { "development test assets" }
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` has changed.
21
- desc "Install the latest gem bundle only if Gemfile.lock has changed"
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
- bundler "install --gemfile #{bundle_gemfile} --path #{bundle_dir} --deployment --quiet --binstubs --without #{bundle_without}"
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
- # To install the bundle automatically each time the code is updated or cloned, hooks are added to
42
- # the `deploy:clone_code` and `deploy:update_code` tasks.
43
- after 'deploy:clone_code', 'bundle:install:if_changed'
44
- after 'deploy:update_code', 'bundle:install:if_changed'
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
- as_app "touch #{path} && chmod g+rw #{path}"
23
- put string, path
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
- # Run a bundle command in the `deploy_to` directory
49
- def bundler(command)
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
- capture("cd #{deploy_to} && [ -f #{path} ]; echo $?").strip == "0"
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
- capture_git("diff --exit-code #{latest_tag} origin/#{branch} #{path} > /dev/null; echo $?").strip == "1"
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
@@ -7,7 +7,7 @@ module Recap
7
7
  attr_accessor :name, :repository
8
8
 
9
9
  def self.source_root
10
- File.expand_path("../templates", __FILE__)
10
+ File.expand_path("../deploy/templates", __FILE__)
11
11
  end
12
12
 
13
13
  desc 'setup', 'Setup basic capistrano recipes, e.g: recap setup'
@@ -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
- Capistrano::Configuration.instance(:must_exist).load do
9
- extend Recap::CapistranoExtensions
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
- require 'recap/bundler'
3
+
3
4
  require 'recap/preflight'
5
+ require 'recap/bootstrap'
6
+ require 'recap/env'
4
7
 
5
- Capistrano::Configuration.instance(:must_exist).load do
6
- extend Recap::CapistranoExtensions
8
+ module Recap::Deploy
9
+ extend Recap::Namespace
7
10
 
8
- # To use this recipe, both the application's name and its git repository are required.
9
- set(:application) { abort "You must set the name of your application in your Capfile, e.g.: set :application, 'tomafro.net'" }
10
- set(:repository) { abort "You must set the git respository location in your Capfile, e.g.: set :respository, 'git@github.com/tomafro/tomafro.net'"}
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
- # The recipe assumes that the application code will be run as a dedicated user. Any any user who
13
- # can deploy the application should be added as a member of the application's group. By default,
14
- # both the application user and group take the same name as the application.
15
- set(:application_user) { application }
16
- set(:application_group) { application_user }
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
- # Deployments can be made from any branch. `master` is used by default.
19
- set(:branch, 'master')
22
+ # Deployments can be made from any branch. `master` is used by default.
23
+ set(:branch, 'master')
20
24
 
21
- # Unlike a standard capistrano deployment, all releases are stored directly in the `deploy_to`
22
- # directory. The default is `/home/#{application_user}/apps/#{application}`.
23
- set(:deploy_to) { "/home/#{application_user}/apps/#{application}" }
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
- # Each release is marked by a unique tag, generated with the current timestamp. While this can be
26
- # changed, it's not recommended, as the sort order of the tag names is important; later tags must
27
- # be listed after earlier tags.
28
- set(:release_tag) { "#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"}
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
- # On tagging a release, a message is also recorded alongside the tag. This message can contain
31
- # anything useful - its contents are not important for the recipe.
32
- set(:release_message, "Deployed at #{Time.now}")
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
- # Some tasks need to know the `latest_tag` - the most recent successful deployment. If no
35
- # deployments have been made, this will be `nil`.
36
- set(:latest_tag) { latest_tag_from_repository }
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
- # To authenticate with github or other git servers, it is easier (and cleaner) to forward the
39
- # deploying user's ssh key than manage keys on deployment servers.
40
- ssh_options[:forward_agent] = true
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
- # If key forwarding isn't possible, git may show a password prompt which stalls capistrano unless
43
- # `:pty` is set to `true`.
44
- default_run_options[:pty] = true
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
- # This is a slightly complicated process, as git doesn't allow us to clone into an existing
58
- # directory. To get around this, using `sudo` we create the base deployment folder (if it
59
- # doesn't already exist).
60
- sudo "mkdir -p #{File.expand_path(deploy_to + "/..")}"
61
- # Next, clone our code into a temporary location. This is necessary as our user might not have
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 it's before or after hooks.
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
- # N.B. To get the environment loaded on every shell invocation add the following to .profile:
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
- # if [ -s "$HOME/.env" ]; then export $(cat $HOME/.env); fi
6
+ # . $HOME/.recap
4
7
  #
5
- # This will eventually be done automatically
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
- extract_environment(capture("cat #{environment_file}").split("\n"))
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
- puts write_environment(current_environment)
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
- additions = extract_environment(ARGV[1..-1])
42
- env = write_environment(current_environment.merge(additions))
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