recap 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/.gitignore +2 -1
  2. data/README.md +55 -10
  3. data/Rakefile +19 -5
  4. data/bin/recap +2 -2
  5. data/features/managing-processes.feature +1 -1
  6. data/features/setting-environment-variables.feature +26 -1
  7. data/features/steps/capistrano_steps.rb +10 -6
  8. data/features/support/project.rb +24 -5
  9. data/features/templates/project/Capfile.erb +1 -1
  10. data/lib/recap/recipes/rails.rb +6 -0
  11. data/lib/recap/recipes/ruby.rb +11 -0
  12. data/lib/recap/recipes/static.rb +3 -0
  13. data/lib/recap/recipes.rb +18 -0
  14. data/lib/recap/support/capistrano_extensions.rb +85 -0
  15. data/lib/recap/support/cli.rb +57 -0
  16. data/lib/recap/{compatibility.rb → support/compatibility.rb} +2 -2
  17. data/lib/recap/support/environment.rb +61 -0
  18. data/lib/recap/support/namespace.rb +47 -0
  19. data/lib/recap/support/shell_command.rb +35 -0
  20. data/lib/recap/support/templates/Capfile.erb +6 -0
  21. data/lib/recap/tasks/bootstrap.rb +77 -0
  22. data/lib/recap/{bundler.rb → tasks/bundler.rb} +15 -6
  23. data/lib/recap/{deploy.rb → tasks/deploy.rb} +30 -17
  24. data/lib/recap/tasks/env.rb +111 -0
  25. data/lib/recap/{foreman.rb → tasks/foreman.rb} +20 -12
  26. data/lib/recap/{preflight.rb → tasks/preflight.rb} +13 -11
  27. data/lib/recap/tasks/rails.rb +42 -0
  28. data/lib/recap/tasks.rb +16 -0
  29. data/lib/recap/version.rb +1 -1
  30. data/lib/recap.rb +119 -10
  31. data/recap.gemspec +3 -2
  32. data/spec/models/capistrano_extensions_spec.rb +41 -0
  33. data/spec/models/cli_spec.rb +25 -0
  34. data/spec/models/environment_spec.rb +14 -14
  35. data/spec/models/shell_command_spec.rb +55 -0
  36. data/spec/spec_helper.rb +1 -1
  37. data/spec/tasks/bootstrap_spec.rb +9 -13
  38. data/spec/tasks/bundler_spec.rb +39 -7
  39. data/spec/tasks/deploy_spec.rb +42 -26
  40. data/spec/tasks/env_spec.rb +81 -5
  41. data/spec/tasks/foreman_spec.rb +10 -5
  42. data/spec/tasks/rails_spec.rb +80 -0
  43. metadata +65 -57
  44. data/doc/index.html +0 -235
  45. data/doc/lib/recap/bootstrap.html +0 -42
  46. data/doc/lib/recap/bundler.html +0 -168
  47. data/doc/lib/recap/capistrano_extensions.html +0 -208
  48. data/doc/lib/recap/cli.html +0 -42
  49. data/doc/lib/recap/compatibility.html +0 -73
  50. data/doc/lib/recap/deploy.html +0 -328
  51. data/doc/lib/recap/env.html +0 -108
  52. data/doc/lib/recap/foreman.html +0 -42
  53. data/doc/lib/recap/namespace.html +0 -42
  54. data/doc/lib/recap/preflight.html +0 -163
  55. data/doc/lib/recap/rails.html +0 -42
  56. data/doc/lib/recap/version.html +0 -42
  57. data/doc/lib/recap.html +0 -42
  58. data/index.rb +0 -62
  59. data/lib/recap/bootstrap.rb +0 -47
  60. data/lib/recap/capistrano_extensions.rb +0 -74
  61. data/lib/recap/cli.rb +0 -32
  62. data/lib/recap/deploy/templates/Capfile.erb +0 -6
  63. data/lib/recap/env.rb +0 -58
  64. data/lib/recap/environment.rb +0 -54
  65. data/lib/recap/namespace.rb +0 -37
  66. data/lib/recap/rails.rb +0 -24
  67. data/lib/recap/ruby.rb +0 -3
  68. data/lib/recap/static.rb +0 -1
data/.gitignore CHANGED
@@ -4,4 +4,5 @@
4
4
  Gemfile.lock
5
5
  pkg/*
6
6
  /test-vm/share/*
7
- capistrano.log
7
+ capistrano.log
8
+ doc
data/README.md CHANGED
@@ -1,14 +1,59 @@
1
- [recap](http://github.com/freerange/recap) is an opinionated set of capistrano deployment recipes, designed to use git's strengths to deploy applications and websites in a fast and simple manner.
1
+ # Recap
2
2
 
3
- Recap's core features are:
3
+ [Recap](https://github.com/freerange/recap) is an opinionated set of [Capistrano](https://github.com/capistrano/capistrano) deployment recipes, designed to use git's strengths to deploy applications and websites in a fast and simple manner.
4
4
 
5
- * Release versions are managed with git. There's no need for `releases` or `current` folders, and no symlinking.
6
- * Intelligently decides whether tasks need to execute. e.g. The `bundle:install` task will only run if a `Gemfile.lock` exists, and if it has changed since the last deployment.
7
- * A dedicated user account and group owns all an application's associated files and processes.
8
- * Deployments are run using personal logins. The right to deploy is granted by adding a user to the application group.
9
- * Environment variables are used for application specific configuration. These can easily be read and set using the `env` and `env:set` tasks.
10
- * Out of the box support for `bundler` and `foreman`
11
5
 
12
- For more information, the main documentation can be found at [http://code.gofreerange.com/recap](http://code.gofreerange.com/recap), while the code is available [on github](https://github.com/freerange/recap).
6
+ ## Features & Aims
13
7
 
14
- Recap was written by [Tom Ward](http://tomafro.net) and the other members of [Go Free Range](http://gofreerange.com), and is released under the [MIT License](https://github.com/freerange/recap/blob/master/LICENSE).
8
+ * Releases are managed using git. All code is deployed to a single directory, and git tags are used to manage different released versions. No `releases`, `current` or `shared` directories are created, avoiding unnecessary sym-linking.
9
+ * Deployments do the minimum work possible, using git to determine whether tasks need to run. e.g. the `bundle:install` task only runs if the app contains a `Gemfile.lock` file and it has changed since the last deployment.
10
+ * Applications have their own user account and group, owning all of that application's associated files and processes. This gives them a dedicated environment, allowing environment variables to be used for application specific configuration. Tasks such as `env`, `env:set` and `env:edit` make setting and changing these variables easy.
11
+ * Personal accounts are used to deploy to the server, distinct from the application user. The right to deploy an application is granted simply by adding a user to the application group.
12
+
13
+
14
+ ## Documentation
15
+
16
+ For more information, the main documentation can be found at [http://gofreerange.com/recap/docs](http://http://gofreerange.com/recap/docs).
17
+
18
+
19
+ ## Prerequistes
20
+
21
+ * Recap's built-in tasks only support deploying to Ubuntu
22
+ * Your user account (as opposed to the application account) must be able to `sudo`
23
+ * Your user account should be able to connect to the remote git repository from your deployment server(s)
24
+
25
+
26
+ ## Source
27
+
28
+ The source code is available [on Github](https://github.com/freerange/recap).
29
+
30
+
31
+ ## Running Tests
32
+
33
+ - Run the following commands from the checked out project directory.
34
+ - Install dependencies (assumes the bundler gem is installed).
35
+
36
+ `$ bundle install`
37
+
38
+ - Run specs
39
+
40
+ `$ bundle exec rake`
41
+
42
+ - Install [VirtualBox](https://www.virtualbox.org/) - only necessary if you want to run [Cucumber](https://github.com/cucumber/cucumber) features.
43
+ - Install and provision a test VM based on the [Vagrantfile](https://github.com/freerange/recap/blob/master/Vagrantfile) (assumes VirtualBox is installed)
44
+
45
+ `$ bundle exec vagrant up`
46
+
47
+ - Run features
48
+
49
+ `$ bundle exec cucumber`
50
+
51
+
52
+ ## Credits
53
+
54
+ Recap was written by [Tom Ward](http://tomafro.net) and the other members of [Go Free Range](http://gofreerange.com).
55
+
56
+
57
+ ## License
58
+
59
+ Recap is released under the [MIT License](https://github.com/freerange/recap/blob/master/LICENSE).
data/Rakefile CHANGED
@@ -3,13 +3,27 @@ require 'rocco/tasks'
3
3
  require 'rspec/core/rake_task'
4
4
 
5
5
  desc 'build docs'
6
- Rocco::Task.new :rocco, 'doc/', ['index.rb', 'lib/**/*.rb']
6
+ task :doc do
7
+ FileUtils.cd('lib') do
8
+ files = Dir['**/*.rb']
9
+ files.each do |source_file|
10
+ rocco = Rocco.new(source_file, files.to_a, {})
11
+ dest_file = '../doc/' + source_file.sub(Regexp.new("#{File.extname(source_file)}$"), '.html')
12
+ FileUtils.mkdir_p(File.dirname(dest_file))
13
+ File.open(dest_file, 'wb') { |fd| fd.write(rocco.to_html) }
14
+ end
15
+ end
16
+ File.open('doc/index.html', 'w') do |f|
17
+ f.write <<-EOS
18
+ <html><meta http-equiv="refresh" content="0; url=recap.html">
19
+ EOS
20
+ end
21
+ end
7
22
 
8
23
  desc 'publish docs'
9
24
  task :publish do
10
- sha = `git ls-tree -d HEAD doc | awk '{print $3}'`.strip
11
- commit = `echo "Publishing docs from master branch" | git commit-tree #{sha} -p refs/heads/gh-pages`.strip
12
- `git update-ref refs/heads/gh-pages #{commit}`
25
+ path = "/home/freerange/docs/recap"
26
+ system %{ssh gofreerange.com "sudo rm -fr #{path} && mkdir -p #{path}" && scp -r doc/* gofreerange.com:#{path}}
13
27
  end
14
28
 
15
29
  RSpec::Core::RakeTask.new(:spec) do |t|
@@ -17,4 +31,4 @@ RSpec::Core::RakeTask.new(:spec) do |t|
17
31
  t.rspec_opts = "-fn --color"
18
32
  end
19
33
 
20
- task :default => :spec
34
+ task :default => :spec
data/bin/recap CHANGED
@@ -1,3 +1,3 @@
1
- require 'recap/cli'
2
- Recap::CLI.start
1
+ require 'recap/support/cli'
2
+ Recap::Support::CLI.start
3
3
 
@@ -1,4 +1,3 @@
1
-
2
1
  Feature: Managing processes with foreman
3
2
 
4
3
  Scenario: Running an application process
@@ -7,6 +6,7 @@ Feature: Managing processes with foreman
7
6
  When I run "cap deploy:setup deploy"
8
7
  Then the project should own the running application process
9
8
 
9
+ @wip
10
10
  Scenario: Running processes can read environment variables
11
11
  Given a new ruby project and a bootstrapped server
12
12
  And the project has an application process defined in a Procfile
@@ -6,13 +6,38 @@ Feature: Setting and unsetting environment config variables
6
6
  When I run "cap env:set SECRET=very-secure"
7
7
  Then the variable "SECRET" should be set to "very-secure"
8
8
 
9
- @wip
10
9
  Scenario: Setting an environment variable based on an existing variable
11
10
 
12
11
  Given a new project and a bootstrapped server
13
12
  When I run "cap env:set SUPER_PATH=\$PATH"
14
13
  Then the variable "SUPER_PATH" should be set to the application's PATH
15
14
 
15
+ Scenario: Setting default environment variable values
16
+
17
+ Given a new project and a bootstrapped server
18
+ When I add a default environment variable "PASSWORD" with the value "sup3r-s3cr3t" to the project
19
+ And I run "cap env:set"
20
+ Then the variable "PASSWORD" should be set to "sup3r-s3cr3t"
21
+
22
+ When I run "cap env:set PASSWORD=anoth3r-passw0rd"
23
+ Then the variable "PASSWORD" should be set to "anoth3r-passw0rd"
24
+
25
+ When I run "cap env:set PASSWORD="
26
+ Then the variable "PASSWORD" should be set back to "sup3r-s3cr3t"
27
+
28
+ Scenario: Resetting back to default values
29
+
30
+ Given a new project and a bootstrapped server
31
+ And I add a default environment variable "PASSWORD" with the value "sup3r-s3cr3t" to the project
32
+
33
+ When I run "cap env:set SECRET=something PASSWORD=anoth3r-passw0rd"
34
+ Then the variable "SECRET" should be set to "something"
35
+ And the variable "PASSWORD" should be set to "anoth3r-passw0rd"
36
+
37
+ When I run "cap env:reset"
38
+ Then the variable "PASSWORD" should be set back to "sup3r-s3cr3t"
39
+ And the variable "SECRET" should have no value
40
+
16
41
  Scenario: Unsetting a variable
17
42
 
18
43
  Given a new project and a bootstrapped server
@@ -14,7 +14,7 @@ end
14
14
 
15
15
  Given /^a new (ruby )?project and a bootstrapped server$/ do |project_type|
16
16
  type = (project_type || 'static').strip
17
- start_project server: server, capfile: { recap_require: "recap/#{type}" }
17
+ start_project server: server, capfile: { recap_require: "recap/recipes/#{type}" }
18
18
  project.run_cap 'bootstrap'
19
19
  end
20
20
 
@@ -60,6 +60,10 @@ When /^I wait for the server to start$/ do
60
60
  sleep(5)
61
61
  end
62
62
 
63
+ When /^I add a default environment variable "([^"]*)" with the value "([^"]*)" to the project$/ do |name, value|
64
+ project.add_default_env_value_to_capfile(name, value)
65
+ end
66
+
63
67
  Then /^the project should be deployed$/ do
64
68
  project.deployed_version.should eql(project.latest_version)
65
69
  end
@@ -76,17 +80,17 @@ Then /^the deployed project should include version "([^"]*)" of "([^"]*)"$/ do |
76
80
  project.run_on_server("bin/#{gem} --version").strip.should eql(version)
77
81
  end
78
82
 
79
- Then /^the variable "([^"]*)" should be set to "([^"]*)"$/ do |name, value|
80
- project.run_on_server("sudo su - #{project.name} -c 'env | grep #{name}'").strip.should eql("#{name}=#{value}")
83
+ Then /^the variable "([^"]*)" should be set (?:back )?to "([^"]*)"$/ do |name, value|
84
+ project.run_on_server("sudo su - #{project.name} -c 'env | grep #{name}'", ".").strip.should eql("#{name}=#{value}")
81
85
  end
82
86
 
83
87
  Then /^the variable "([^"]*)" should be set to the application's PATH$/ do |name|
84
- path = project.run_on_server("echo $PATH").strip
85
- project.run_on_server("sudo su - #{project.name} -c 'env | grep #{name}'").strip.should eql("#{name}=#{path}")
88
+ path = project.run_on_server("echo $PATH", ".").strip
89
+ project.run_on_server("sudo su - #{project.name} -c 'env | grep #{name}'", ".").strip.should eql("#{name}=#{path}")
86
90
  end
87
91
 
88
92
  Then /^the variable "([^"]*)" should have no value$/ do |name|
89
- project.run_on_server("sudo su - #{project.name} -c 'env'").include?("#{name}=").should be_false
93
+ project.run_on_server("sudo su - #{project.name} -c 'env'", ".").include?("#{name}=").should be_false
90
94
  end
91
95
 
92
96
  Then /^the project should own the running application process$/ do
@@ -42,7 +42,11 @@ module ProjectSupport
42
42
  def initialize(project, options = {})
43
43
  super('project/Capfile.erb')
44
44
  @project = project
45
- @recap_require = options[:recap_require] || 'recap/static'
45
+ @recap_require = options[:recap_require] || 'recap/recipes/static'
46
+ end
47
+
48
+ def default_environment_values
49
+ @default_environment_values ||= {}
46
50
  end
47
51
  end
48
52
 
@@ -137,10 +141,19 @@ module ProjectSupport
137
141
  end
138
142
 
139
143
  def write_and_commit_file(path, content = "")
144
+ write_file(path, content)
145
+ commit_files(path)
146
+ end
147
+
148
+ def read_file(path)
149
+ full_path = File.join(repository_path, path)
150
+ File.read(full_path)
151
+ end
152
+
153
+ def write_file(path, content = "")
140
154
  full_path = File.join(repository_path, path)
141
155
  FileUtils.mkdir_p File.dirname(full_path)
142
156
  File.write(full_path, content)
143
- commit_files(path)
144
157
  end
145
158
 
146
159
  def commit_files(*paths)
@@ -153,7 +166,7 @@ module ProjectSupport
153
166
  end
154
167
 
155
168
  def deployment_path(path = "")
156
- File.join("/home/#{name}/apps/#{name}", path)
169
+ File.join("/home/#{name}/app", path)
157
170
  end
158
171
 
159
172
  def deployed_version
@@ -165,8 +178,8 @@ module ProjectSupport
165
178
  raise "Exit code returned running 'cap #{command}'" if $?.exitstatus != 0
166
179
  end
167
180
 
168
- def run_on_server(cmd)
169
- @server.run("cd #{deployment_path} && #{cmd}")
181
+ def run_on_server(cmd, path = deployment_path)
182
+ @server.run("cd #{path} && #{cmd}")
170
183
  end
171
184
 
172
185
  def git(command)
@@ -202,6 +215,12 @@ module ProjectSupport
202
215
  write_and_commit_file 'Procfile', Procfile.new(name, command)
203
216
  end
204
217
 
218
+ def add_default_env_value_to_capfile(name, value)
219
+ content = read_file('Capfile')
220
+ content << "\nset_default_env '#{name}', '#{value}'"
221
+ write_and_commit_file('Capfile', content)
222
+ end
223
+
205
224
  def commit_changes
206
225
  write_and_commit_file 'project-file', Faker::Lorem.sentence
207
226
  end
@@ -8,7 +8,7 @@ set :user, 'vagrant'
8
8
  ssh_options[:port] = 2222
9
9
  ssh_options[:keys] = ['<%= project.private_key_path %>']
10
10
 
11
- server '127.0.0.1', :web
11
+ server '127.0.0.1', :app
12
12
 
13
13
  # Each project has its own location shared between the host machine and the VM
14
14
 
@@ -0,0 +1,6 @@
1
+ # The `recap/recipes/rails` builds on the [ruby](ruby.html)
2
+ # recipe, which provides support for both `bundler` and `foreman`.
3
+ require 'recap/recipes/ruby'
4
+
5
+ # It adds to this with a number of rails specific tasks.
6
+ require 'recap/tasks/rails'
@@ -0,0 +1,11 @@
1
+ # Require `recap/recipes/ruby` in your `Capfile` to use the default recap recipies for deploying a
2
+ # Ruby application.
3
+ require 'recap/tasks/deploy'
4
+
5
+ # If your application uses Bundler, `bundle install` will be run automatically when deploying
6
+ # any changes to your `Gemfile`.
7
+ require 'recap/tasks/bundler'
8
+
9
+ # If your application uses Foreman, recap will use that to stop, start and restart your
10
+ # application processes.
11
+ require 'recap/tasks/foreman'
@@ -0,0 +1,3 @@
1
+ # Require `recap/recipes/static` if you are deploying a simple application which doesn't use
2
+ # Rails, Bundler or Foreman.
3
+ require 'recap/tasks/deploy'
@@ -0,0 +1,18 @@
1
+ # There are three main recipes, defined in [recap/recipes/static.rb](recipes/static.html),
2
+ # [recap/recipes/ruby.rb](recipes/ruby.html) and [recap/recipes/rails.rb](recipes/rails.html)
3
+ # that include tasks for static, ruby-based and rails sites respectively. One of these should be
4
+ # required at the top of your `Capfile`.
5
+ #
6
+ # The static recipe includes all the main deployment behaviour. It provides everything you
7
+ # should need to push static content up to one or more servers, as well as the ability to
8
+ # rollback to a previous release if you make a mistake.
9
+ #
10
+ # The ruby recipe builds on this with support for `bundler`, to automatically install any
11
+ # bundled gems. It also includes `foreman` support, starting and restarting processes
12
+ # defined in a `Procfile`.
13
+ #
14
+ # The rails recipe includes all the above, and adds automatic database migration and
15
+ # asset compilation to each deploy.
16
+ #
17
+ # To swap between each of these, simply change the top line of your `Capfile` to require
18
+ # the one you want.
@@ -0,0 +1,85 @@
1
+ require 'tempfile'
2
+
3
+ # These methods are used by recap tasks to run commands and detect when files have changed
4
+ # as part of a deployments
5
+
6
+ module Recap::Support::CapistranoExtensions
7
+ # Run a command as the given user
8
+ def as_user(user, command, pwd = deploy_to)
9
+ sudo "su - #{user} -c 'cd #{pwd} && #{command}'"
10
+ end
11
+
12
+ # Run a command as root
13
+ def as_root(command, pwd = deploy_to)
14
+ as_user 'root', command, pwd
15
+ end
16
+
17
+ # Run a command as the application user
18
+ def as_app(command, pwd = deploy_to)
19
+ as_user application_user, command, pwd
20
+ end
21
+
22
+ # Put a string into a file as the application user
23
+ def put_as_app(string, path)
24
+ put string, "/tmp/recap-put-as-app"
25
+ as_app "cp /tmp/recap-put-as-app #{path} && chmod g+rw #{path}", "/"
26
+ ensure
27
+ run "rm /tmp/recap-put-as-app"
28
+ end
29
+
30
+ def editor
31
+ ENV['DEPLOY_EDITOR'] || ENV['EDITOR']
32
+ end
33
+
34
+ # Edit a file on the remote server, using a local editor
35
+ def edit_file(path)
36
+ if editor
37
+ as_app "touch #{path} && chmod g+rw #{path}"
38
+ local_path = Tempfile.new('deploy-edit').path
39
+ get(path, local_path)
40
+ Recap::Support::ShellCommand.execute_interactive("#{editor} #{local_path}")
41
+ File.read(local_path)
42
+ else
43
+ abort "To edit a remote file, either the EDITOR or DEPLOY_EDITOR environment variables must be set"
44
+ end
45
+ end
46
+
47
+ # Run a git command in the `deploy_to` directory
48
+ def git(command)
49
+ run "cd #{deploy_to} && umask 002 && sg #{application_group} -c \"git #{command}\""
50
+ end
51
+
52
+ # Capture the result of a git command run within the `deploy_to` directory
53
+ def capture_git(command)
54
+ capture "cd #{deploy_to} && umask 002 && sg #{application_group} -c 'git #{command}'"
55
+ end
56
+
57
+ def exit_code(command)
58
+ capture("#{command} > /dev/null 2>&1; echo $?").strip
59
+ end
60
+
61
+ def exit_code_as_app(command, pwd = deploy_to)
62
+ capture(%|sudo -p 'sudo password: ' su - #{application_user} -c 'cd #{pwd} && #{command} > /dev/null 2>&1'; echo $?|).strip
63
+ end
64
+
65
+ # Find the latest tag from the repository. As `git tag` returns tags in order, and our release
66
+ # tags are timestamps, the latest tag will always be the last in the list.
67
+ def latest_tag_from_repository
68
+ result = capture_git("tag | tail -n1").strip
69
+ result.empty? ? nil : result
70
+ end
71
+
72
+ # Does the given file exist within the deployment directory?
73
+ def deployed_file_exists?(path, root_path = deploy_to)
74
+ exit_code("cd #{root_path} && [ -f #{path} ]") == "0"
75
+ end
76
+
77
+ # Has the given path been created or changed since the previous deployment? During the first
78
+ # successful deployment this will always return true.
79
+ def deployed_file_changed?(path)
80
+ return true unless latest_tag
81
+ exit_code("cd #{deploy_to} && git diff --exit-code #{latest_tag} origin/#{branch} #{path}") == "1"
82
+ end
83
+
84
+ Capistrano::Configuration.send :include, self
85
+ end
@@ -0,0 +1,57 @@
1
+ require 'thor'
2
+ require 'recap/support/shell_command'
3
+
4
+ module Recap::Support
5
+
6
+ # Recap provides a simple command-line tool (`recap`) to generate a `Capfile` in your
7
+ # project.
8
+
9
+ class CLI < Thor
10
+ include Thor::Actions
11
+
12
+ attr_accessor :name, :repository, :recipe, :server
13
+
14
+ def self.source_root
15
+ File.expand_path("../templates", __FILE__)
16
+ end
17
+
18
+ desc 'setup', 'Setup basic capistrano recipes, e.g: recap setup'
19
+ method_option :name
20
+ method_option :repository
21
+ method_option :server
22
+ method_option :recipe, :type => 'string', :banner => 'static|ruby|rails'
23
+
24
+ def setup
25
+ self.name = options["name"] || guess_name
26
+ self.repository = options["repo"] || guess_repository
27
+ self.recipe = options["recipe"] || guess_recipe
28
+ self.server = options["server"] || 'your-server-address'
29
+ template 'Capfile.erb', 'Capfile'
30
+ end
31
+
32
+ private
33
+
34
+ def guess_name
35
+ Dir.pwd.split(File::SEPARATOR).last
36
+ end
37
+
38
+ def guess_repository
39
+ ShellCommand.execute('git remote -v').split[1]
40
+ rescue
41
+ warn "Unable to determine git repository. Setting to <unknown>."
42
+ "<unknown>"
43
+ end
44
+
45
+ def guess_recipe
46
+ if File.exist?('Gemfile.lock')
47
+ if File.read('Gemfile.lock') =~ / rails /
48
+ 'rails'
49
+ else
50
+ 'ruby'
51
+ end
52
+ else
53
+ 'static'
54
+ end
55
+ end
56
+ end
57
+ end
@@ -5,8 +5,8 @@
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
- module Recap::Compatibility
9
- extend Recap::Namespace
8
+ module Recap::Support::Compatibility
9
+ extend Recap::Support::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).
@@ -0,0 +1,61 @@
1
+ module Recap::Support
2
+
3
+ # This class is used to manipulate environment variables on the remote server.
4
+ # You should not need to use it directly; you are probably looking for the
5
+ # [env](../tasks/env.html) tasks instead.
6
+
7
+ class Environment
8
+ def initialize(variables = {})
9
+ @variables = variables
10
+ end
11
+
12
+ def get(name)
13
+ @variables[name]
14
+ end
15
+
16
+ def set(name, value)
17
+ if value.nil? || value.empty?
18
+ @variables.delete(name)
19
+ else
20
+ @variables[name] = value
21
+ end
22
+ end
23
+
24
+ def set_string(string)
25
+ if string =~ /\A([A-Za-z0-9_]+)=(.*)\z/
26
+ set $1, $2
27
+ end
28
+ end
29
+
30
+ def empty?
31
+ @variables.empty?
32
+ end
33
+
34
+ def merge(hash)
35
+ hash.each {|k, v| set(k, v)}
36
+ end
37
+
38
+ def each(&block)
39
+ @variables.sort.each(&block)
40
+ end
41
+
42
+ def include?(key)
43
+ @variables.include?(key)
44
+ end
45
+
46
+ def to_s
47
+ @variables.keys.sort.map do |key|
48
+ key + "=" + @variables[key] + "\n" if @variables[key]
49
+ end.compact.join
50
+ end
51
+
52
+ class << self
53
+ def from_string(string)
54
+ string.split(/[\n\r]/).inject(new) do |env, line|
55
+ env.set_string(line)
56
+ env
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ require 'capistrano'
2
+ require 'recap/support/capistrano_extensions'
3
+
4
+ # This module is used to capture the definition of capistrano tasks, which makes it
5
+ # easier to test the behaviour of specific tasks without loading everything. If you
6
+ # are writing tests for a collection of tasks, you should put those tasks in a module
7
+ # and extend that module with `Recap::Support::Namespace.
8
+ #
9
+ # You can look at some of the existing tasks (such as [env](../tasks/env.html)) and
10
+ # its corresponding specs for an example of this in practice.
11
+ #
12
+ # You should not need to use this module directly when using recap to deploy.
13
+
14
+ module Recap::Support::Namespace
15
+ def self.default_config
16
+ @default_config
17
+ end
18
+
19
+ def self.default_config=(config)
20
+ @default_config = config
21
+ end
22
+
23
+ if Capistrano::Configuration.instance
24
+ self.default_config = Capistrano::Configuration.instance(:must_exist)
25
+ end
26
+
27
+ def capistrano_definitions
28
+ @capistrano_definitions ||= []
29
+ end
30
+
31
+ def namespace(name, &block)
32
+ capistrano_definitions << Proc.new do
33
+ namespace name do
34
+ instance_eval(&block)
35
+ end
36
+ end
37
+
38
+ load_into(Recap::Support::Namespace.default_config) if Recap::Support::Namespace.default_config
39
+ end
40
+
41
+ def load_into(configuration)
42
+ configuration.extend(self)
43
+ capistrano_definitions.each do |definition|
44
+ configuration.load(&definition)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ require 'open4'
2
+
3
+ module Recap::Support
4
+ class ShellCommand
5
+ def self.execute(*commands)
6
+ output, error = "", ""
7
+ commands.each do |command|
8
+ status = Open4::popen4(command) do |pid, stdin, stdout, stderr|
9
+ output, error = stdout.read, stderr.read
10
+ end
11
+ unless status.success?
12
+ message = [
13
+ "Executing shell command failed.",
14
+ " Command: #{command}",
15
+ " Status: #{status.exitstatus}",
16
+ " Message: #{error}"
17
+ ].join("\n")
18
+ raise message
19
+ end
20
+ end
21
+ output
22
+ end
23
+
24
+ def self.execute_interactive(command)
25
+ unless system(command)
26
+ message = [
27
+ "Executing shell command failed.",
28
+ " Command: #{command}",
29
+ " Status: #{$?.exitstatus}"
30
+ ].join("\n")
31
+ raise message
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ require 'recap/recipes/<%= recipe %>'
2
+
3
+ set :application, '<%= name %>'
4
+ set :repository, '<%= repository %>'
5
+
6
+ server '<%= server %>', :app