mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
Move switchtower to the tools directory, to decouple it from rails
git-svn-id: http://svn.rubyonrails.org/rails/tools/switchtower@2074 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
d89f08f780
commit
824d22bfb8
15 changed files with 116 additions and 80 deletions
|
@ -1,5 +1,13 @@
|
|||
*SVN*
|
||||
|
||||
* Specify the revision to release via the :revision variable (defaults to latest revision)
|
||||
|
||||
* Allow variables to be set via the cli using the -s switch
|
||||
|
||||
* Log checkouts to a "revisions.log" file
|
||||
|
||||
* Changed behavior of checkout to use the timestamp as the release name, instead of the revision number
|
||||
|
||||
* Added CVS module (very very experimental!)
|
||||
|
||||
* Works with public keys now, for passwordless deployment
|
||||
|
|
4
README
4
README
|
@ -21,7 +21,9 @@ As with the rest of Rails, if you can abide by these assumptions, you can use Sw
|
|||
|
||||
== Usage
|
||||
|
||||
More documentation is always pending, but you'll want to see the user manual for detailed usage instructions. In general, you'll use SwitchTower as follows:
|
||||
More documentation is always pending, but you'll want to see the user manual for detailed usage instructions. (The manual is online at http://manuals.rubyonrails.com/read/book/17).
|
||||
|
||||
In general, you'll use SwitchTower as follows:
|
||||
|
||||
* Create a deployment recipe ("deploy.rb") for your application. You can use the sample recipe in examples/sample.rb as a starting point.
|
||||
* Use the +switchtower+ script to execute your recipe (see below).
|
||||
|
|
15
Rakefile
15
Rakefile
|
@ -2,11 +2,14 @@ require 'rake'
|
|||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
|
||||
require "./lib/switchtower/version"
|
||||
|
||||
SOFTWARE_NAME = "switchtower"
|
||||
SOFTWARE_VERSION = SwitchTower::Version::STRING
|
||||
PKG_NAME = "switchtower"
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_VERSION = SwitchTower::Version::STRING + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
desc "Default task"
|
||||
task :default => [ :test ]
|
||||
|
@ -19,7 +22,7 @@ Rake::TestTask.new do |t|
|
|||
t.verbose = true
|
||||
end
|
||||
|
||||
GEM_SPEC = eval(File.read("#{File.dirname(__FILE__)}/#{SOFTWARE_NAME}.gemspec"))
|
||||
GEM_SPEC = eval(File.read("#{File.dirname(__FILE__)}/#{PKG_NAME}.gemspec"))
|
||||
|
||||
Rake::GemPackageTask.new(GEM_SPEC) do |p|
|
||||
p.gem_spec = GEM_SPEC
|
||||
|
@ -36,3 +39,9 @@ Rake::RDocTask.new do |rdoc|
|
|||
rdoc.rdoc_files.include 'lib/**/*.rb'
|
||||
rdoc.template = "jamis"
|
||||
end
|
||||
|
||||
desc "Publish the beta gem"
|
||||
task :pgem => [:package] do
|
||||
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
|
||||
end
|
|
@ -27,7 +27,7 @@ rescue LoadError
|
|||
end
|
||||
end
|
||||
|
||||
options = { :verbose => 0, :recipes => [], :actions => [] }
|
||||
options = { :verbose => 0, :recipes => [], :actions => [], :vars => {} }
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{$0} [options]"
|
||||
|
@ -56,6 +56,14 @@ OptionParser.new do |opts|
|
|||
"be specified, and are loaded in the given order."
|
||||
) { |value| options[:recipes] << value }
|
||||
|
||||
opts.on("-s", "--set NAME=VALUE",
|
||||
"Specify a variable and it's value to set. This",
|
||||
"will be set after loading all recipe files."
|
||||
) do |pair|
|
||||
name, value = pair.split(/=/)
|
||||
options[:vars][name.to_sym] = value
|
||||
end
|
||||
|
||||
opts.on("-v", "--verbose",
|
||||
"Specify the verbosity of the output.",
|
||||
"May be given multiple times. (Default: silent)"
|
||||
|
@ -104,6 +112,7 @@ config.set :pretend, options[:pretend]
|
|||
config.load "standard" # load the standard recipe definition
|
||||
|
||||
options[:recipes].each { |recipe| config.load(recipe) }
|
||||
options[:vars].each { |name, value| config.set(name, value) }
|
||||
|
||||
actor = config.actor
|
||||
options[:actions].each { |action| actor.send action }
|
||||
|
|
|
@ -248,7 +248,7 @@ module SwitchTower
|
|||
buffer << out if str == :out
|
||||
raise "could not determine releases #{out.inspect}" if str == :err
|
||||
end
|
||||
@releases = buffer.split.map { |i| i.to_i }.sort
|
||||
@releases = buffer.split.sort
|
||||
end
|
||||
|
||||
@releases
|
||||
|
|
|
@ -26,12 +26,17 @@ module SwitchTower
|
|||
# The load paths used for locating recipe files.
|
||||
attr_reader :load_paths
|
||||
|
||||
# The time (in UTC) at which this configuration was created, used for
|
||||
# determining the release path.
|
||||
attr_reader :now
|
||||
|
||||
def initialize(actor_class=Actor) #:nodoc:
|
||||
@roles = Hash.new { |h,k| h[k] = [] }
|
||||
@actor = actor_class.new(self)
|
||||
@logger = Logger.new
|
||||
@load_paths = [".", File.join(File.dirname(__FILE__), "recipes")]
|
||||
@variables = {}
|
||||
@now = Time.now.utc
|
||||
|
||||
set :application, nil
|
||||
set :repository, nil
|
||||
|
@ -39,12 +44,14 @@ module SwitchTower
|
|||
set :user, nil
|
||||
set :password, nil
|
||||
|
||||
set :deploy_to, Proc.new { "/u/apps/#{application}" }
|
||||
set :deploy_to, Proc.new { "/u/apps/#{application}" }
|
||||
|
||||
set :version_dir, DEFAULT_VERSION_DIR_NAME
|
||||
set :current_dir, DEFAULT_CURRENT_DIR_NAME
|
||||
set :shared_dir, DEFAULT_SHARED_DIR_NAME
|
||||
set :scm, :subversion
|
||||
|
||||
set :revision, Proc.new { source.latest_revision }
|
||||
end
|
||||
|
||||
# Set a variable to the given value.
|
||||
|
@ -165,10 +172,10 @@ module SwitchTower
|
|||
File.join(deploy_to, shared_dir)
|
||||
end
|
||||
|
||||
# Return the full path to the named revision (defaults to the most current
|
||||
# revision in the repository).
|
||||
def release_path(revision=source.latest_revision)
|
||||
File.join(releases_path, revision)
|
||||
# Return the full path to the named release. If a release is not specified,
|
||||
# +now+ is used (the time at which the configuration was created).
|
||||
def release_path(release=now.strftime("%Y%m%d%H%M%S"))
|
||||
File.join(releases_path, release)
|
||||
end
|
||||
|
||||
def respond_to?(sym) #:nodoc:
|
||||
|
|
|
@ -71,7 +71,7 @@ symlinks and deleting the current release from all servers.
|
|||
DESC
|
||||
task :rollback_code do
|
||||
if releases.length < 2
|
||||
raise "could not rollback the code because there is no previous version"
|
||||
raise "could not rollback the code because there is no prior release"
|
||||
else
|
||||
run <<-CMD
|
||||
ln -nfs #{previous_release} #{current_path} &&
|
||||
|
|
35
lib/switchtower/scm/base.rb
Normal file
35
lib/switchtower/scm/base.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
module SwitchTower
|
||||
module SCM
|
||||
|
||||
# The ancestor class of the various SCM module implementations.
|
||||
class Base
|
||||
attr_reader :configuration
|
||||
|
||||
def initialize(configuration) #:nodoc:
|
||||
@configuration = configuration
|
||||
end
|
||||
|
||||
def latest_revision
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_checkout(actor, guts, &block)
|
||||
log = "#{configuration.deploy_to}/revisions.log"
|
||||
directory = File.basename(configuration.release_path)
|
||||
|
||||
command = <<-STR
|
||||
if [[ ! -d #{configuration.release_path} ]]; then
|
||||
#{guts}
|
||||
echo `date +"%Y-%m-%d %H:%M:%S"` $USER #{configuration.revision} #{directory} >> #{log};
|
||||
chmod 666 #{log};
|
||||
fi
|
||||
STR
|
||||
|
||||
actor.run(command, &block)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
require 'time'
|
||||
require 'switchtower/scm/base'
|
||||
|
||||
module SwitchTower
|
||||
module SCM
|
||||
|
@ -21,13 +22,7 @@ module SwitchTower
|
|||
# Also, you can specify the CVS_RSH variable to use on the remote machine(s)
|
||||
# via the <tt>:cvs_rsh</tt> variable. This defaults to the value of the
|
||||
# CVS_RSH environment variable locally, or if it is not set, to "ssh".
|
||||
class Cvs
|
||||
attr_reader :configuration
|
||||
|
||||
def initialize(configuration) #:nodoc:
|
||||
@configuration = configuration
|
||||
end
|
||||
|
||||
class Cvs < Base
|
||||
# Return a string representing the date of the last revision (CVS is
|
||||
# seriously retarded, in that it does not give you a way to query when
|
||||
# the last revision was made to the repository, so this is a fairly
|
||||
|
@ -37,7 +32,7 @@ module SwitchTower
|
|||
configuration.logger.debug "querying latest revision..."
|
||||
@latest_revision = cvs_log(configuration.local).
|
||||
split(/\r?\n/).
|
||||
grep(/^date: (.*?);/) { Time.parse($1).strftime("%FT%T") }.
|
||||
grep(/^date: (.*?);/) { Time.parse($1).strftime("%F %T") }.
|
||||
sort.
|
||||
last
|
||||
end
|
||||
|
@ -49,15 +44,11 @@ module SwitchTower
|
|||
cvs_rsh = configuration[:cvs_rsh] || ENV['CVS_RSH'] || "ssh"
|
||||
|
||||
command = <<-CMD
|
||||
if [[ -d #{actor.release_path} ]]; then
|
||||
cd #{actor.release_path};
|
||||
CVS_RSH="#{cvs_rsh}" #{cvs} -q up -d#{latest_revision};
|
||||
else
|
||||
cd #{configuration.releases_path};
|
||||
CVS_RSH="#{cvs_rsh}" #{cvs} -d #{configuration.repository} -q co -D #{latest_revision} -d #{latest_revision} #{actor.application};
|
||||
fi
|
||||
cd #{configuration.releases_path};
|
||||
CVS_RSH="#{cvs_rsh}" #{cvs} -d #{configuration.repository} -Q co -D "#{configuration.revision}" -d #{File.basename(actor.release_path)} #{actor.application};
|
||||
CMD
|
||||
actor.run(command) do |ch, stream, out|
|
||||
|
||||
run_checkout(actor, command) do |ch, stream, out|
|
||||
prefix = "#{stream} :: #{ch[:host]}"
|
||||
actor.logger.info out, prefix
|
||||
if out =~ %r{password:}
|
||||
|
@ -72,7 +63,7 @@ module SwitchTower
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def cvs_log(path)
|
||||
`cd #{path || "."} && cvs -q log -N -rHEAD`
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
require 'time'
|
||||
require 'switchtower/scm/base'
|
||||
|
||||
module SwitchTower
|
||||
module SCM
|
||||
|
@ -13,37 +13,13 @@ module SwitchTower
|
|||
# executable on the remote machine:
|
||||
#
|
||||
# set :darcs, "/opt/local/bin/darcs"
|
||||
class Darcs
|
||||
attr_reader :configuration
|
||||
|
||||
def initialize(configuration) #:nodoc:
|
||||
@configuration = configuration
|
||||
end
|
||||
|
||||
# Return an integer identifying the last known revision (patch) in the
|
||||
# darcs repository. (This integer is currently the 14-digit timestamp
|
||||
# of the last known patch.)
|
||||
def latest_revision
|
||||
unless @latest_revision
|
||||
configuration.logger.debug "querying latest revision..."
|
||||
@latest_revision = Time.
|
||||
parse(`darcs changes --last 1 --repo #{configuration.repository}`).
|
||||
strftime("%Y%m%d%H%M%S").to_i
|
||||
end
|
||||
@latest_revision
|
||||
end
|
||||
|
||||
class Darcs < Base
|
||||
# Check out (on all servers associated with the current task) the latest
|
||||
# revision. Uses the given actor instance to execute the command.
|
||||
def checkout(actor)
|
||||
darcs = configuration[:darcs] ? configuration[:darcs] : "darcs"
|
||||
|
||||
command = <<-CMD
|
||||
if [[ ! -d #{actor.release_path} ]]; then
|
||||
#{darcs} get --set-scripts-executable #{configuration.repository} #{actor.release_path};
|
||||
fi
|
||||
CMD
|
||||
actor.run(command)
|
||||
revision = configuration[:revision] ? %(--to-match "#{configuration.revision}") : ""
|
||||
run_checkout(actor, "#{darcs} get -q --set-scripts-executable #{revision} #{configuration.repository} #{actor.release_path};")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require 'switchtower/scm/base'
|
||||
|
||||
module SwitchTower
|
||||
module SCM
|
||||
|
||||
|
@ -12,13 +14,7 @@ module SwitchTower
|
|||
# executable on the remote machine:
|
||||
#
|
||||
# set :svn, "/opt/local/bin/svn"
|
||||
class Subversion
|
||||
attr_reader :configuration
|
||||
|
||||
def initialize(configuration) #:nodoc:
|
||||
@configuration = configuration
|
||||
end
|
||||
|
||||
class Subversion < Base
|
||||
# Return an integer identifying the last known revision in the svn
|
||||
# repository. (This integer is currently the revision number.) If latest
|
||||
# revision does not exist in the given repository, this routine will
|
||||
|
@ -46,14 +42,9 @@ module SwitchTower
|
|||
def checkout(actor)
|
||||
svn = configuration[:svn] ? configuration[:svn] : "svn"
|
||||
|
||||
command = <<-CMD
|
||||
if [[ -d #{actor.release_path} ]]; then
|
||||
#{svn} up -q -r#{latest_revision} #{actor.release_path};
|
||||
else
|
||||
#{svn} co -q -r#{latest_revision} #{configuration.repository} #{actor.release_path};
|
||||
fi
|
||||
CMD
|
||||
actor.run(command) do |ch, stream, out|
|
||||
command = "#{svn} co -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path};"
|
||||
|
||||
run_checkout(actor, command) do |ch, stream, out|
|
||||
prefix = "#{stream} :: #{ch[:host]}"
|
||||
actor.logger.info out, prefix
|
||||
if out =~ /^Password.*:/
|
||||
|
@ -64,9 +55,9 @@ module SwitchTower
|
|||
prefix
|
||||
ch.send_data "yes\n"
|
||||
elsif out =~ %r{passphrase}
|
||||
message = "subversion needs your key's passphrase and cannot proceed"
|
||||
message = "subversion needs your key's passphrase, sending empty string"
|
||||
actor.logger.info message, prefix
|
||||
raise message
|
||||
ch.send_data "\n"
|
||||
elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
|
||||
message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
|
||||
actor.logger.info message, prefix
|
||||
|
@ -76,7 +67,7 @@ module SwitchTower
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def svn_log(path)
|
||||
`svn log -q -rhead #{path}`
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ require './lib/switchtower/version'
|
|||
Gem::Specification.new do |s|
|
||||
|
||||
s.name = 'switchtower'
|
||||
s.version = SwitchTower::Version::STRING
|
||||
s.version = PKG_VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = <<-DESC.strip.gsub(/\n/, " ")
|
||||
SwitchTower is a framework and utility for executing commands in parallel
|
||||
|
|
|
@ -18,7 +18,6 @@ class ConfigurationTest < Test::Unit::TestCase
|
|||
|
||||
class MockSCM
|
||||
attr_reader :configuration
|
||||
attr_accessor :latest_revision
|
||||
|
||||
def initialize(config)
|
||||
@configuration = config
|
||||
|
@ -184,8 +183,7 @@ class ConfigurationTest < Test::Unit::TestCase
|
|||
|
||||
def test_release_path_implicit
|
||||
@config.set :deploy_to, "/start/of/path"
|
||||
@config.source.latest_revision = 2257
|
||||
assert_equal "/start/of/path/releases/2257", @config.release_path
|
||||
assert_equal "/start/of/path/releases/#{@config.now.strftime("%Y%m%d%H%M%S")}", @config.release_path
|
||||
end
|
||||
|
||||
def test_release_path_explicit
|
||||
|
|
|
@ -44,6 +44,10 @@ class ScmCvsTest < Test::Unit::TestCase
|
|||
story.each { |stream, line| yield @channels.last, stream, line }
|
||||
end
|
||||
|
||||
def release_path
|
||||
(@config[:now] || Time.now.utc).strftime("%Y%m%d%H%M%S")
|
||||
end
|
||||
|
||||
def method_missing(sym, *args)
|
||||
@config.send(sym, *args)
|
||||
end
|
||||
|
@ -55,6 +59,7 @@ class ScmCvsTest < Test::Unit::TestCase
|
|||
@config[:local] = "/hello/world"
|
||||
@config[:cvs] = "/path/to/cvs"
|
||||
@config[:password] = "chocolatebrownies"
|
||||
@config[:now] = Time.utc(2005,8,24,12,0,0)
|
||||
@scm = CvsTest.new(@config)
|
||||
@actor = MockActor.new(@config)
|
||||
@log_msg = <<MSG.strip
|
||||
|
@ -140,7 +145,7 @@ MSG
|
|||
|
||||
def test_latest_revision
|
||||
@scm.story = [ @log_msg ]
|
||||
assert_equal "2004-10-12T02:21:02", @scm.latest_revision
|
||||
assert_equal "2004-10-12 02:21:02", @scm.latest_revision
|
||||
assert_equal "/hello/world", @scm.last_path
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,11 @@ class MockLogger
|
|||
end
|
||||
|
||||
class MockConfiguration < Hash
|
||||
def initialize(*args)
|
||||
super
|
||||
self[:release_path] = "/path/to/releases/version"
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= MockLogger.new
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue