recap 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/README.md +55 -10
- data/Rakefile +19 -5
- data/bin/recap +2 -2
- data/features/managing-processes.feature +1 -1
- data/features/setting-environment-variables.feature +26 -1
- data/features/steps/capistrano_steps.rb +10 -6
- data/features/support/project.rb +24 -5
- data/features/templates/project/Capfile.erb +1 -1
- data/lib/recap/recipes/rails.rb +6 -0
- data/lib/recap/recipes/ruby.rb +11 -0
- data/lib/recap/recipes/static.rb +3 -0
- data/lib/recap/recipes.rb +18 -0
- data/lib/recap/support/capistrano_extensions.rb +85 -0
- data/lib/recap/support/cli.rb +57 -0
- data/lib/recap/{compatibility.rb → support/compatibility.rb} +2 -2
- data/lib/recap/support/environment.rb +61 -0
- data/lib/recap/support/namespace.rb +47 -0
- data/lib/recap/support/shell_command.rb +35 -0
- data/lib/recap/support/templates/Capfile.erb +6 -0
- data/lib/recap/tasks/bootstrap.rb +77 -0
- data/lib/recap/{bundler.rb → tasks/bundler.rb} +15 -6
- data/lib/recap/{deploy.rb → tasks/deploy.rb} +30 -17
- data/lib/recap/tasks/env.rb +111 -0
- data/lib/recap/{foreman.rb → tasks/foreman.rb} +20 -12
- data/lib/recap/{preflight.rb → tasks/preflight.rb} +13 -11
- data/lib/recap/tasks/rails.rb +42 -0
- data/lib/recap/tasks.rb +16 -0
- data/lib/recap/version.rb +1 -1
- data/lib/recap.rb +119 -10
- data/recap.gemspec +3 -2
- data/spec/models/capistrano_extensions_spec.rb +41 -0
- data/spec/models/cli_spec.rb +25 -0
- data/spec/models/environment_spec.rb +14 -14
- data/spec/models/shell_command_spec.rb +55 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/tasks/bootstrap_spec.rb +9 -13
- data/spec/tasks/bundler_spec.rb +39 -7
- data/spec/tasks/deploy_spec.rb +42 -26
- data/spec/tasks/env_spec.rb +81 -5
- data/spec/tasks/foreman_spec.rb +10 -5
- data/spec/tasks/rails_spec.rb +80 -0
- metadata +65 -57
- data/doc/index.html +0 -235
- data/doc/lib/recap/bootstrap.html +0 -42
- data/doc/lib/recap/bundler.html +0 -168
- data/doc/lib/recap/capistrano_extensions.html +0 -208
- data/doc/lib/recap/cli.html +0 -42
- data/doc/lib/recap/compatibility.html +0 -73
- data/doc/lib/recap/deploy.html +0 -328
- data/doc/lib/recap/env.html +0 -108
- data/doc/lib/recap/foreman.html +0 -42
- data/doc/lib/recap/namespace.html +0 -42
- data/doc/lib/recap/preflight.html +0 -163
- data/doc/lib/recap/rails.html +0 -42
- data/doc/lib/recap/version.html +0 -42
- data/doc/lib/recap.html +0 -42
- data/index.rb +0 -62
- data/lib/recap/bootstrap.rb +0 -47
- data/lib/recap/capistrano_extensions.rb +0 -74
- data/lib/recap/cli.rb +0 -32
- data/lib/recap/deploy/templates/Capfile.erb +0 -6
- data/lib/recap/env.rb +0 -58
- data/lib/recap/environment.rb +0 -54
- data/lib/recap/namespace.rb +0 -37
- data/lib/recap/rails.rb +0 -24
- data/lib/recap/ruby.rb +0 -3
- data/lib/recap/static.rb +0 -1
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,59 @@
|
|
1
|
-
|
1
|
+
# Recap
|
2
2
|
|
3
|
-
Recap's
|
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
|
-
|
6
|
+
## Features & Aims
|
13
7
|
|
14
|
-
|
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
|
-
|
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
|
-
|
11
|
-
|
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
|
data/features/support/project.rb
CHANGED
@@ -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}/
|
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 #{
|
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
|
@@ -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,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
|