recap 0.2.0 → 1.0.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 (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