recap 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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