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.
- 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
|