1
0
Fork 0
mirror of https://github.com/capistrano/capistrano synced 2023-03-27 23:21:18 -04:00

Merge branch '3.1.x'

Conflicts:
	CHANGELOG.md
	README.md
	lib/capistrano/tasks/git.rake
	lib/capistrano/templates/deploy.rb.erb
This commit is contained in:
seenmyfate 2013-11-02 11:08:10 +00:00
commit 22a98f30a6
32 changed files with 378 additions and 55 deletions

View file

@ -2,9 +2,20 @@
Reverse Chronological Order:
## `3.1.0` (not released)
* `deploy:restart` task is no longer run by default.
From this version, developers who restart the app on each deploy need to declare it in their deploy flow (@kirs)
* Fixed bug when `deploy:cleanup` was executed twice by default (@kirs)
* Config location can now be changed with `deploy_config_path` and `stage_config_path` options (@seenmyfate)
* `no_release` option is now available (@seenmyfate)
* Raise an error if developer tries to define `:all` role, which is reserved (@kirs)
* `deploy:fallback` hook was added to add some custom behaviour on failed deploy (@seenmyfate)
* Correctly infer namespace in task enhancements (@seenmyfate)
## `3.0.1`
* capify' not listed as executable (@leehambley)
* `capify` not listed as executable (@leehambley)
* Confirm license as MIT (@leehambley)
* Move the git ssh helper to application path (@mpapis)

View file

@ -126,7 +126,7 @@ This method is widely used.
``` ruby
desc "Ask about breakfast"
task :breakfast do
breakfast = ask(:breakfast, "What would you like your colleagues to get you for breakfast?")
breakfast = ask(:breakfast, "What would you like your colleagues to bring you for breakfast?")
on roles(:all) do |h|
execute "echo \"$(whoami) wants #{breakfast} for breakfast!\" | wall"
end

View file

@ -0,0 +1,15 @@
Feature: The path to the configuration can be changed, removing the need to
follow Ruby/Rails conventions
Background:
Given a test app with the default configuration
And servers with the roles app and web
Scenario: Deploying with configuration in default location
When I run "cap test"
Then the task is successful
Scenario: Deploying with configuration in a custom location
But the configuration is in a custom location
When I run "cap test"
Then the task is successful

View file

@ -34,6 +34,7 @@ Feature: Deploy
Then the task will be successful
Scenario: Creating a release
Given I run cap "deploy:check:directories"
When I run cap "git:create_release" as part of a release
Then the repo is cloned
And the release is created

View file

@ -0,0 +1,17 @@
Feature: Deploy failure
Background:
Given a test app with the default configuration
And a custom task that will simulate a failure
And a custom task to run in the event of a failure
And servers with the roles app and web
Scenario: Triggering the custom task
When I run cap "deploy:starting"
But an error is raised
Then the failure task will not run
Scenario: Triggering the custom task
When I run cap "deploy"
But an error is raised
Then the failure task will run

View file

@ -88,3 +88,22 @@ end
Then(/^it will not recreate the file$/) do
#
end
Then(/^the task is successful$/) do
expect(@success).to be_true
end
Then(/^the failure task will run$/) do
failed = TestApp.shared_path.join('failed')
run_vagrant_command(test_file_exists(failed))
end
Then(/^the failure task will not run$/) do
failed = TestApp.shared_path.join('failed')
!run_vagrant_command(test_file_exists(failed))
end
When(/^an error is raised$/) do
error = TestApp.shared_path.join('fail')
run_vagrant_command(test_file_exists(error))
end

View file

@ -1,8 +1,12 @@
When(/^I run cap "(.*?)"$/) do |task|
TestApp.cap(task)
@success = TestApp.cap(task)
end
When(/^I run cap "(.*?)" as part of a release$/) do |task|
TestApp.cap("deploy:new_release_path #{task}")
end
When(/^I run "(.*?)"$/) do |command|
@success = TestApp.run(command)
end

View file

@ -23,3 +23,16 @@ Given(/^a custom task to generate a file$/) do
TestApp.copy_task_to_test_app('spec/support/tasks/database.cap')
end
Given(/^the configuration is in a custom location$/) do
TestApp.move_configuration_to_custom_location('app')
end
Given(/^a custom task that will simulate a failure$/) do
safely_remove_file(TestApp.shared_path.join('failed'))
TestApp.copy_task_to_test_app('spec/support/tasks/fail.cap')
end
Given(/^a custom task to run in the event of a failure$/) do
safely_remove_file(TestApp.shared_path.join('failed'))
TestApp.copy_task_to_test_app('spec/support/tasks/failed.cap')
end

View file

@ -15,6 +15,10 @@ module RemoteCommandHelpers
def exists?(type, path)
%{[ -#{type} "#{path}" ] && echo "#{path} exists." || echo "Error: #{path} does not exist."}
end
def safely_remove_file(path)
run_vagrant_command("rm #{test_file}") rescue Vagrant::Errors::VagrantError
end
end
World(RemoteCommandHelpers)

View file

@ -30,6 +30,14 @@ module Capistrano
end
end
def exit_because_of_exception(ex)
if deploying?
exit_deploy_because_of_exception(ex)
else
super
end
end
private
# allows the `cap install` task to load without a capfile

View file

@ -34,6 +34,10 @@ module Capistrano
end
def role(name, hosts, options={})
if name == :all
raise ArgumentError.new("#{name} reserved name for role. Please choose another name")
end
servers.add_role(name, hosts, options)
end

View file

@ -23,7 +23,7 @@ module Capistrano
end
def select?(options)
selector = Selector.new(options)
selector = Selector.for(options)
selector.call(self)
end
@ -103,6 +103,14 @@ module Capistrano
@options = options
end
def self.for(options)
if options.has_key?(:exclude)
Exclusive
else
self
end.new(options)
end
def callable
if key.respond_to?(:call)
key
@ -126,6 +134,17 @@ module Capistrano
->(server) { :all }
end
class Exclusive < Selector
def key
options[:exclude]
end
def call(server)
!callable.call(server)
end
end
end
end

View file

@ -43,6 +43,16 @@ module Capistrano
env.roles_for(names)
end
def release_roles(*names)
options = { exclude: :no_release }
if names.last.is_a? Hash
names.last.merge(options)
else
names << options
end
roles(*names)
end
def primary(role)
env.primary(role)
end

View file

@ -27,6 +27,14 @@ module Capistrano
set(:release_path, releases_path.join(timestamp))
end
def stage_config_path
Pathname.new fetch(:stage_config_path, 'config/deploy')
end
def deploy_config_path
Pathname.new fetch(:deploy_config_path, 'config/deploy.rb')
end
def repo_url
require 'cgi'
require 'uri'

View file

@ -3,7 +3,14 @@ module Capistrano
module Stages
def stages
Dir['config/deploy/*.rb'].map { |f| File.basename(f, '.rb') }
Dir[stage_definitions].map { |f| File.basename(f, '.rb') }
end
def infer_stages_from_stage_files
end
def stage_definitions
stage_config_path.join('*.rb')
end
def stage_set?

View file

@ -6,9 +6,10 @@ module Capistrano
end
def after(task, post_task, *args, &block)
post_task = Rake::Task.define_task(post_task, *args, &block) if block_given?
Rake::Task.define_task(post_task, *args, &block) if block_given?
post_task = Rake::Task[post_task]
Rake::Task[task].enhance do
invoke(post_task)
post_task.invoke
end
end
@ -49,5 +50,15 @@ module Capistrano
%w{install}
end
def exit_deploy_because_of_exception(ex)
warn t(:deploy_failed, ex: ex.inspect)
invoke 'deploy:failed'
exit(false)
end
def deploying?
fetch(:deploying, false)
end
end
end

View file

@ -7,7 +7,6 @@ en = {
start: 'Start',
update: 'Update',
finalize: 'Finalise',
restart: 'Restart',
finishing: 'Finishing',
finished: 'Finished',
stage_not_set: 'Stage not set, please call something such as `cap production deploy`, where production is a stage you have defined.',
@ -19,6 +18,7 @@ en = {
mirror_exists: "The repository mirror is at %{at}",
revision_log_message: 'Branch %{branch} deployed as release %{release} by %{user}',
rollback_log_message: '%{user} rolled back to release %{release}',
deploy_failed: 'The deploy has failed with an error: %{ex}',
console: {
welcome: 'capistrano console - enter command to execute on %{stage}',
bye: 'bye'

View file

@ -8,11 +8,12 @@ end
stages.each do |stage|
Rake::Task.define_task(stage) do
invoke 'load:defaults'
load 'config/deploy.rb'
load "config/deploy/#{stage}.rb"
load "capistrano/#{fetch(:scm)}.rb"
set(:stage, stage.to_sym)
invoke 'load:defaults'
load deploy_config_path
load stage_config_path.join("#{stage}.rb")
load "capistrano/#{fetch(:scm)}.rb"
I18n.locale = fetch(:locale, :en)
configure_backend
end

View file

@ -15,7 +15,6 @@ namespace :deploy do
task :publishing do
invoke 'deploy:symlink:release'
invoke 'deploy:restart'
end
task :finishing do
@ -42,7 +41,7 @@ namespace :deploy do
namespace :check do
desc 'Check shared and release directories exist'
task :directories do
on roles :all do
on release_roles :all do
execute :mkdir, '-pv', shared_path, releases_path
end
end
@ -80,7 +79,7 @@ namespace :deploy do
namespace :symlink do
desc 'Symlink release to current'
task :release do
on roles :all do
on release_roles :all do
execute :rm, '-rf', current_path
execute :ln, '-s', release_path, current_path
end
@ -133,7 +132,7 @@ namespace :deploy do
desc 'Clean up old releases'
task :cleanup do
on roles :all do |host|
on release_roles :all do |host|
releases = capture(:ls, '-x', releases_path).split
if releases.count >= fetch(:keep_releases)
info t(:keeping_releases, host: host.to_s, keep_releases: fetch(:keep_releases), releases: releases.count)
@ -201,4 +200,7 @@ namespace :deploy do
end
end
task :restart
task :failed
end

View file

@ -57,6 +57,7 @@ end
desc 'Deploy a new release.'
task :deploy do
set(:deploying, true)
%w{ starting started
updating updated
publishing published

View file

@ -9,7 +9,7 @@ namespace :git do
desc 'Upload the git wrapper script, this script guarantees that we can script git without getting an interactive prompt'
task :wrapper do
on roles :all do
on release_roles :all do
execute :mkdir, "-p", "#{fetch(:tmp_dir)}/#{fetch(:application)}/"
upload! StringIO.new("#!/bin/sh -e\nexec /usr/bin/ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no \"$@\"\n"), "#{fetch(:tmp_dir)}/#{fetch(:application)}/git-ssh.sh"
execute :chmod, "+x", "#{fetch(:tmp_dir)}/#{fetch(:application)}/git-ssh.sh"
@ -19,7 +19,7 @@ namespace :git do
desc 'Check that the repository is reachable'
task check: :'git:wrapper' do
fetch(:branch)
on roles :all do
on release_roles :all do
with fetch(:git_environmental_variables) do
exit 1 unless test :git, :'ls-remote', repo_url
end
@ -28,7 +28,7 @@ namespace :git do
desc 'Clone the repo to the cache'
task clone: :'git:wrapper' do
on roles :all do
on release_roles :all do
if test " [ -f #{repo_path}/HEAD ] "
info t(:mirror_exists, at: repo_path)
else
@ -43,7 +43,7 @@ namespace :git do
desc 'Update the repo mirror to reflect the origin state'
task update: :'git:clone' do
on roles :all do
on release_roles :all do
within repo_path do
execute :git, :remote, :update
end
@ -52,7 +52,7 @@ namespace :git do
desc 'Copy repo to releases'
task create_release: :'git:update' do
on roles :all do
on release_roles :all do
with fetch(:git_environmental_variables) do
within repo_path do
execute :mkdir, '-p', release_path

View file

@ -1,14 +1,14 @@
namespace :hg do
desc 'Check that the repo is reachable'
task :check do
on roles :all do
on release_roles :all do
execute "hg", "id", repo_url
end
end
desc 'Clone the repo to the cache'
task :clone do
on roles :all do
on release_roles :all do
if test " [ -d #{repo_path}/.hg ] "
info t(:mirror_exists, at: repo_path)
else
@ -21,7 +21,7 @@ namespace :hg do
desc 'Pull changes from the remote repo'
task :update => :'hg:clone' do
on roles :all do
on release_roles :all do
within repo_path do
execute "hg", "pull"
end
@ -30,7 +30,7 @@ namespace :hg do
desc 'Copy repo to releases'
task :create_release => :'hg:update' do
on roles :all do
on release_roles :all do
within repo_path do
execute "hg", "archive", release_path, "--rev", fetch(:branch)
end

View file

@ -26,6 +26,8 @@ namespace :deploy do
end
end
after :publishing, :restart
after :restart, :clear_cache do
on roles(:web), in: :groups, limit: 3, wait: 10 do
# Here we can do anything such as:
@ -35,6 +37,4 @@ namespace :deploy do
end
end
after :finishing, 'deploy:cleanup'
end

View file

@ -1,10 +1,9 @@
set :stage, :<%= stage %>
# Simple Role Syntax
# ==================
# Supports bulk-adding hosts to roles, the primary
# server in each group is considered to be the first
# unless any hosts have the primary property set.
# Don't declare `role :all`, it's a meta role
role :app, %w{deploy@example.com}
role :web, %w{deploy@example.com}
role :db, %w{deploy@example.com}

View file

@ -11,13 +11,33 @@ describe Capistrano::DSL do
dsl.server 'example2.com', roles: %w{web}
dsl.server 'example3.com', roles: %w{app web}, active: true
dsl.server 'example4.com', roles: %w{app}, primary: true
dsl.server 'example5.com', roles: %w{db}, no_release: true
end
describe 'fetching all servers' do
subject { dsl.roles(:all) }
it 'returns all servers' do
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com}
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com example5.com}
end
end
describe 'fetching all release servers' do
context 'with no additional options' do
subject { dsl.release_roles(:all) }
it 'returns all release servers' do
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com}
end
end
context 'with filter options' do
subject { dsl.release_roles(:all, filter: :active) }
it 'returns all release servers that match the filter' do
expect(subject.map(&:hostname)).to eq %w{example1.com example3.com}
end
end
end
@ -70,6 +90,14 @@ describe Capistrano::DSL do
end
describe 'when defining role with reserved name' do
it 'fails with ArgumentError' do
expect {
dsl.role :all, %w{example1.com}
}.to raise_error(ArgumentError, "all reserved name for role. Please choose another name")
end
end
describe 'when defining hosts using the `role` syntax' do
before do
dsl.role :web, %w{example1.com example2.com example3.com}
@ -77,16 +105,37 @@ describe Capistrano::DSL do
dsl.role :app, %w{example3.com example4.com}
dsl.role :app, %w{example3.com}, active: true
dsl.role :app, %w{example4.com}, primary: true
dsl.role :db, %w{example5.com}, no_release: true
end
describe 'fetching all servers' do
subject { dsl.roles(:all) }
it 'returns all servers' do
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com}
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com example5.com}
end
end
describe 'fetching all release servers' do
context 'with no additional options' do
subject { dsl.release_roles(:all) }
it 'returns all release servers' do
expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com}
end
end
context 'with filter options' do
subject { dsl.release_roles(:all, filter: :active) }
it 'returns all release servers that match the filter' do
expect(subject.map(&:hostname)).to eq %w{example1.com example3.com}
end
end
end
describe 'fetching servers by role' do
subject { dsl.roles(:app) }
@ -340,5 +389,53 @@ describe Capistrano::DSL do
end
end
describe 'setting deploy configuration path' do
subject { dsl.deploy_config_path.to_s }
context 'where no config path is set' do
before do
dsl.delete(:deploy_config_path)
end
it 'returns "config/deploy.rb"' do
expect(subject).to eq 'config/deploy.rb'
end
end
context 'where a custom path is set' do
before do
dsl.set(:deploy_config_path, 'my/custom/path.rb')
end
it 'returns the custom path' do
expect(subject).to eq 'my/custom/path.rb'
end
end
end
describe 'setting stage configuration path' do
subject { dsl.stage_config_path.to_s }
context 'where no config path is set' do
before do
dsl.delete(:stage_config_path)
end
it 'returns "config/deploy"' do
expect(subject).to eq 'config/deploy'
end
end
context 'where a custom path is set' do
before do
dsl.set(:stage_config_path, 'my/custom/path')
end
it 'returns the custom path' do
expect(subject).to eq 'my/custom/path'
end
end
end
end
end

View file

@ -159,6 +159,11 @@ module Capistrano
let(:options) { { select: :active }}
it { should be_true }
end
context 'with :exclude' do
let(:options) { { exclude: :active }}
it { should be_false }
end
end
context 'value does not match server properly' do
@ -171,6 +176,11 @@ module Capistrano
let(:options) { { select: :inactive }}
it { should be_false }
end
context 'with :exclude' do
let(:options) { { exclude: :inactive }}
it { should be_true }
end
end
end
@ -186,6 +196,12 @@ module Capistrano
let(:options) { { select: ->(s) { s.properties.active } } }
it { should be_true }
end
context 'with :exclude' do
let(:options) { { exclude: ->(s) { s.properties.active } } }
it { should be_false }
end
end
context 'value does not match server properly' do
@ -198,6 +214,12 @@ module Capistrano
let(:options) { { select: ->(s) { s.properties.inactive } } }
it { should be_false }
end
context 'with :exclude' do
let(:options) { { exclude: ->(s) { s.properties.inactive } } }
it { should be_true }
end
end
end

View file

@ -141,6 +141,35 @@ module Capistrano
end
describe 'excluding by property' do
before do
servers.add_host('1', roles: :app, active: true)
servers.add_host('2', roles: :app, active: true, no_release: true)
end
it 'is empty if the filter would remove all matching hosts' do
hosts = servers.roles_for([:app, exclude: :active])
expect(hosts.map(&:hostname)).to be_empty
end
it 'returns the servers without the attributes specified' do
hosts = servers.roles_for([:app, exclude: :no_release])
expect(hosts.map(&:hostname)).to eq %w{1}
end
it 'can exclude hosts by properties on the host using a regular proc' do
hosts = servers.roles_for([:app, exclude: ->(h) { h.properties.no_release }])
expect(hosts.map(&:hostname)).to eq %w{1}
end
it 'is empty if the regular proc filter would remove all matching hosts' do
hosts = servers.roles_for([:app, exclude: ->(h) { h.properties.active }])
expect(hosts.map(&:hostname)).to be_empty
end
end
describe 'filtering roles' do
before do

View file

@ -1,10 +0,0 @@
require 'spec_helper'
module Capistrano
module DSL
describe Env do
end
end
end

View file

@ -20,17 +20,6 @@ module Capistrano
end
end
describe '#stages' do
before do
Dir.expects(:[]).with('config/deploy/*.rb').
returns(['config/deploy/staging.rb', 'config/deploy/production.rb'])
end
it 'returns a list of defined stages' do
expect(dsl.stages).to eq %w{staging production}
end
end
describe '#stage_set?' do
subject { dsl.stage_set? }

View file

@ -0,0 +1,7 @@
set :fail, proc { fail }
before 'deploy:starting', :fail do
on roles :all do
execute :touch, shared_path.join('fail')
end
fetch(:fail)
end

View file

@ -0,0 +1,5 @@
after 'deploy:failed', :failed do
on roles :all do
execute :touch, shared_path.join('failed')
end
end

View file

@ -8,10 +8,9 @@ module TestApp
def default_config
%{
set :stage, :#{stage}
set :deploy_to, '#{deploy_to}'
set :repo_url, 'git://github.com/capistrano/capistrano.git'
set :branch, 'v3'
set :branch, 'master'
set :ssh_options, { keys: "\#{ENV['HOME']}/.vagrant.d/insecure_private_key" }
server 'vagrant@localhost:2220', roles: %w{web app}
set :linked_files, #{linked_files}
@ -58,6 +57,14 @@ module TestApp
end
end
def prepend_to_capfile(config)
current_capfile = File.read(capfile)
File.open(capfile, 'w') do |file|
file.write config
file.write current_capfile
end
end
def create_shared_directory(path)
FileUtils.mkdir_p(shared_path.join(path))
end
@ -67,9 +74,14 @@ module TestApp
end
def cap(task)
run "bundle exec cap #{stage} #{task}"
end
def run(command)
Dir.chdir(test_app_path) do
%x[bundle exec cap #{stage} #{task}]
%x[#{command}]
end
$?.success?
end
def stage
@ -135,4 +147,22 @@ module TestApp
def copy_task_to_test_app(source)
FileUtils.cp(source, task_dir)
end
def config_path
test_app_path.join('config')
end
def move_configuration_to_custom_location(location)
prepend_to_capfile(
%{
set :stage_config_path, "app/config/deploy"
set :deploy_config_path, "app/config/deploy.rb"
}
)
location = test_app_path.join(location)
FileUtils.mkdir_p(location)
FileUtils.mv(config_path, location)
end
end