capistrano_deploy_lock 1.0.0 → 1.1.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/README.md +51 -16
- data/bin/cap_deploy_lock_msg +22 -0
- data/lib/capistrano/date_helper.rb +7 -0
- data/lib/capistrano/deploy_lock.rb +25 -116
- data/lib/capistrano/recipes/deploy_lock.rb +156 -0
- data/lib/capistrano_deploy_lock/version.rb +1 -1
- metadata +9 -5
data/README.md
CHANGED
@@ -17,10 +17,11 @@ Add this line to your `config/deploy.rb`:
|
|
17
17
|
|
18
18
|
require 'capistrano/deploy_lock'
|
19
19
|
|
20
|
+
|
20
21
|
## Usage
|
21
22
|
|
22
23
|
Your deploys will now be protected by a lock. Simply run `cap deploy` as usual.
|
23
|
-
However, if someone else
|
24
|
+
However, if someone else tries to deploy at the same time, their deploy will abort
|
24
25
|
with an error like this:
|
25
26
|
|
26
27
|
```
|
@@ -30,34 +31,39 @@ with an error like this:
|
|
30
31
|
.../capistrano/deploy_lock.rb:132:in `block (3 levels) in <top (required)>': Capistrano::DeployLockedError (Capistrano::DeployLockedError)
|
31
32
|
```
|
32
33
|
|
33
|
-
The default
|
34
|
-
This is so that crashed or interrupted deploys don't leave a stale lock for the next developer to deal with.
|
35
|
-
This default expiry time can be configured with:
|
34
|
+
The default deploy lock will expire after 15 minutes. This is so that crashed or interrupted deploys don't leave a stale lock behind.
|
36
35
|
|
37
|
-
|
36
|
+
The following tasks will be run before deploy:
|
38
37
|
|
39
|
-
|
38
|
+
* `deploy:check_lock`
|
39
|
+
* Checks for an existing deploy lock. Aborts deploy if a lock exists and it wasn't created by you.
|
40
|
+
* `deploy:refresh_lock`
|
41
|
+
* If you previously created a lock, this task ensures that your lock won't expire before the default expiry time
|
42
|
+
* `deploy:create_lock`
|
43
|
+
* If no locks already exist, a default lock will be created with the message: `Deploying <branch>`
|
40
44
|
|
41
|
-
|
45
|
+
The following task will be run after deploy:
|
42
46
|
|
43
|
-
|
47
|
+
* `deploy:unlock`
|
48
|
+
* Removes any default deploy locks. If you set a custom lock, it will not be removed at this step.
|
49
|
+
* You can remove a custom deploy lock by running `cap deploy:unlock` by itself, or by chaining `deploy:unlock:force` at the end of the command.
|
44
50
|
|
45
|
-
set :deploy_lockfile, "path/to/deploy/lock/file"
|
46
51
|
|
52
|
+
## Tasks
|
47
53
|
|
48
|
-
|
54
|
+
### `deploy:with_lock`
|
49
55
|
|
50
|
-
|
56
|
+
Deploy the latest revision with a custom deploy lock. This lock will not be removed at the end of the deploy.
|
51
57
|
|
52
|
-
|
58
|
+
### `deploy:lock`
|
53
59
|
|
54
|
-
You will receive two prompts:
|
60
|
+
Sets a custom deploy lock. You will receive two prompts for input:
|
55
61
|
|
56
|
-
* Lock Message
|
62
|
+
* **Lock Message:**
|
57
63
|
|
58
64
|
Type the reason for the lock. This message will be displayed to any developers who attempt to deploy.
|
59
65
|
|
60
|
-
* Expire lock at? (optional)
|
66
|
+
* **Expire lock at? (optional):**
|
61
67
|
|
62
68
|
Set an expiry time for the lock. Leave this blank to make the lock last until someone removes it with `cap deploy:unlock`.
|
63
69
|
|
@@ -65,7 +71,36 @@ If the [chronic](https://github.com/mojombo/chronic) gem is available, you can t
|
|
65
71
|
natural language times like `2 hours`, or `tomorrow at 6am`. If not, you must type times in a format that `DateTime.parse()` can handle,
|
66
72
|
such as `06:30:00` or `2012-12-12 00:00:00`.
|
67
73
|
|
68
|
-
|
74
|
+
### `deploy:unlock`
|
75
|
+
|
76
|
+
Removes the deploy lock. Will not remove a custom deploy lock when it is chained after `deploy:lock`.
|
77
|
+
|
78
|
+
### `deploy:unlock:force`
|
79
|
+
|
80
|
+
Removes any deploy lock, even when chained after `deploy:lock`.
|
81
|
+
|
82
|
+
### `deploy:check_lock`
|
83
|
+
|
84
|
+
Check if server is locked. If the deploy lock was not created by you, an error will be raised and the deploy will abort.
|
85
|
+
If the lock **was** created by you, the deploy will pause for 4 seconds, which gives you time to press `Ctrl+C` to cancel the deploy.
|
86
|
+
|
87
|
+
This task is also responsible for deleting any expired locks.
|
88
|
+
|
89
|
+
### `deploy:refresh_lock`
|
90
|
+
|
91
|
+
Refreshes the current lock's expiry time if it is less than the default time.
|
92
|
+
|
93
|
+
|
94
|
+
## Configuration
|
95
|
+
|
96
|
+
If your deploys usually take longer than 15 minutes, you can configure the default expiry time with:
|
97
|
+
|
98
|
+
set :default_lock_expiry, (20 * 60) # Sets the default expiry to 20 minutes
|
99
|
+
|
100
|
+
The lock file will be created at `#{shared_path}/capistrano.lock.yml` by default. You can configure this with:
|
101
|
+
|
102
|
+
set :deploy_lockfile, "path/to/deploy/lock/file"
|
103
|
+
|
69
104
|
|
70
105
|
## Thanks
|
71
106
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
if ARGV.size != 3
|
3
|
+
puts "Usage: cap_deploy_lock_msg <application_name> <stage> <path/to/lock_file.yml>"
|
4
|
+
else
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
6
|
+
require 'rubygems'
|
7
|
+
require 'yaml'
|
8
|
+
require 'capistrano/deploy_lock'
|
9
|
+
|
10
|
+
application, stage, lock_file = *ARGV
|
11
|
+
if File.exists?(lock_file)
|
12
|
+
deploy_lock = YAML.load_file(lock_file)
|
13
|
+
|
14
|
+
# Only show lock message if lock hasn't expired
|
15
|
+
if deploy_lock[:expire_at] && deploy_lock[:expire_at] > Time.now
|
16
|
+
puts Capistrano::DeployLock.message(application, stage, deploy_lock)
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "No deploy locks for #{application} (#{stage})"
|
22
|
+
end
|
@@ -9,130 +9,39 @@ begin; require 'chronic'; rescue LoadError; end
|
|
9
9
|
begin
|
10
10
|
# Use Rails distance_of_time_in_words_to_now helper if available
|
11
11
|
require 'action_view'
|
12
|
-
|
13
|
-
class DateHelper
|
14
|
-
class << self
|
15
|
-
include ActionView::Helpers::DateHelper
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
12
|
+
require 'capistrano/date_helper'
|
19
13
|
rescue LoadError
|
20
14
|
end
|
21
15
|
|
22
|
-
Capistrano
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# Show lock message as bright red
|
34
|
-
log_formatter(:match => /Deploy locked/, :color => :red, :style => :bright, :priority => 20)
|
35
|
-
|
36
|
-
namespace :deploy do
|
37
|
-
# Set deploy lock with a custom lock message and expiry time
|
38
|
-
task :lock do
|
39
|
-
set :lock_message, Capistrano::CLI.ui.ask("Lock Message: ")
|
40
|
-
|
41
|
-
while self[:lock_expiry].nil?
|
42
|
-
expiry_str = Capistrano::CLI.ui.ask("Expire lock at? (optional): ")
|
43
|
-
if expiry_str == ""
|
44
|
-
# Never expire an explicit lock if no time given
|
45
|
-
set :lock_expiry, false
|
46
|
-
else
|
47
|
-
parsed_expiry = nil
|
48
|
-
if defined?(Chronic)
|
49
|
-
parsed_expiry = (Chronic.parse(expiry_str) || Chronic.parse("#{expiry_str} from now"))
|
50
|
-
else
|
51
|
-
if dt = (DateTime.parse(expiry_str) rescue nil)
|
52
|
-
parsed_expiry = dt.to_time
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
if parsed_expiry
|
57
|
-
set :lock_expiry, parsed_expiry.utc
|
58
|
-
else
|
59
|
-
logger.info "'#{expiry_str}' could not be parsed. Please try again."
|
60
|
-
end
|
61
|
-
end
|
16
|
+
module Capistrano
|
17
|
+
DeployLockedError = Class.new(StandardError)
|
18
|
+
|
19
|
+
module DeployLock
|
20
|
+
def self.message(application, stage, deploy_lock)
|
21
|
+
message = "#{application} (#{stage}) was locked"
|
22
|
+
if defined?(Capistrano::DateHelper)
|
23
|
+
locked_ago = Capistrano::DateHelper.distance_of_time_in_words_to_now deploy_lock[:created_at].localtime
|
24
|
+
message << " #{locked_ago} ago"
|
25
|
+
else
|
26
|
+
message << " at #{deploy_lock[:created_at].localtime}"
|
62
27
|
end
|
28
|
+
message << " by '#{deploy_lock[:username]}'\nMessage: #{deploy_lock[:message]}"
|
63
29
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
task :create_lock do
|
69
|
-
if self[:lock_message].nil?
|
70
|
-
set :lock_message, "Deploying #{branch} branch"
|
71
|
-
end
|
72
|
-
if self[:lock_expiry].nil?
|
73
|
-
set :lock_expiry, (Time.now + default_lock_expiry).utc
|
74
|
-
end
|
75
|
-
|
76
|
-
lock = {
|
77
|
-
:created_at => Time.now.utc,
|
78
|
-
:username => ENV['USER'],
|
79
|
-
:expire_at => self[:lock_expiry],
|
80
|
-
:message => self[:lock_message]
|
81
|
-
}
|
82
|
-
put lock.to_yaml, deploy_lockfile, :mode => 0777
|
83
|
-
end
|
84
|
-
|
85
|
-
desc "Unlocks the server for deployment."
|
86
|
-
task :unlock do
|
87
|
-
run "rm -f #{deploy_lockfile}"
|
88
|
-
end
|
89
|
-
|
90
|
-
desc "Checks for a deploy lock. If present, deploy is aborted and message is displayed. Any expired locks are deleted."
|
91
|
-
task :check_lock do
|
92
|
-
lock_file = capture("[ -e #{deploy_lockfile} ] && cat #{deploy_lockfile} || true").strip
|
93
|
-
|
94
|
-
if lock_file != ""
|
95
|
-
lock = YAML.load(lock_file)
|
96
|
-
|
97
|
-
if lock[:expire_at] && lock[:expire_at] < Time.now
|
98
|
-
logger.info "Deleting expired deploy lock..."
|
99
|
-
unlock
|
30
|
+
if deploy_lock[:expire_at]
|
31
|
+
if defined?(Capistrano::DateHelper)
|
32
|
+
expires_in = Capistrano::DateHelper.distance_of_time_in_words_to_now deploy_lock[:expire_at].localtime
|
33
|
+
message << "\nExpires in #{expires_in}"
|
100
34
|
else
|
101
|
-
|
102
|
-
locked_ago = Capistrano::DateHelper.distance_of_time_in_words_to_now lock[:created_at].localtime
|
103
|
-
message = "Deploy locked #{locked_ago} ago"
|
104
|
-
else
|
105
|
-
message = "Deploy locked at #{lock[:created_at].localtime}"
|
106
|
-
end
|
107
|
-
|
108
|
-
message << " by '#{lock[:username]}'\nMessage: #{lock[:message]}"
|
109
|
-
|
110
|
-
if lock[:expire_at]
|
111
|
-
if defined?(Capistrano::DateHelper)
|
112
|
-
expires_in = Capistrano::DateHelper.distance_of_time_in_words_to_now lock[:expire_at].localtime
|
113
|
-
message << "\nExpires in #{expires_in}"
|
114
|
-
else
|
115
|
-
message << "\nExpires at #{lock[:expire_at].localtime.strftime("%H:%M:%S")}"
|
116
|
-
end
|
117
|
-
else
|
118
|
-
message << "\nLock must be manually removed with: cap #{stage} deploy:unlock"
|
119
|
-
end
|
120
|
-
|
121
|
-
logger.important message
|
122
|
-
|
123
|
-
# Don't raise exception if current user owns the lock.
|
124
|
-
# Just sleep so they have a chance to Ctrl-C
|
125
|
-
if lock[:username] == ENV['USER']
|
126
|
-
4.downto(1) do |i|
|
127
|
-
Kernel.print "\rDeploy lock was created by you (#{ENV['USER']}). Continuing deploy in #{i}..."
|
128
|
-
sleep 1
|
129
|
-
end
|
130
|
-
puts
|
131
|
-
else
|
132
|
-
raise Capistrano::DeployLockedError
|
133
|
-
end
|
35
|
+
message << "\nExpires at #{deploy_lock[:expire_at].localtime.strftime("%H:%M:%S")}"
|
134
36
|
end
|
37
|
+
else
|
38
|
+
message << "\nLock must be manually removed with: cap #{stage} deploy:unlock"
|
135
39
|
end
|
136
40
|
end
|
137
41
|
end
|
138
42
|
end
|
43
|
+
|
44
|
+
# Load recipe if required from deploy script
|
45
|
+
if defined?(Capistrano::Configuration) && Capistrano::Configuration.instance
|
46
|
+
require 'capistrano/recipes/deploy_lock'
|
47
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
2
|
+
before "deploy", "deploy:check_lock"
|
3
|
+
before "deploy", "deploy:refresh_lock"
|
4
|
+
before "deploy", "deploy:create_lock"
|
5
|
+
after "deploy", "deploy:unlock"
|
6
|
+
|
7
|
+
# Default lock expiry of 15 minutes (in case deploy crashes or is interrupted)
|
8
|
+
_cset :default_lock_expiry, (15 * 60)
|
9
|
+
_cset(:deploy_lockfile) { "#{shared_path}/capistrano.lock.yml" }
|
10
|
+
|
11
|
+
# Show lock message as bright red
|
12
|
+
log_formatter(:match => /was locked/, :color => :red, :style => :bright, :priority => 20)
|
13
|
+
|
14
|
+
namespace :deploy do
|
15
|
+
# Fetch the deploy lock unless already cached
|
16
|
+
def fetch_deploy_lock
|
17
|
+
if self[:deploy_lock].nil?
|
18
|
+
lock_file = capture("[ -e #{deploy_lockfile} ] && cat #{deploy_lockfile} || true").strip
|
19
|
+
if lock_file != ""
|
20
|
+
set :deploy_lock, YAML.load(lock_file)
|
21
|
+
else
|
22
|
+
set :deploy_lock, false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_deploy_lock(deploy_lock)
|
28
|
+
put deploy_lock.to_yaml, deploy_lockfile, :mode => 0777
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Deploy with a custom deploy lock"
|
32
|
+
task :with_lock do
|
33
|
+
lock
|
34
|
+
deploy.default
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Set deploy lock with a custom lock message and expiry time"
|
38
|
+
task :lock do
|
39
|
+
set :lock_message, Capistrano::CLI.ui.ask("Lock Message: ")
|
40
|
+
|
41
|
+
while self[:lock_expiry].nil?
|
42
|
+
expiry_str = Capistrano::CLI.ui.ask("Expire lock at? (optional): ")
|
43
|
+
if expiry_str == ""
|
44
|
+
# Never expire an explicit lock if no time given
|
45
|
+
set :lock_expiry, false
|
46
|
+
else
|
47
|
+
parsed_expiry = nil
|
48
|
+
if defined?(Chronic)
|
49
|
+
parsed_expiry = Chronic.parse(expiry_str) || Chronic.parse("#{expiry_str} from now")
|
50
|
+
elsif dt = (DateTime.parse(expiry_str) rescue nil)
|
51
|
+
parsed_expiry = dt.to_time
|
52
|
+
end
|
53
|
+
|
54
|
+
if parsed_expiry
|
55
|
+
set :lock_expiry, parsed_expiry.utc
|
56
|
+
else
|
57
|
+
logger.info "'#{expiry_str}' could not be parsed. Please try again."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
create_lock
|
63
|
+
set :custom_deploy_lock, true
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Creates a lock file, so that futher deploys will be prevented"
|
67
|
+
task :create_lock do
|
68
|
+
if self[:custom_deploy_lock]
|
69
|
+
logger.info 'Custom deploy lock already created.'
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
if self[:lock_message].nil?
|
74
|
+
set :lock_message, "Deploying #{branch} branch"
|
75
|
+
end
|
76
|
+
if self[:lock_expiry].nil?
|
77
|
+
set :lock_expiry, (Time.now + default_lock_expiry).utc
|
78
|
+
end
|
79
|
+
|
80
|
+
deploy_lock = {
|
81
|
+
:created_at => Time.now.utc,
|
82
|
+
:username => ENV['USER'],
|
83
|
+
:expire_at => self[:lock_expiry],
|
84
|
+
:message => self[:lock_message]
|
85
|
+
}
|
86
|
+
write_deploy_lock(deploy_lock)
|
87
|
+
end
|
88
|
+
|
89
|
+
namespace :unlock do
|
90
|
+
desc "Unlocks the server for deployment"
|
91
|
+
task :default do
|
92
|
+
# Don't automatically remove custom deploy locks created by deploy:lock task
|
93
|
+
if self[:custom_deploy_lock]
|
94
|
+
logger.info 'Not removing custom deploy lock.'
|
95
|
+
else
|
96
|
+
force
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
task :force do
|
101
|
+
run "rm -f #{deploy_lockfile}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Checks for a deploy lock. If present, deploy is aborted and message is displayed. Any expired locks are deleted."
|
106
|
+
task :check_lock do
|
107
|
+
# Don't check the lock if we just created it
|
108
|
+
next if self[:custom_deploy_lock]
|
109
|
+
|
110
|
+
fetch_deploy_lock
|
111
|
+
# Return if no lock
|
112
|
+
next unless self[:deploy_lock]
|
113
|
+
|
114
|
+
if deploy_lock[:expire_at] && deploy_lock[:expire_at] < Time.now
|
115
|
+
logger.info "Deleting expired deploy lock..."
|
116
|
+
unlock
|
117
|
+
next
|
118
|
+
end
|
119
|
+
|
120
|
+
# Unexpired lock is present, so display the lock message
|
121
|
+
logger.important Capistrano::DeployLock.message(application, stage, deploy_lock)
|
122
|
+
|
123
|
+
# Don't raise exception if current user owns the lock.
|
124
|
+
# Just sleep so they have a chance to Ctrl-C
|
125
|
+
if deploy_lock[:username] == ENV['USER']
|
126
|
+
4.downto(1) do |i|
|
127
|
+
Kernel.print "\rDeploy lock was created by you (#{ENV['USER']}). Continuing deploy in #{i}..."
|
128
|
+
sleep 1
|
129
|
+
end
|
130
|
+
puts
|
131
|
+
else
|
132
|
+
raise Capistrano::DeployLockedError
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
desc "Refreshes an existing deploy lock's expiry time, if it is less than the default time"
|
137
|
+
task :refresh_lock do
|
138
|
+
# Don't refresh custom locks
|
139
|
+
next if self[:custom_deploy_lock]
|
140
|
+
|
141
|
+
fetch_deploy_lock
|
142
|
+
next unless self[:deploy_lock]
|
143
|
+
|
144
|
+
# Refresh lock expiry time if it's going to expire soon
|
145
|
+
if deploy_lock[:expire_at] && deploy_lock[:expire_at] < (Time.now + default_lock_expiry)
|
146
|
+
logger.info "Resetting lock expiry to default..."
|
147
|
+
deploy_lock[:expire_at] = (Time.now + default_lock_expiry).utc
|
148
|
+
|
149
|
+
write_deploy_lock(deploy_lock)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Set the deploy_lock_created flag so that the lock isn't automatically removed after deploy
|
153
|
+
set :custom_deploy_lock, true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capistrano_deploy_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,13 +9,14 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-16 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Lock a server during deploy, to prevent people from deploying at the
|
15
15
|
same time.
|
16
16
|
email:
|
17
17
|
- nathan.f77@gmail.com
|
18
|
-
executables:
|
18
|
+
executables:
|
19
|
+
- cap_deploy_lock_msg
|
19
20
|
extensions: []
|
20
21
|
extra_rdoc_files: []
|
21
22
|
files:
|
@@ -24,8 +25,11 @@ files:
|
|
24
25
|
- LICENSE.txt
|
25
26
|
- README.md
|
26
27
|
- Rakefile
|
28
|
+
- bin/cap_deploy_lock_msg
|
27
29
|
- capistrano_deploy_lock.gemspec
|
30
|
+
- lib/capistrano/date_helper.rb
|
28
31
|
- lib/capistrano/deploy_lock.rb
|
32
|
+
- lib/capistrano/recipes/deploy_lock.rb
|
29
33
|
- lib/capistrano_deploy_lock.rb
|
30
34
|
- lib/capistrano_deploy_lock/version.rb
|
31
35
|
homepage: https://github.com/ndbroadbent/capistrano_deploy_lock
|
@@ -43,7 +47,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
47
|
version: '0'
|
44
48
|
segments:
|
45
49
|
- 0
|
46
|
-
hash:
|
50
|
+
hash: -2094697491794115315
|
47
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
52
|
none: false
|
49
53
|
requirements:
|
@@ -52,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
56
|
version: '0'
|
53
57
|
segments:
|
54
58
|
- 0
|
55
|
-
hash:
|
59
|
+
hash: -2094697491794115315
|
56
60
|
requirements: []
|
57
61
|
rubyforge_project:
|
58
62
|
rubygems_version: 1.8.24
|