chap 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +7 -0
- data/bin/chap +6 -0
- data/chap.gemspec +32 -0
- data/lib/chap/cli.rb +26 -0
- data/lib/chap/config.rb +113 -0
- data/lib/chap/hook.rb +16 -0
- data/lib/chap/runner.rb +129 -0
- data/lib/chap/special_methods.rb +23 -0
- data/lib/chap/strategy/base.rb +20 -0
- data/lib/chap/strategy/checkout.rb +62 -0
- data/lib/chap/strategy/copy.rb +10 -0
- data/lib/chap/strategy/hardlink.rb +13 -0
- data/lib/chap/strategy/util/copy.rb +55 -0
- data/lib/chap/strategy.rb +6 -0
- data/lib/chap/task.rb +28 -0
- data/lib/chap/version.rb +3 -0
- data/lib/chap.rb +17 -0
- data/lib/mash.rb +225 -0
- data/lib/setup/chap.json +10 -0
- data/lib/setup/chap.yml +2 -0
- data/lib/setup/node.json +5 -0
- data/spec/fixtures/chapdemo/.gitignore +15 -0
- data/spec/fixtures/chapdemo/Gemfile +3 -0
- data/spec/fixtures/chapdemo/chap/deploy +4 -0
- data/spec/fixtures/chapdemo/chap/restart +10 -0
- data/spec/fixtures/chapdemo/config.ru +1 -0
- data/spec/fixtures/chapdemo/public/.gitkeep +0 -0
- data/spec/lib/chap_spec.rb +44 -0
- data/spec/spec_helper.rb +43 -0
- metadata +251 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3@chap
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/chap/(.+)\.rb$}) { |m| "spec/lib/chap_spec.rb" }
|
7
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
end
|
10
|
+
|
11
|
+
guard 'bundler' do
|
12
|
+
watch('Gemfile')
|
13
|
+
watch(/^.+\.gemspec/)
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Tung Nguyen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# Chap
|
2
|
+
|
3
|
+
chef + capistrano = chap: deploy your app with either chef or capistrano. This was written to solve the issue between having 2 deployment systems that are very similar but not exactly the same. With chap you can deploy to a single server by running one command:
|
4
|
+
|
5
|
+
<pre>
|
6
|
+
$ chap deploy
|
7
|
+
</pre>
|
8
|
+
|
9
|
+
The same command is called whether you're using chef or capistrano for deployment. The chap deploy command does the heavy lifting and manages the deploy instead of capistrano or chef.
|
10
|
+
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
<pre>
|
14
|
+
$ gem install chap
|
15
|
+
</pre>
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
The chap command is meant to be executed on the server which you want to deploy the code.
|
20
|
+
|
21
|
+
## Setup
|
22
|
+
|
23
|
+
Chap requires 3 configuration files: chap.yml, chap.json and node.json.
|
24
|
+
|
25
|
+
* chap.json: contains capistrano-like configuration settings.
|
26
|
+
* node.json: is intended to be the same file that chef solo uses and contains instance specific information, like node[:instance_role].
|
27
|
+
: chap.yml: The paths of the chap.json and node.json are configured in this file.
|
28
|
+
|
29
|
+
Here are examples of the starter setup files that you can generate via:
|
30
|
+
|
31
|
+
<pre>
|
32
|
+
$ chap setup -o /etc/chef
|
33
|
+
</pre>
|
34
|
+
|
35
|
+
<pre>
|
36
|
+
$ cat /etc/chef/chap.yml
|
37
|
+
chap: /etc/chef/chap.json
|
38
|
+
node: /etc/chef/node.json
|
39
|
+
$ cat /etc/chef/chap.json
|
40
|
+
{
|
41
|
+
"repo": "git@github.com:tongueroo/chapdemo.git",
|
42
|
+
"branch": "master",
|
43
|
+
"application": "chapdemo",
|
44
|
+
"deploy_to": "/data/chapdemo",
|
45
|
+
"strategy": "checkout",
|
46
|
+
"keep": 5,
|
47
|
+
"user": "deploy",
|
48
|
+
"group": "deploy"
|
49
|
+
}
|
50
|
+
$ cat /etc/chef/node.json
|
51
|
+
{
|
52
|
+
"environment": "staging",
|
53
|
+
"application": "chapdemo",
|
54
|
+
"instance_role": "app"
|
55
|
+
}
|
56
|
+
</pre>
|
57
|
+
|
58
|
+
### Deploy sequence
|
59
|
+
|
60
|
+
The deploy sequence is based on the sequence from the capistrano and chef deploy resource provider.
|
61
|
+
|
62
|
+
1. Download code to [deploy_to]/releases/[timestamp]
|
63
|
+
2. Run chap/deploy hook
|
64
|
+
3. Symlink [deploy_to]/releases/[timestamp] to [deploy_to]/current
|
65
|
+
4. Run chap/restart hook
|
66
|
+
5. Clean up old releases
|
67
|
+
|
68
|
+
On the server:
|
69
|
+
|
70
|
+
<pre>
|
71
|
+
$ chap deploy
|
72
|
+
</pre>
|
73
|
+
|
74
|
+
From capistrano, on local or deploy box:
|
75
|
+
|
76
|
+
<pre>
|
77
|
+
$ cap deploy # cap recipe calls "chap deploy"
|
78
|
+
</pre>
|
79
|
+
|
80
|
+
Chef Chap LWRP:
|
81
|
+
|
82
|
+
<pre>
|
83
|
+
# chef LWRP creates chap.yml and chap.json setup files and calls "chap deploy"
|
84
|
+
chap_deploy "chapdemo" do
|
85
|
+
repo "git@github.com:tongueroo/chapdemo.git"
|
86
|
+
revision "master"
|
87
|
+
end
|
88
|
+
</pre>
|
89
|
+
|
90
|
+
Chap loads up information from node.json because it needs the information for hooks, which tend to work differently for different server roles. For example, the chap/restart hook below will run "touch tmp/restart.txt" for an app role and will run "rvmsudo bluepill restart resque" for a resque role. Example:
|
91
|
+
|
92
|
+
<pre>
|
93
|
+
$ cat chap/restart
|
94
|
+
#!/usr/bin/env ruby
|
95
|
+
restart = case node[:instance_role]
|
96
|
+
when 'app'
|
97
|
+
"touch tmp/restart.txt"
|
98
|
+
when 'resque'
|
99
|
+
"rvmsudo bluepill restart resque"
|
100
|
+
end
|
101
|
+
run "cd #{current_path} && #{restart}"
|
102
|
+
</pre>
|
103
|
+
|
104
|
+
## Deploy Hooks
|
105
|
+
|
106
|
+
Define your deploy hooks in the chap folder of the project. There are 2 deploy hooks.
|
107
|
+
|
108
|
+
* chap/deploy - runs after the code has been deployed but not yet symlinked
|
109
|
+
* chap/restart - runs after the code has been symlinked and app needs to be restarted
|
110
|
+
|
111
|
+
Deploy hooks get evaluated within the context of a chap deploy run and have some special variables and methods:
|
112
|
+
|
113
|
+
Special variables:
|
114
|
+
|
115
|
+
* node - contains data from /etc/chef/node.json. Avaiable as mash.
|
116
|
+
* chap - contains data from /etc/chef/chap.json and some special variables added by chap. Avaiable as mash. Special variables: release_path, current_path, shared_path, cached_path. The special variables are also available directly as methods.
|
117
|
+
|
118
|
+
Special methods:
|
119
|
+
|
120
|
+
* run - output the command to be ran and runs command.
|
121
|
+
* log - log messages to [shared_path]/chap/chap.log.
|
data/Rakefile
ADDED
data/bin/chap
ADDED
data/chap.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'chap/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "chap"
|
8
|
+
gem.version = Chap::VERSION
|
9
|
+
gem.authors = ["Tung Nguyen"]
|
10
|
+
gem.email = ["tongueroo@gmail.com"]
|
11
|
+
gem.description = %q{chef + capistrano = chap: deploy your app with either chef or capistrano}
|
12
|
+
gem.summary = %q{chef + capistrano = chap: deploy your app with either chef or capistrano}
|
13
|
+
gem.homepage = "https://github.com/tongueroo/chap"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "rake"
|
21
|
+
gem.add_dependency "json"
|
22
|
+
gem.add_dependency "thor"
|
23
|
+
gem.add_dependency "colorize"
|
24
|
+
gem.add_dependency "logger"
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rspec'
|
27
|
+
gem.add_development_dependency 'guard'
|
28
|
+
gem.add_development_dependency 'guard-rspec'
|
29
|
+
gem.add_development_dependency 'guard-bundler'
|
30
|
+
gem.add_development_dependency 'rb-fsevent'
|
31
|
+
# gem.add_development_dependency 'fakefs'
|
32
|
+
end
|
data/lib/chap/cli.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Chap
|
2
|
+
class CLI < Thor
|
3
|
+
desc "setup", "Sets up chap config files"
|
4
|
+
long_desc "Creates chap.json, chap.yml and node.json example files."
|
5
|
+
method_option :force, :aliases => '-f', :type => :boolean, :desc => "Overwrite existing files"
|
6
|
+
method_option :quiet, :aliases => '-q', :type => :boolean, :desc => "Quiet commands"
|
7
|
+
method_option :output, :aliases => '-o', :desc => "Folder which example files will be written to"
|
8
|
+
def setup
|
9
|
+
Chap::Task.setup(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "deploy", "Deploy application"
|
13
|
+
long_desc <<-EOL
|
14
|
+
Example:
|
15
|
+
|
16
|
+
$ chap deploy
|
17
|
+
|
18
|
+
Deploys code using settings from chap.json and node.json. chap.json and node.json should be referenced in chap.yml.
|
19
|
+
EOL
|
20
|
+
method_option :quiet, :aliases => '-q', :type => :boolean, :desc => "Quiet commands"
|
21
|
+
method_option :config, :aliases => '-c', :default => '/etc/chef/chap.yml', :desc => "chap.yml config to use"
|
22
|
+
def deploy
|
23
|
+
Chap::Task.deploy(options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/chap/config.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module Chap
|
2
|
+
class Config
|
3
|
+
attr_reader :options, :node, :chap
|
4
|
+
def initialize(options={})
|
5
|
+
@options = options
|
6
|
+
# preload all config files so validatation happens before deploys
|
7
|
+
_ = yaml, chap, node
|
8
|
+
end
|
9
|
+
|
10
|
+
def yaml
|
11
|
+
path = options[:config]
|
12
|
+
if File.exist?(path)
|
13
|
+
@yaml ||= Mash.from_hash(YAML.load(IO.read(path)))
|
14
|
+
else
|
15
|
+
puts "ERROR: chap.yaml config does not exist at: #{path}"
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def chap
|
21
|
+
return @chap if @chap
|
22
|
+
@chap = load_json(:chap)
|
23
|
+
@chap[:release_path] = release_path
|
24
|
+
@chap[:current_path] = current_path
|
25
|
+
@chap[:cached_path] = cached_path
|
26
|
+
@chap
|
27
|
+
end
|
28
|
+
|
29
|
+
def node
|
30
|
+
@node ||= load_json(:node)
|
31
|
+
end
|
32
|
+
|
33
|
+
# the chap.json and node.json is assumed to be in th same folder as
|
34
|
+
# chap.yml if a relative path is given
|
35
|
+
def load_json(key)
|
36
|
+
path = if yaml[key] =~ %r{^/} # root path given
|
37
|
+
yaml[key]
|
38
|
+
else # relative path
|
39
|
+
dirname = File.dirname(options[:config])
|
40
|
+
"#{dirname}/#{yaml[key]}"
|
41
|
+
end
|
42
|
+
if File.exist?(path)
|
43
|
+
Mash.from_hash(JSON.parse(IO.read(path)))
|
44
|
+
else
|
45
|
+
puts "ERROR: #{key}.json config does not exist at: #{path}"
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def timestamp
|
51
|
+
@timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
|
52
|
+
end
|
53
|
+
|
54
|
+
def deploy_to
|
55
|
+
chap[:deploy_to]
|
56
|
+
end
|
57
|
+
|
58
|
+
# special attributes added to chap
|
59
|
+
def release_path
|
60
|
+
return @release_path if @release_path
|
61
|
+
@release_path = "#{deploy_to}/releases/#{timestamp}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_path
|
65
|
+
return @current_path if @current_path
|
66
|
+
@current_path = "#{deploy_to}/current"
|
67
|
+
end
|
68
|
+
|
69
|
+
def shared_path
|
70
|
+
return @shared_path if @shared_path
|
71
|
+
@shared_path = "#{deploy_to}/shared"
|
72
|
+
end
|
73
|
+
|
74
|
+
def cached_path
|
75
|
+
return @cached_path if @cached_path
|
76
|
+
path = chap[:repo].split(':').last.sub('.git','')
|
77
|
+
@cached_path = "#{shared_path}/cache/#{strategy}/#{path}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def strategy
|
81
|
+
chap[:strategy] || 'checkout'
|
82
|
+
end
|
83
|
+
|
84
|
+
def log(msg)
|
85
|
+
puts msg unless options[:quiet]
|
86
|
+
logger.info(msg)
|
87
|
+
end
|
88
|
+
|
89
|
+
def chap_log_path
|
90
|
+
return @chap_log_path if @chap_log_path
|
91
|
+
dir = "#{shared_path}/chap"
|
92
|
+
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
93
|
+
@chap_log_path = "#{dir}/chap.log"
|
94
|
+
system("cat /dev/null > #{@chap_log_path}")
|
95
|
+
@chap_log_path
|
96
|
+
end
|
97
|
+
|
98
|
+
def logger
|
99
|
+
return @logger if @logger
|
100
|
+
@logger = Logger.new(chap_log_path)
|
101
|
+
# @logger.level = Logger::WARN
|
102
|
+
@logger
|
103
|
+
end
|
104
|
+
|
105
|
+
def run(cmd)
|
106
|
+
log "Running: #{cmd}"
|
107
|
+
cmd = "#{cmd} 2>&1" unless cmd.include?(" > ")
|
108
|
+
out = `#{cmd}`
|
109
|
+
log out
|
110
|
+
raise "DeployError" if $?.exitstatus > 0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/chap/hook.rb
ADDED
data/lib/chap/runner.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
module Chap
|
2
|
+
class Runner
|
3
|
+
include SpecialMethods
|
4
|
+
|
5
|
+
attr_reader :options, :config
|
6
|
+
def initialize(options={})
|
7
|
+
@options = options
|
8
|
+
@config = Config.new(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy
|
12
|
+
setup
|
13
|
+
strategy.deploy
|
14
|
+
symlink_shared
|
15
|
+
hook(:deploy)
|
16
|
+
symlink_current
|
17
|
+
hook(:restart)
|
18
|
+
cleanup
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
user = config.chap[:user] || ENV['USER']
|
23
|
+
group = config.chap[:group]
|
24
|
+
begin
|
25
|
+
FileUtils.mkdir_p(deploy_to)
|
26
|
+
FileUtils.chown_R user, group, deploy_to
|
27
|
+
rescue Exception
|
28
|
+
# retry to create deploy_to folder with sudo
|
29
|
+
user_group = [user,group].compact.join(':')
|
30
|
+
raise unless system("sudo mkdir -p #{deploy_to}")
|
31
|
+
raise unless system("sudo chown -R #{user_group} #{deploy_to}")
|
32
|
+
end
|
33
|
+
dirs = ["#{deploy_to}/releases"]
|
34
|
+
dirs += shared_dirs
|
35
|
+
dirs.each do |dir|
|
36
|
+
FileUtils.mkdir_p(dir)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def strategy
|
41
|
+
strategy = config.strategy
|
42
|
+
klass = Strategy.const_get(camel_case(strategy))
|
43
|
+
@strategy ||= klass.new(:config => @config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def camel_case(string)
|
47
|
+
return string if string !~ /_/ && string =~ /[A-Z]+.*/
|
48
|
+
string.split('_').map{|e| e.capitalize}.join
|
49
|
+
end
|
50
|
+
|
51
|
+
def symlink_shared
|
52
|
+
shared_dirs.each do |path|
|
53
|
+
src = path
|
54
|
+
relative_path = path.sub("#{shared_path}/",'')
|
55
|
+
dest = "#{release_path}/#{relative_path}"
|
56
|
+
# make sure the directory exist for symlink creation
|
57
|
+
dirname = File.dirname(dest)
|
58
|
+
FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
|
59
|
+
if File.symlink?(dest)
|
60
|
+
File.delete(dest)
|
61
|
+
elsif File.directory?(dest)
|
62
|
+
FileUtils.rm_rf(dest)
|
63
|
+
end
|
64
|
+
FileUtils.ln_s(src,dest)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def symlink_current
|
69
|
+
FileUtils.rm(current_path) if File.exist?(current_path)
|
70
|
+
FileUtils.ln_s(release_path, current_path)
|
71
|
+
log "Current symlink updated".colorize(:green)
|
72
|
+
end
|
73
|
+
|
74
|
+
def cleanup
|
75
|
+
log "Cleaning up".colorize(:green)
|
76
|
+
remove_old_releases
|
77
|
+
logrotate
|
78
|
+
end
|
79
|
+
|
80
|
+
def remove_old_releases
|
81
|
+
dirname = File.dirname(release_path)
|
82
|
+
releases = Dir.glob("#{dirname}/*").sort.reverse
|
83
|
+
keep = config.chap[:keep] || 5
|
84
|
+
delete = releases - releases[0..keep-1]
|
85
|
+
delete.each do |old_release|
|
86
|
+
FileUtils.rm_rf(old_release)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def logrotate
|
91
|
+
logrotate_file(config.chap_log_path)
|
92
|
+
end
|
93
|
+
|
94
|
+
def logrotate_timestamp
|
95
|
+
@logrotate_timestamp ||= Time.now.strftime("%Y-%m-%d-%H-%M-%S")
|
96
|
+
end
|
97
|
+
|
98
|
+
def logrotate_file(log_path)
|
99
|
+
dirname = File.dirname(log_path)
|
100
|
+
basename = File.basename(log_path, '.log')
|
101
|
+
archive = "#{dirname}/#{basename}-#{logrotate_timestamp}.log"
|
102
|
+
FileUtils.cp(log_path, archive) if File.exist?(log_path)
|
103
|
+
logs = Dir.glob("#{dirname}/#{basename}-*.log").sort.reverse
|
104
|
+
delete = logs - logs[0..9] # keep last 10 chap logs
|
105
|
+
delete.each do |old_log|
|
106
|
+
FileUtils.rm_f(old_log)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def shared_dirs
|
111
|
+
dirs = config.chap[:shared_dirs] || [
|
112
|
+
"public/system",
|
113
|
+
"log",
|
114
|
+
"tmp/pids"
|
115
|
+
]
|
116
|
+
dirs.map! {|p| "#{shared_path}/#{p}"}
|
117
|
+
end
|
118
|
+
|
119
|
+
def hook(name)
|
120
|
+
log "Running hook: #{name}".colorize(:green)
|
121
|
+
path = "#{release_path}/chap/#{name}"
|
122
|
+
if File.exist?(path)
|
123
|
+
Hook.new(path, @config).evaluate
|
124
|
+
else
|
125
|
+
log "chap/#{name} hook does not exist".colorize(:red)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Chap
|
2
|
+
module SpecialMethods
|
3
|
+
SPECIAL_METHODS = %w/deploy_to release_path current_path shared_path cached_path node chap log run/
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
base.define_special_methods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# delegate to the config class
|
12
|
+
def define_special_methods
|
13
|
+
SPECIAL_METHODS.each do |method|
|
14
|
+
class_eval <<-EOL
|
15
|
+
def #{method}(*args)
|
16
|
+
@config.#{method}(*args)
|
17
|
+
end
|
18
|
+
EOL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Chap
|
2
|
+
module Strategy
|
3
|
+
class Base
|
4
|
+
include SpecialMethods
|
5
|
+
|
6
|
+
attr_reader :options, :config
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options
|
9
|
+
@config = options[:config]
|
10
|
+
log "Deploying via #{self.class} strategy".colorize(:green)
|
11
|
+
end
|
12
|
+
|
13
|
+
# should download code to the release_path
|
14
|
+
def deploy
|
15
|
+
raise "Must implement deploy method"
|
16
|
+
end
|
17
|
+
|
18
|
+
end # of Base
|
19
|
+
end # of Strategy
|
20
|
+
end # of Chap
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Chap
|
2
|
+
module Strategy
|
3
|
+
class Checkout < Base
|
4
|
+
def deploy
|
5
|
+
update
|
6
|
+
copy
|
7
|
+
end
|
8
|
+
|
9
|
+
def update
|
10
|
+
log "Updating repo in #{cached_path}".colorize(:green)
|
11
|
+
cached_root = File.dirname(cached_path)
|
12
|
+
FileUtils.mkdir_p(cached_root) unless File.exist?(cached_root)
|
13
|
+
if File.exist?(cached_path)
|
14
|
+
sync
|
15
|
+
else
|
16
|
+
checkout
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def sync
|
21
|
+
command =<<-BASH
|
22
|
+
cd #{cached_path} && \
|
23
|
+
git fetch -q origin && \
|
24
|
+
git fetch --tags -q origin && \
|
25
|
+
git reset -q --hard #{revision} && \
|
26
|
+
git clean -q -d -x -f
|
27
|
+
BASH
|
28
|
+
run(command)
|
29
|
+
end
|
30
|
+
|
31
|
+
def checkout
|
32
|
+
command =<<BASH
|
33
|
+
git clone -q #{config.chap[:repo]} #{cached_path} && \
|
34
|
+
cd #{cached_path} && \
|
35
|
+
git checkout -q -b deploy #{revision}
|
36
|
+
BASH
|
37
|
+
run(command)
|
38
|
+
end
|
39
|
+
|
40
|
+
def copy
|
41
|
+
command = "cp -RPp #{cached_path} #{release_path} && #{mark}"
|
42
|
+
run command
|
43
|
+
log "Code copied to #{release_path}".colorize(:green)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def mark
|
49
|
+
"(echo #{revision} > #{release_path}/REVISION)"
|
50
|
+
end
|
51
|
+
|
52
|
+
def revision
|
53
|
+
return @revision if @revision
|
54
|
+
result = `git ls-remote #{config.chap[:repo]} #{config.chap[:branch]}`
|
55
|
+
@revision = result.split(/\s/).first
|
56
|
+
log "Fetched revision #{@revision}".colorize(:green)
|
57
|
+
@revision
|
58
|
+
end
|
59
|
+
|
60
|
+
end # of RemoteCache
|
61
|
+
end # of Strategy
|
62
|
+
end # of Chap
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Chap
|
2
|
+
module Strategy
|
3
|
+
class Hardlink < Checkout
|
4
|
+
def copy
|
5
|
+
copy = File.expand_path("../util/copy.rb", __FILE__)
|
6
|
+
command = "#{copy} #{cached_path} #{release_path} && #{mark}"
|
7
|
+
run command
|
8
|
+
log "Code copied to #{release_path}".colorize(:green)
|
9
|
+
end
|
10
|
+
|
11
|
+
end # of HardLink
|
12
|
+
end # of Strategy
|
13
|
+
end # of Chap
|