├── VERSION ├── spec ├── .rspec ├── support │ └── capistrano.rb ├── spec_helper.rb ├── localchanges_spec.rb └── capistrano-wp_spec.rb ├── .document ├── lib ├── capistrano │ ├── templates │ │ ├── Capfile │ │ └── config │ │ │ ├── deploy.rb │ │ │ └── deploy │ │ │ └── production.rb │ ├── crowdfavorite │ │ └── wordpress.rb │ └── recipes │ │ └── deploy │ │ └── scm │ │ └── git-enhanced.rb ├── capistrano-wp.rb ├── crowdfavorite │ ├── version.rb │ ├── tasks.rb │ ├── wordpress.rb │ ├── support │ │ ├── namespace.rb │ │ └── capistrano_extensions.rb │ └── tasks │ │ ├── localchanges.rb │ │ └── wordpress.rb └── crowdfavorite.rb ├── .gitignore ├── Gemfile ├── doc └── examples │ ├── Capfile │ └── config │ ├── staging-local-config.php │ ├── deploy.rb │ └── deploy │ ├── production.rb │ └── staging.rb ├── Gemfile.lock ├── bin └── capify-wp ├── Rakefile ├── capistrano-wp.gemspec ├── LICENSE.txt └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 -------------------------------------------------------------------------------- /spec/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /lib/capistrano/templates/Capfile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'railsless-deploy' 3 | require 'crowdfavorite/wordpress' 4 | load 'config/deploy' # remove this line to skip loading any of the default tasks 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "capistrano", "~> 2.15.3" 4 | gem "capistrano-ext", "~> 1.2.1" 5 | gem "railsless-deploy", "~> 1.1.2" 6 | #gem "colored", ">= 1.2" 7 | #gem "json", "~> 1.7.1", :platforms => [:ruby, :jruby] 8 | #gem "json-pure", :platforms => :mswin 9 | 10 | #gem "capistrano_colors", ">= 0.5.5" 11 | 12 | gem "erubis", "~> 2.7.0" 13 | 14 | group :development do 15 | gem "rspec", "~> 2.11" 16 | gem "bundler", "~> 1.0" 17 | gem "jeweler", "~> 1.8" 18 | gem "simplecov", ">= 0" 19 | gem "capistrano-spec" 20 | end 21 | -------------------------------------------------------------------------------- /lib/capistrano-wp.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'rubygems' 16 | 17 | require 'crowdfavorite' 18 | -------------------------------------------------------------------------------- /lib/crowdfavorite/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module CrowdFavorite 16 | VERSION = '0.2.0' 17 | end 18 | -------------------------------------------------------------------------------- /lib/crowdfavorite/tasks.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'crowdfavorite' 16 | module CrowdFavorite::Tasks 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/capistrano/crowdfavorite/wordpress.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'crowdfavorite/wordpress' 16 | $stderr.puts " * capistrano/crowdfavorite/wordpress is deprecated - please require 'crowdfavorite/wordpress' instead" 17 | -------------------------------------------------------------------------------- /spec/support/capistrano.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'capistrano/spec' 16 | 17 | RSpec.configure do |config| 18 | config.include Capistrano::Spec::Helpers 19 | config.include Capistrano::Spec::Matchers 20 | end 21 | -------------------------------------------------------------------------------- /doc/examples/Capfile: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'rubygems' 16 | require 'railsless-deploy' 17 | require 'crowdfavorite/wordpress' 18 | load 'config/deploy' # remove this line to skip loading any of the default tasks 19 | 20 | -------------------------------------------------------------------------------- /lib/crowdfavorite.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'rubygems' 16 | 17 | module CrowdFavorite 18 | module Support 19 | autoload :Namespace, 'crowdfavorite/support/namespace' 20 | end 21 | autoload :Version, 'crowdfavorite/version' 22 | end 23 | 24 | -------------------------------------------------------------------------------- /doc/examples/config/staging-local-config.php: -------------------------------------------------------------------------------- 1 | e 19 | $stderr.puts e.message 20 | $stderr.puts "Run `bundle install` to install missing gems" 21 | exit e.status_code 22 | end 23 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 24 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 25 | require 'crowdfavorite' 26 | require 'rspec' 27 | require 'rspec/autorun' 28 | 29 | CrowdFavorite::Support::Namespace.default_config = nil 30 | 31 | # Requires supporting files with custom matchers and macros, etc, 32 | # in ./support/ and its subdirectories. 33 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 34 | 35 | RSpec.configure do |config| 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/capistrano/templates/config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :stages, %w(production) 2 | set :default_stage, "production" 3 | 4 | require 'capistrano/ext/multistage' 5 | 6 | #============================================================================= 7 | # app details and WordPress requirements 8 | 9 | # tags/3.5.1, branches/3.5, trunk 10 | set :wordpress_version, "trunk" 11 | set :application, "my-wordpress-site.com" 12 | 13 | #============================================================================= 14 | # app source repository configuration 15 | 16 | set :scm, :git 17 | set :repository, "" 18 | set :git_enable_submodules, 1 19 | #set :git_shallow_clone, 1 20 | 21 | #============================================================================= 22 | # Housekeeping 23 | # clean up old releases on each deploy 24 | set :keep_releases, 5 25 | after "deploy:create_symlink", "deploy:cleanup" 26 | 27 | #============================================================================= 28 | # Additional Project specific directories 29 | 30 | # Uncomment these lines to additionally create your upload and cache 31 | # directories in the shared location when running `deploy:setup`. 32 | # 33 | # Modify these commands to make sure these directories are writable by 34 | # your web server. 35 | 36 | # after "deploy:setup" do 37 | # ['uploads', 'cache'].each do |dir| 38 | # run "cd #{shared_path} && mkdir #{dir} && chgrp www-data #{dir} && chmod 775 #{dir}" 39 | # end 40 | # end 41 | -------------------------------------------------------------------------------- /doc/examples/config/deploy.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set :stages, %w(production staging) 16 | set :default_stage, "production" 17 | 18 | require "capistrano/ext/multistage" 19 | 20 | #============================================================================= 21 | # app details and WordPress requirements 22 | 23 | # tags/3.5.2, branches/3.5, trunk 24 | set :wordpress_version, "branches/3.5" 25 | set :application, "my_application" 26 | 27 | #============================================================================= 28 | # app source repository configuration 29 | 30 | set :scm, :git 31 | set :repository, "https://github.com/example/example-wp.git" 32 | set :git_enable_submodules, 1 33 | 34 | #============================================================================= 35 | # Housekeeping 36 | # clean up old releases on each deploy 37 | set :keep_releases, 5 38 | after "deploy:create_symlink", "deploy:cleanup" 39 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | capistrano (2.15.4) 5 | highline 6 | net-scp (>= 1.0.0) 7 | net-sftp (>= 2.0.0) 8 | net-ssh (>= 2.0.14) 9 | net-ssh-gateway (>= 1.1.0) 10 | capistrano-ext (1.2.1) 11 | capistrano (>= 1.0.0) 12 | capistrano-spec (0.4.0) 13 | diff-lcs (1.2.4) 14 | erubis (2.7.0) 15 | git (1.2.5) 16 | highline (1.6.19) 17 | jeweler (1.8.4) 18 | bundler (~> 1.0) 19 | git (>= 1.2.5) 20 | rake 21 | rdoc 22 | json (1.8.0) 23 | multi_json (1.7.5) 24 | net-scp (1.1.1) 25 | net-ssh (>= 2.6.5) 26 | net-sftp (2.1.2) 27 | net-ssh (>= 2.6.5) 28 | net-ssh (2.6.7) 29 | net-ssh-gateway (1.2.0) 30 | net-ssh (>= 2.6.5) 31 | railsless-deploy (1.1.2) 32 | rake (10.0.4) 33 | rdoc (4.0.1) 34 | json (~> 1.4) 35 | rspec (2.13.0) 36 | rspec-core (~> 2.13.0) 37 | rspec-expectations (~> 2.13.0) 38 | rspec-mocks (~> 2.13.0) 39 | rspec-core (2.13.1) 40 | rspec-expectations (2.13.0) 41 | diff-lcs (>= 1.1.3, < 2.0) 42 | rspec-mocks (2.13.1) 43 | simplecov (0.7.1) 44 | multi_json (~> 1.0) 45 | simplecov-html (~> 0.7.1) 46 | simplecov-html (0.7.1) 47 | 48 | PLATFORMS 49 | ruby 50 | 51 | DEPENDENCIES 52 | bundler (~> 1.0) 53 | capistrano (~> 2.15.3) 54 | capistrano-ext (~> 1.2.1) 55 | capistrano-spec 56 | erubis (~> 2.7.0) 57 | jeweler (~> 1.8) 58 | railsless-deploy (~> 1.1.2) 59 | rspec (~> 2.11) 60 | simplecov 61 | -------------------------------------------------------------------------------- /bin/capify-wp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'fileutils' 5 | 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: #{File.basename($0)} [path]" 8 | 9 | opts.on("-h", "--help", "Displays this help info") do 10 | puts opts 11 | exit 0 12 | end 13 | 14 | begin 15 | opts.parse!(ARGV) 16 | rescue OptionParser::ParseError => e 17 | warn e.message 18 | puts opts 19 | exit 1 20 | end 21 | end 22 | 23 | if ARGV.empty? 24 | abort "Please specify the directory to capify, e.g. `#{File.basename($0)} .'" 25 | elsif !File.exists?(ARGV.first) 26 | abort "`#{ARGV.first}' does not exist." 27 | elsif !File.directory?(ARGV.first) 28 | abort "`#{ARGV.first}' is not a directory." 29 | elsif !File.writable?(ARGV.first) 30 | abort "`#{ARGV.first}' is not writable by you." 31 | elsif ARGV.length > 1 32 | abort "Too many arguments; please specify only the directory to capify-wp." 33 | end 34 | 35 | def template_dir() 36 | t = ["#{File.dirname(File.expand_path(__FILE__))}/../lib/capistrano/templates", 37 | "#{Gem::Specification.find_by_name("capistrano-wp")}/lib/capisrano/templates"] 38 | t.each { |dir| return dir if File.readable? dir } 39 | raise "Paths invalid: #{t}" if not File.readable? t 40 | end 41 | 42 | base = ARGV.shift 43 | templates = template_dir() 44 | 45 | Dir.glob("#{templates}/**/*").each do |file| 46 | target = file.gsub(templates, "") 47 | next if target.empty? 48 | 49 | target = File.join(base, target) 50 | 51 | if File.exists? target 52 | warn "[skip] '#{target}' already exists" 53 | elsif File.exists? target.downcase 54 | warn "[skip] '#{target.downcase}' exists, which could conflict with `#{target}'" 55 | else 56 | if File.directory? file 57 | puts "[add] making directory '#{target}'" 58 | FileUtils.mkdir_p(target) 59 | else 60 | puts "[add] writing '#{target}'" 61 | FileUtils.cp file, target 62 | end 63 | end 64 | end 65 | 66 | puts "[done] capify-wped!" 67 | -------------------------------------------------------------------------------- /spec/localchanges_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 16 | require 'crowdfavorite/tasks/localchanges' 17 | 18 | describe CrowdFavorite::Tasks::LocalChanges, "loaded into capistrano" do 19 | before do 20 | @configuration = Capistrano::Configuration.new 21 | @configuration.extend(Capistrano::Spec::ConfigurationExtension) 22 | CrowdFavorite::Tasks::LocalChanges.load_into(@configuration) 23 | end 24 | 25 | it "defines cf:localchanges:snapshot" do 26 | @configuration.find_task('cf:localchanges:snapshot').should_not == nil 27 | end 28 | it "defines cf:localchanges:compare" do 29 | @configuration.find_task('cf:localchanges:compare').should_not == nil 30 | end 31 | it "defines cf:localchanges:allow_differences" do 32 | @configuration.find_task('cf:localchanges:allow_differences').should_not == nil 33 | end 34 | it "defines cf:localchanges:forbid_differences" do 35 | @configuration.find_task('cf:localchanges:forbid_differences').should_not == nil 36 | end 37 | it "sets snapshot_allow_differences to false" do 38 | @configuration.find_and_execute_task('cf:localchanges:forbid_differences') 39 | @configuration.fetch(:snapshot_allow_differences, nil).should === false 40 | end 41 | it "sets snapshot_allow_differences to true" do 42 | @configuration.find_and_execute_task('cf:localchanges:allow_differences') 43 | @configuration.fetch(:snapshot_allow_differences, nil).should === true 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright 2012-2013 Crowd Favorite, Ltd. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | require 'rubygems' 17 | require 'bundler' 18 | begin 19 | Bundler.setup(:default, :development) 20 | rescue Bundler::BundlerError => e 21 | $stderr.puts e.message 22 | $stderr.puts "Run `bundle install` to install missing gems" 23 | exit e.status_code 24 | end 25 | require 'rake' 26 | 27 | require 'jeweler' 28 | Jeweler::Tasks.new do |gem| 29 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 30 | gem.name = "capistrano-wp" 31 | gem.homepage = "http://github.com/crowdfavorite/gem-capistrano-wp" 32 | gem.license = "Apache License version 2" 33 | gem.summary = %Q{Crowd Favorite WordPress Capistrano recipes} 34 | gem.description = <<-EOF.gsub(/^ {4}/, '') 35 | Recipes for deploying and maintaining remote WordPress installations with 36 | Capistrano. Pulls in WordPress from SVN, optionally using a local or 37 | remote cache, and supports a number of common operations and tasks towards 38 | the care and feeding of sites that may not be 100% maintained through 39 | version control. 40 | EOF 41 | gem.authors = ["Crowd Favorite"] 42 | # dependencies defined in Gemfile 43 | end 44 | Jeweler::RubygemsDotOrgTasks.new 45 | 46 | require 'rspec/core' 47 | require 'rspec/core/rake_task' 48 | RSpec::Core::RakeTask.new(:spec) do |spec| 49 | spec.pattern = FileList['spec/**/*_spec.rb'] 50 | end 51 | 52 | RSpec::Core::RakeTask.new(:rcov) do |spec| 53 | ENV["COVERAGE"] = 'yes' 54 | Rake::Task['spec'].execute 55 | end 56 | 57 | task :default => :spec 58 | 59 | -------------------------------------------------------------------------------- /lib/crowdfavorite/support/namespace.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'capistrano' 16 | require 'crowdfavorite/support/capistrano_extensions' 17 | 18 | # This module is used to capture the definition of capistrano tasks, which makes it 19 | # easier to test the behaviour of specific tasks without loading everything. If you 20 | # are writing tests for a collection of tasks, you should put those tasks in a module 21 | # and extend that module with `CrowdFavorite::Support::Namespace. 22 | # 23 | # You can look at some of the existing tasks (such as [env](../tasks/env.html)) and 24 | # its corresponding specs for an example of this in practice. 25 | # 26 | # You should not need to use this module directly when using recap to deploy. 27 | 28 | module CrowdFavorite::Support::Namespace 29 | def self.default_config 30 | @default_config 31 | end 32 | 33 | def self.default_config=(config) 34 | @default_config = config 35 | end 36 | 37 | if Capistrano::Configuration.instance 38 | self.default_config = Capistrano::Configuration.instance(:must_exist) 39 | end 40 | 41 | def capistrano_definitions 42 | @capistrano_definitions ||= [] 43 | end 44 | 45 | def namespace(name, &block) 46 | capistrano_definitions << Proc.new do 47 | namespace name do 48 | instance_eval(&block) 49 | end 50 | end 51 | 52 | load_into(CrowdFavorite::Support::Namespace.default_config) if CrowdFavorite::Support::Namespace.default_config 53 | end 54 | 55 | def load_into(configuration) 56 | configuration.extend(self) 57 | capistrano_definitions.each do |definition| 58 | configuration.load(&definition) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/capistrano/templates/config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | set :stage, "production" 2 | 3 | set :user, 'deployuser' 4 | set :use_sudo, false 5 | 6 | server '0.0.0.0', :app, :web, :db, :primary => true 7 | 8 | ## For multiple server setups 9 | # 10 | # server '0.0.0.0', :app, :web, :primary => true 11 | # server '0.0.0.0', :app, :web 12 | # # Don't push code to this server with 'cap deploy' 13 | # server '0.0.0.0', :db, :no_release => true 14 | 15 | # Site base directory 16 | set :base_dir, "/var/local/www/example" 17 | set :deploy_to, File.join(fetch(:base_dir)) 18 | 19 | # Webroot 20 | set :current_dir, "httpdocs" 21 | 22 | # Path to WordPress, supports WP at the root (empty string) and WordPress 23 | # in a custom location (webroot/wp in our example). 24 | set(:wp_path) { File.join(release_path, "wp") } 25 | 26 | # Deploy strategy - use :remote_cache when possible, but some servers need :copy 27 | #set :deploy_via, :copy 28 | set :deploy_via, :remote_cache 29 | 30 | # Specify a git branch to deploy 31 | # 32 | # Using fetch() here allows you to set your branch from the command line, 33 | # but allows a default, "master" in this case. 34 | # 35 | # cap deploy -s branch=my-custom-branch 36 | # 37 | set :branch, fetch(:branch, "master") 38 | 39 | #============================================================================= 40 | # Files to link or copy into web root from capistrano's shared directory 41 | # Symlinks are symlinked in 42 | 43 | # wp_symlinks defaults to: 44 | # "cache" => "wp-content/cache" 45 | # "uploads" => "wp-content/uploads" 46 | # "blogs.dir" => "wp-content/blogs.dir" 47 | # 48 | # To override, set the target to nil: 49 | # 50 | #set :wp_symlinks, [{ 51 | # "cache" => nil 52 | #}] 53 | # 54 | # Or add other files: 55 | # 56 | #set :wp_symlinks, [{ 57 | # "authcache" => "wp-content/authcache" 58 | #}] 59 | # 60 | # Configs are copied in, and default to: 61 | # "db-config.php" => "/", 62 | # "advanced-cache.php" => "wp-content/", 63 | # "object-cache.php" => "wp-content/", 64 | # "*.html" => "/", 65 | # 66 | # To override (like wp_symlinks): 67 | #set :wp_configs, [{ 68 | #}] 69 | # 70 | # Stage-specific overrides are copied from the config directory, 71 | # like production-example.txt or staging-example.txt 72 | # Default list: 73 | # 74 | # "local-config.php" => "local-config.php", 75 | # ".htaccess" => ".htaccess" 76 | # 77 | # To override or add other files (as above, but note no []): 78 | # 79 | #set :stage_specific_overrides, { 80 | #} 81 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/git-enhanced.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | require 'capistrano/recipes/deploy/scm/git' 3 | 4 | module Capistrano 5 | module Deploy 6 | module SCM 7 | class Git < Base 8 | # Merges the changes to 'head' since the last fetch, for remote_cache 9 | # deployment strategy 10 | def sync(revision, destination) 11 | git = command 12 | remote = origin 13 | 14 | execute = [] 15 | execute << "cd #{destination}" 16 | 17 | # Use git-config to setup a remote tracking branches. Could use 18 | # git-remote but it complains when a remote of the same name already 19 | # exists, git-config will just silenty overwrite the setting every 20 | # time. This could cause wierd-ness in the remote cache if the url 21 | # changes between calls, but as long as the repositories are all 22 | # based from each other it should still work fine. 23 | # 24 | # Since it's even worse to have the URL be out of date 25 | # than it is to set it too many times, set it every time 26 | # even for origin. 27 | execute << "#{git} config remote.#{remote}.url #{variable(:repository)}" 28 | execute << "#{git} config remote.#{remote}.fetch +refs/heads/*:refs/remotes/#{remote}/*" 29 | 30 | # since we're in a local branch already, just reset to specified revision rather than merge 31 | execute << "#{git} fetch #{verbose} #{remote} && #{git} fetch --tags #{verbose} #{remote} && #{git} reset #{verbose} --hard #{revision}" 32 | 33 | if variable(:git_enable_submodules) 34 | execute << "#{git} submodule #{verbose} init" 35 | execute << "#{git} submodule #{verbose} sync" 36 | if false == variable(:git_submodules_recursive) 37 | execute << "#{git} submodule #{verbose} update --init" 38 | else 39 | execute << %Q(export GIT_RECURSIVE=$([ ! "`#{git} --version`" \\< "git version 1.6.5" ] && echo --recursive)) 40 | execute << "#{git} submodule #{verbose} update --init $GIT_RECURSIVE" 41 | end 42 | end 43 | 44 | # Make sure there's nothing else lying around in the repository (for 45 | # example, a submodule that has subsequently been removed). 46 | execute << "#{git} clean #{verbose} -d -x -f -f" 47 | 48 | if variable(:git_enable_submodules) 49 | execute << "#{git} submodule #{verbose} foreach $GIT_RECURSIVE #{git} clean #{verbose} -d -x -f -f" 50 | end 51 | execute.join(" && ") 52 | end 53 | end 54 | end 55 | end 56 | end 57 | 58 | -------------------------------------------------------------------------------- /doc/examples/config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set :stage, "production" 16 | 17 | set :user, 'deployuser' 18 | set :use_sudo, false 19 | 20 | server '172.16.42.42', :app, :web, :primary => true 21 | server '172.16.42.43', :app, :web 22 | 23 | # Don't push code to this server with 'cap deploy' 24 | server '172.16.17.17', :db, :no_release => true 25 | 26 | set :base_dir, "/var/local/www/example" 27 | set :deploy_to, File.join(fetch(:base_dir)) 28 | set :current_dir, "httpdocs" 29 | set(:wp_path) { File.join(release_path, "wp") } 30 | # :version_dir - where versions live, 'versions' 31 | # :shared_dir - where shared files (wordpress cache, et cetera) live, 'shared' 32 | 33 | # Deploy strategy - use :remote_cache when possible, but some servers need :copy 34 | #set :deploy_via, :remote_cache 35 | set :deploy_via, :remote_cache 36 | 37 | # Specify a git branch to deploy 38 | set :branch, fetch(:branch, "master") 39 | 40 | #============================================================================= 41 | # Files to link or copy into web root 42 | # Symlinks are symlinked in 43 | 44 | # wp_symlinks defaults to: 45 | # "cache" => "wp-content/cache" 46 | # "uploads" => "wp-content/uploads" 47 | # "blogs.dir" => "wp-content/blogs.dir" 48 | # 49 | # To override, set the target to nil: 50 | # 51 | #set :wp_symlinks, [{ 52 | # "cache" => nil 53 | #}] 54 | # 55 | # Or add other files: 56 | # 57 | #set :wp_symlinks, [{ 58 | # "authcache" => "wp-content/authcache" 59 | #}] 60 | # 61 | # Configs are copied in, and default to: 62 | # "db-config.php" => "/", 63 | # "advanced-cache.php" => "wp-content/", 64 | # "object-cache.php" => "wp-content/", 65 | # "*.html" => "/", 66 | # 67 | # To override (like wp_symlinks): 68 | #set :wp_configs, [{ 69 | #}] 70 | # 71 | # Stage-specific overrides are copied from the config directory, 72 | # like production-example.txt or staging-example.txt 73 | # Default list: 74 | # 75 | # "local-config.php" => "local-config.php", 76 | # ".htaccess" => ".htaccess" 77 | # 78 | # To override or add other files (as above, but note no []): 79 | # 80 | #set :stage_specific_overrides, { 81 | #} 82 | -------------------------------------------------------------------------------- /doc/examples/config/deploy/staging.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set :stage, "staging" 16 | 17 | set :user, 'deployuser' 18 | set :use_sudo, false 19 | 20 | server '172.16.1.42', :app, :web, :primary => true 21 | server '172.16.1.43', :app, :web 22 | 23 | # Don't push code to this server with 'cap deploy' 24 | server '172.16.1.17', :db, :no_release => true 25 | 26 | set :base_dir, "/var/local/www/example-staging" 27 | set :deploy_to, File.join(fetch(:base_dir)) 28 | set :current_dir, "httpdocs" 29 | set(:wp_path) { File.join(release_path, "wp") } 30 | # :version_dir - where versions live, 'versions' 31 | # :shared_dir - where shared files (wordpress cache, et cetera) live, 'shared' 32 | 33 | # Deploy strategy - use :remote_cache when possible, but some servers need :copy 34 | #set :deploy_via, :remote_cache 35 | set :deploy_via, :remote_cache 36 | 37 | # Specify a git branch to deploy 38 | set :branch, fetch(:branch, "develop") 39 | 40 | #============================================================================= 41 | # Files to link or copy into web root 42 | # Symlinks are symlinked in 43 | 44 | # wp_symlinks defaults to: 45 | # "cache" => "wp-content/cache" 46 | # "uploads" => "wp-content/uploads" 47 | # "blogs.dir" => "wp-content/blogs.dir" 48 | # 49 | # To override, set the target to nil: 50 | # 51 | #set :wp_symlinks, [{ 52 | # "cache" => nil 53 | #}] 54 | # 55 | # Or add other files: 56 | # 57 | #set :wp_symlinks, [{ 58 | # "authcache" => "wp-content/authcache" 59 | #}] 60 | # 61 | # Configs are copied in, and default to: 62 | # "db-config.php" => "/", 63 | # "advanced-cache.php" => "wp-content/", 64 | # "object-cache.php" => "wp-content/", 65 | # "*.html" => "/", 66 | # 67 | # To override (like wp_symlinks): 68 | #set :wp_configs, [{ 69 | #}] 70 | # 71 | # Stage-specific overrides are copied from the config directory, 72 | # like production-example.txt or staging-example.txt 73 | # Default list: 74 | # 75 | # "local-config.php" => "local-config.php", 76 | # ".htaccess" => ".htaccess" 77 | # 78 | # To override or add other files (as above, but note no []): 79 | # 80 | #set :stage_specific_overrides, { 81 | #} 82 | -------------------------------------------------------------------------------- /spec/capistrano-wp_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 16 | require 'crowdfavorite/tasks/wordpress' 17 | 18 | describe CrowdFavorite::Tasks::WordPress, "loaded into capistrano" do 19 | before do 20 | @configuration = Capistrano::Configuration.new 21 | @configuration.extend(Capistrano::Spec::ConfigurationExtension) 22 | CrowdFavorite::Tasks::WordPress.load_into(@configuration) 23 | end 24 | 25 | it "defines cf:wordpress:install" do 26 | @configuration.find_task('cf:wordpress:install').should_not == nil 27 | end 28 | 29 | it "defines cf:wordpress:install:with_remote_cache" do 30 | @configuration.find_task('cf:wordpress:install:with_remote_cache').should_not == nil 31 | end 32 | 33 | it "defines cf:wordpress:install:with_copy" do 34 | @configuration.find_task('cf:wordpress:install:with_copy').should_not == nil 35 | end 36 | 37 | it "defines cf:wordpress:generate_config" do 38 | @configuration.find_task('cf:wordpress:generate_config').should_not == nil 39 | end 40 | 41 | it "defines cf:wordpress:link_symlinks" do 42 | @configuration.find_task('cf:wordpress:link_symlinks').should_not == nil 43 | end 44 | 45 | it "defines cf:wordpress:copy_configs" do 46 | @configuration.find_task('cf:wordpress:copy_configs').should_not == nil 47 | end 48 | 49 | it "defines cf:wordpress:touch_release" do 50 | @configuration.find_task('cf:wordpress:touch_release').should_not == nil 51 | end 52 | 53 | it "does cf:wordpress:touch_release before deploy:cleanup" do 54 | @configuration.should callback('cf:wordpress:touch_release').after('deploy:finalize_update') 55 | end 56 | 57 | it "does cf:wordpress:generate_config before deploy:finalize_update" do 58 | @configuration.should callback('cf:wordpress:generate_config').before('deploy:finalize_update') 59 | end 60 | 61 | it "does cf:wordpress:link_symlinks after cf:wordpress:generate_config" do 62 | @configuration.should callback('cf:wordpress:link_symlinks').after('cf:wordpress:generate_config') 63 | end 64 | 65 | it "does cf:wordpress:copy_configs after cf:wordpress:link_symlinks" do 66 | @configuration.should callback('cf:wordpress:copy_configs').after('cf:wordpress:link_symlinks') 67 | end 68 | 69 | it "does cf:wordpress:install after cf:wordpress:copy_configs" do 70 | @configuration.should callback('cf:wordpress:install').after('cf:wordpress:copy_configs') 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/crowdfavorite/support/capistrano_extensions.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'tempfile' 16 | 17 | # These methods are used by recap tasks to run commands and detect when files have changed 18 | # as part of a deployments 19 | 20 | module CrowdFavorite::Support::CapistranoExtensions 21 | # Run a command as the given user 22 | def as_user(user, command, pwd = deploy_to) 23 | sudo "su - #{user} -c 'cd #{pwd} && #{command}'" 24 | end 25 | 26 | # Run a command as root 27 | def as_root(command, pwd = deploy_to) 28 | as_user 'root', command, pwd 29 | end 30 | 31 | # Run a command as the application user 32 | def as_app(command, pwd = deploy_to) 33 | as_user application_user, command, pwd 34 | end 35 | 36 | # Put a string into a file as the application user 37 | def put_as_app(string, path) 38 | put string, "/tmp/cf-put-as-app" 39 | as_app "cp /tmp/cf-put-as-app #{path} && chmod g+rw #{path}", "/" 40 | ensure 41 | run "rm /tmp/cf-put-as-app" 42 | end 43 | 44 | def editor 45 | ENV['DEPLOY_EDITOR'] || ENV['EDITOR'] 46 | end 47 | 48 | # Edit a file on the remote server, using a local editor 49 | def edit_file(path) 50 | if editor 51 | as_app "touch #{path} && chmod g+rw #{path}" 52 | local_path = Tempfile.new('deploy-edit').path 53 | get(path, local_path) 54 | CrowdFavorite::Support::ShellCommand.execute_interactive("#{editor} #{local_path}") 55 | File.read(local_path) 56 | else 57 | abort "To edit a remote file, either the EDITOR or DEPLOY_EDITOR environment variables must be set" 58 | end 59 | end 60 | 61 | # Run a git command in the `deploy_to` directory 62 | def git(command) 63 | run "cd #{deploy_to} && umask 002 && sg #{application_group} -c \"git #{command}\"" 64 | end 65 | 66 | # Capture the result of a git command run within the `deploy_to` directory 67 | def capture_git(command) 68 | capture "cd #{deploy_to} && umask 002 && sg #{application_group} -c 'git #{command}'" 69 | end 70 | 71 | def exit_code(command) 72 | capture("#{command} > /dev/null 2>&1; echo $?").strip 73 | end 74 | 75 | def exit_code_as_app(command, pwd = deploy_to) 76 | capture(%|sudo -p 'sudo password: ' su - #{application_user} -c 'cd #{pwd} && #{command} > /dev/null 2>&1'; echo $?|).strip 77 | end 78 | 79 | # Find the latest tag from the repository. As `git tag` returns tags in order, and our release 80 | # tags are timestamps, the latest tag will always be the last in the list. 81 | def latest_tag_from_repository 82 | result = capture_git("tag | tail -n1").strip 83 | result.empty? ? nil : result 84 | end 85 | 86 | # Does the given file exist within the deployment directory? 87 | def deployed_file_exists?(path, root_path = deploy_to) 88 | exit_code("cd #{root_path} && [ -f #{path} ]") == "0" 89 | end 90 | 91 | # Has the given path been created or changed since the previous deployment? During the first 92 | # successful deployment this will always return true. 93 | def deployed_file_changed?(path) 94 | return true unless latest_tag 95 | exit_code("cd #{deploy_to} && git diff --exit-code #{latest_tag} origin/#{branch} #{path}") == "1" 96 | end 97 | 98 | Capistrano::Configuration.send :include, self 99 | end 100 | -------------------------------------------------------------------------------- /capistrano-wp.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: capistrano-wp 0.5.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "capistrano-wp" 9 | s.version = "0.5.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Crowd Favorite"] 14 | s.date = "2014-10-22" 15 | s.description = "Recipes for deploying and maintaining remote WordPress installations with\nCapistrano. Pulls in WordPress from SVN, optionally using a local or\nremote cache, and supports a number of common operations and tasks towards\nthe care and feeding of sites that may not be 100% maintained through\nversion control.\n" 16 | s.executables = ["capify-wp"] 17 | s.extra_rdoc_files = [ 18 | "LICENSE.txt", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".document", 23 | "Gemfile", 24 | "Gemfile.lock", 25 | "LICENSE.txt", 26 | "README.md", 27 | "Rakefile", 28 | "VERSION", 29 | "bin/capify-wp", 30 | "capistrano-wp.gemspec", 31 | "doc/examples/Capfile", 32 | "doc/examples/config/deploy.rb", 33 | "doc/examples/config/deploy/production.rb", 34 | "doc/examples/config/deploy/staging.rb", 35 | "doc/examples/config/staging-local-config.php", 36 | "lib/capistrano-wp.rb", 37 | "lib/capistrano/crowdfavorite/wordpress.rb", 38 | "lib/capistrano/recipes/deploy/scm/git-enhanced.rb", 39 | "lib/capistrano/templates/Capfile", 40 | "lib/capistrano/templates/config/deploy.rb", 41 | "lib/capistrano/templates/config/deploy/production.rb", 42 | "lib/crowdfavorite.rb", 43 | "lib/crowdfavorite/support/capistrano_extensions.rb", 44 | "lib/crowdfavorite/support/namespace.rb", 45 | "lib/crowdfavorite/tasks.rb", 46 | "lib/crowdfavorite/tasks/localchanges.rb", 47 | "lib/crowdfavorite/tasks/wordpress.rb", 48 | "lib/crowdfavorite/version.rb", 49 | "lib/crowdfavorite/wordpress.rb", 50 | "spec/.rspec", 51 | "spec/capistrano-wp_spec.rb", 52 | "spec/localchanges_spec.rb", 53 | "spec/spec_helper.rb", 54 | "spec/support/capistrano.rb" 55 | ] 56 | s.homepage = "http://github.com/crowdfavorite/gem-capistrano-wp" 57 | s.licenses = ["Apache License version 2"] 58 | s.rubygems_version = "2.2.2" 59 | s.summary = "Crowd Favorite WordPress Capistrano recipes" 60 | 61 | if s.respond_to? :specification_version then 62 | s.specification_version = 4 63 | 64 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 65 | s.add_runtime_dependency(%q, ["~> 2.15.3"]) 66 | s.add_runtime_dependency(%q, ["~> 1.2.1"]) 67 | s.add_runtime_dependency(%q, ["~> 1.1.2"]) 68 | s.add_runtime_dependency(%q, ["~> 2.7.0"]) 69 | s.add_development_dependency(%q, ["~> 2.11"]) 70 | s.add_development_dependency(%q, ["~> 1.0"]) 71 | s.add_development_dependency(%q, ["~> 1.8"]) 72 | s.add_development_dependency(%q, [">= 0"]) 73 | s.add_development_dependency(%q, [">= 0"]) 74 | else 75 | s.add_dependency(%q, ["~> 2.15.3"]) 76 | s.add_dependency(%q, ["~> 1.2.1"]) 77 | s.add_dependency(%q, ["~> 1.1.2"]) 78 | s.add_dependency(%q, ["~> 2.7.0"]) 79 | s.add_dependency(%q, ["~> 2.11"]) 80 | s.add_dependency(%q, ["~> 1.0"]) 81 | s.add_dependency(%q, ["~> 1.8"]) 82 | s.add_dependency(%q, [">= 0"]) 83 | s.add_dependency(%q, [">= 0"]) 84 | end 85 | else 86 | s.add_dependency(%q, ["~> 2.15.3"]) 87 | s.add_dependency(%q, ["~> 1.2.1"]) 88 | s.add_dependency(%q, ["~> 1.1.2"]) 89 | s.add_dependency(%q, ["~> 2.7.0"]) 90 | s.add_dependency(%q, ["~> 2.11"]) 91 | s.add_dependency(%q, ["~> 1.0"]) 92 | s.add_dependency(%q, ["~> 1.8"]) 93 | s.add_dependency(%q, [">= 0"]) 94 | s.add_dependency(%q, [">= 0"]) 95 | end 96 | end 97 | 98 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | capistrano-wp - Capistrano recipes for WordPress deploys 2 | 3 | Copyright (c) 2012-2013 Crowd Favorite, Ltd 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 18 | 19 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 20 | 21 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 22 | 23 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 24 | 25 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 26 | 27 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 28 | 29 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 30 | 31 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 32 | 33 | 2. Grant of Copyright License. 34 | 35 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 36 | 37 | 3. Grant of Patent License. 38 | 39 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 40 | 41 | 4. Redistribution. 42 | 43 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 44 | 45 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 46 | You must cause any modified files to carry prominent notices stating that You changed the files; and 47 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 48 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 49 | 50 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 51 | 52 | 5. Submission of Contributions. 53 | 54 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 55 | 56 | 6. Trademarks. 57 | 58 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 59 | 60 | 7. Disclaimer of Warranty. 61 | 62 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 63 | 64 | 8. Limitation of Liability. 65 | 66 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 67 | 68 | 9. Accepting Warranty or Additional Liability. 69 | 70 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 71 | 72 | END OF TERMS AND CONDITIONS 73 | 74 | APPENDIX: How to apply the Apache License to your work 75 | 76 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 77 | 78 | Copyright [yyyy] [name of copyright owner] 79 | 80 | Licensed under the Apache License, Version 2.0 (the "License"); 81 | you may not use this file except in compliance with the License. 82 | You may obtain a copy of the License at 83 | 84 | http://www.apache.org/licenses/LICENSE-2.0 85 | 86 | Unless required by applicable law or agreed to in writing, software 87 | distributed under the License is distributed on an "AS IS" BASIS, 88 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 89 | See the License for the specific language governing permissions and 90 | limitations under the License. 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capistrano-wp 2 | 3 | Recipes for deploying and maintaining remote WordPress installations with 4 | Capistrano. 5 | 6 | This is an alternative version control and deployment strategy from the 7 | one presented in [WP-Stack](https://github.com/markjaquith/WP-Stack). 8 | WP-Stack expects WordPress Core to be included in the project as a git 9 | submodule; these recipes pull WordPress in from SVN (and can therefore 10 | also deploy multisite environments with WP at the root). 11 | 12 | **This is a Capistrano 2.X plugin, not for use with capistrano 3** 13 | 14 | **[Contribute to the (possible) migration to Cap 3](https://github.com/crowdfavorite/gem-capistrano-wp/issues/6)** 15 | 16 | 17 | ## Usage 18 | 19 | This is a plugin for the Capistrano deployment tool. If you are unfamiliar 20 | with Capistrano, we would suggest at least familiarizing yourself with 21 | the general concepts outlined in the [Capistrano Wiki](https://github.com/capistrano/capistrano/wiki). 22 | 23 | ### Assumptions (Requirements) 24 | 25 | - Your code repository is your webroot 26 | 27 | ### Install / Setup 28 | 29 | gem install capistrano-wp 30 | cd /path/to/repository 31 | capify-wp . 32 | 33 | ### Abridged General Capistrano Usage 34 | 35 | 1. Create a user for deploying your WordPress install 36 | 2. Create an SSH key for the deploy user, and make sure you can SSH to it from your local machine 37 | 3. [Install RubyGems][rubygems]. Crowd Favorite prefers to use [RVM][rvm] to maintain ruby versions, rubygems, and self-contained sets of gems. 38 | 4. Install the capistrano-wp gem (which will install Capistrano and friends): `gem install capistrano-wp` 39 | 5. Follow **Install / Setup** steps above 40 | 6. Make sure your `:deploy_to` path exists and is owned by the deploy user 41 | 7. Run `cap deploy:setup` to set up the initial directories 42 | 8. Run `cap deploy` to push out a new version of your code 43 | 9. Update your web server configuration to point to the current-release directory (in the `:deply_to` directory, named `httpdocs` by default) 44 | 10. Relax and enjoy painless deployment 45 | 46 | ## Capistrano Multi-stage 47 | 48 | This deployment strategy comes with multi-stage support baked in. 49 | 50 | For documentation regarding this portion of functionality, see the 51 | [Capistrano Multistage Documentation](https://github.com/capistrano/capistrano/wiki/2.x-Multistage-Extension). 52 | 53 | ## Capistrano-WP Specific Features 54 | 55 | ### Handling of WordPress 56 | 57 | This gem handles WordPress via SVN directly from WordPress.org. 58 | 59 | In your main `config/deploy.rb` file you will see how to declare what 60 | version of WordPress you wish to use by defining an SVN location 61 | like `branches/3.6`, `tags/3.6.1` or even `trunk`: 62 | 63 | ```ruby 64 | set :wordpress_version, "branches/3.5" 65 | ``` 66 | 67 | It then places WordPress where you declare it to live within the stage 68 | specific configuration files, for example `config/deploy/production.rb`: 69 | 70 | ```ruby 71 | set(:wp_path) { File.join(release_path, "wp") } 72 | ``` 73 | 74 | This places WordPress in a directory called "wp" within your webroot. 75 | 76 | It also gracefully handles the situation where both your code repository 77 | and WordPress live at the webroot. 78 | 79 | This process enables you to not have to track WordPress within your code 80 | repository. 81 | 82 | If for some reason you want to avoid installing WordPress, omit or set to 83 | false the `:wordpress_version` option: 84 | 85 | ```ruby 86 | set :wordpress_version, false 87 | ``` 88 | 89 | ### Persistent file/directory symlinks 90 | 91 | This gem augments the way capistrano handles directories you need to "persist" 92 | between releases. Providing a declarative interface for these items. 93 | 94 | There are some common directories that WordPress needs to act this way. By 95 | default, if the following directories exist in the "shared" directory, they 96 | will be symlinked into every release. 97 | 98 | - `cache` is linked to `wp-content/cache` 99 | - `uploads` is linked to `wp-content/uploads` 100 | - `blogs.dir` is linked to `wp-content/blogs.dir` 101 | 102 | This is the way these would be declared, either in the main `config/deploy.rb` or 103 | in your stage specific files, if they weren't defaults 104 | 105 | ```ruby 106 | set :wp_symlinks, [{ 107 | "cache" => "wp-content/cache" 108 | "uploads" => "wp-content/uploads" 109 | "blogs.dir" => "wp-content/blogs.dir" 110 | }] 111 | ``` 112 | 113 | These will happen without any further configuration changes. If you wish 114 | to override any of these defaults, you can set the target of the link to `nil` 115 | 116 | ```ruby 117 | set :wp_symlinks, [{ 118 | "cache" => nil 119 | }] 120 | ``` 121 | 122 | This would turn off the default `cache` symlink 123 | 124 | You can easily add your own project (or even stage) specific links 125 | 126 | If you have a `customlink` directory in the shared directory, you can add 127 | a custom link like so. 128 | 129 | ```ruby 130 | set :wp_symlinks, [{ 131 | "customlink" => "wp-content/themes/mytheme/customlinktarget" 132 | }] 133 | ``` 134 | 135 | ### Persistent Configs 136 | 137 | These are handled almost identically as above except they are copied 138 | from the shared directory instead of symlinked. 139 | 140 | This is primarily for config files that are sometimes written 141 | to by plugins. In some cases when php tries to write to a symbolic 142 | link, the link is destroyed and becomes a zero byte file. 143 | 144 | By default the following copies are attempted 145 | 146 | ```ruby 147 | set :wp_configs, [{ 148 | "db-config.php" => "/", 149 | "advanced-cache.php" => "wp-content/", 150 | "object-cache.php" => "wp-content/", 151 | "*.html" => "/", 152 | }] 153 | ``` 154 | 155 | You can follow the same steps as the symlinks for modification or addition 156 | to the default config copying rules. 157 | 158 | ### Stage specific overrides 159 | 160 | Stage specific overrides allow you to target specific configuration 161 | files to their respective stage. 162 | 163 | You need to use a specific set of `.htaccess` rules for production. 164 | 165 | If you place a file named `production-htaccess` in your `config/` directory 166 | 167 | and add it to your `:stage_specific_overrides` in your `config/deploy/production.rb` 168 | 169 | ```ruby 170 | set :stage_specific_overrides, { 171 | ".htaccess" => ".htaccess" 172 | } 173 | ``` 174 | 175 | This will place the proper `production-htaccess` file in the root of 176 | your next release, overriding any existing file of the same name. 177 | 178 | By default, it looks for the common `.htaccess` situation 179 | along with `local-config.php` 180 | 181 | ```ruby 182 | set :stage_specific_overrides, { 183 | "local-config.php" => "local-config.php", 184 | ".htaccess" => ".htaccess" 185 | } 186 | ``` 187 | 188 | Modifications and additions are handled similarly to symlinks and 189 | configs, but note the lack of a wrapping `[]` 190 | 191 | ### Stripping out unnecessary files and directories 192 | 193 | You can remove specific files and directories from your releases 194 | at the time of deploy. 195 | 196 | By default the list of things the gem strips out looks like this 197 | 198 | ```ruby 199 | set :copy_exclude, [ 200 | ".git", 201 | "Capfile", 202 | "/config", 203 | "capinfo.json", 204 | ".DS_Store", 205 | ] 206 | ``` 207 | 208 | This excludes the listed files from making it into a release 209 | 210 | **For this you actually need to re-declare the set to add / remove these exclusions.** 211 | 212 | For example, to allow the `.git` directory to exist in the releases, you would 213 | re-declare the option completely. Removing the `.git` entry. 214 | 215 | ```ruby 216 | set :copy_exclude, [ 217 | "Capfile", 218 | "/config", 219 | "capinfo.json", 220 | ".DS_Store", 221 | ] 222 | ``` 223 | 224 | This is usually placed in `config/deploy.rb` but can also be placed at the stage level. 225 | 226 | ### Detecting Local Changes 227 | 228 | This gem by default checks the current release for modifications since 229 | it was deployed. Either you're dealing with clients that like to make 230 | changes in production, or you have plugins that write configs and other 231 | things to the file system. This step protects you against moving changes 232 | that have happened in the target stage out of use. 233 | 234 | When deploying, if it detects a change it will stop the deploy process, and 235 | provide you with a listing of all the files that have been either added, 236 | changed, or deleted. 237 | 238 | At this point you can rectify the changes yourself if you wish, adding them to 239 | your source control, or verifying you don't need them. 240 | 241 | Then you call the deploy like this to force it to create the new release. 242 | 243 | cap cf:localchanges:allow_differences deploy 244 | 245 | This will tell the deploy to ignore any of these changes and proceed. 246 | 247 | If you would like to turn this feature off, you can have it force this by 248 | default with the following option set in either your main `config/deploy.rb` 249 | or your stage specific files. 250 | 251 | ```ruby 252 | set :snapshot_allow_differences, true 253 | ``` 254 | 255 | If you would like to ignore changes to specific files, you can declare an option: 256 | 257 | ```ruby 258 | set :localchanges_excludes, { 259 | :deleted => ['deleted_file_to_ignore'], 260 | :created => ['subdirectory/createdfile'], 261 | :changed => ['changedfile'], 262 | :any => ['ignoredfile'] 263 | } 264 | ``` 265 | 266 | Filenames are relative to the webroot, and are exact; there is no current provision 267 | for globbing or directory tree exclusion. The `:any` list will ignore all changes 268 | to a given file - deletion, creation, or content changes - while the other lists 269 | may be useful for more limited exclusions - for instance, a file that can be deleted 270 | but should never be changed if it remains present. 271 | 272 | ### Enhanced :git capistrano scm module 273 | 274 | capistrano-wp includes a slight enhancement to the `:git` scm 275 | module. The one shipped with Capistrano 2 does not gracefully 276 | handle submodules being removed in the repo; they stick around in 277 | the cached copy. 278 | 279 | The enhancement gives an extra -f to `git clean` to induce it to remove 280 | the submodule detritus, and also runs the clean in every submodule 281 | (with `git submodule foreach`). 282 | 283 | ## Development 284 | 285 | [rubygems]: http://rubygems.org/pages/download 286 | [rvm]: https://rvm.io/ 287 | 288 | gem install bundle 289 | bundle install 290 | rake install 291 | 292 | When updating the gem requirements: 293 | 294 | rake gemspec 295 | 296 | # Copyright 297 | 298 | Copyright (c) 2012-2013 Crowd Favorite, Ltd. Released under the Apache License, version 2.0. See LICENSE.txt for further details. 299 | 300 | -------------------------------------------------------------------------------- /lib/crowdfavorite/tasks/localchanges.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'crowdfavorite/tasks' 16 | 17 | module CrowdFavorite::Tasks::LocalChanges 18 | extend CrowdFavorite::Support::Namespace 19 | 20 | namespace :cf do 21 | def _cset(name, *args, &block) 22 | unless exists?(name) 23 | set(name, *args, &block) 24 | end 25 | end 26 | 27 | _cset(:comparison_target) { current_release rescue File.dirname(release_path) } 28 | _cset(:hash_creation) { 29 | # Find out what hashing mechanism we can use - shasum, sha1sum, openssl, or just an ls command. 30 | # Unfortunate double-call of which handles some systems which output an error message on stdout 31 | # when the program cannot be found. 32 | creation = capture('((which shasum >/dev/null 2>&1 && which shasum) || (which sha1sum >/dev/null 2>&1 && which sha1sum) || (which openssl >/dev/null 2>&1 && which openssl) || echo "ls -ld")').to_s.strip 33 | if creation.match(/openssl$/) 34 | "#{creation} sha1 -r" 35 | end 36 | creation 37 | } 38 | 39 | _cset(:hash_directory) { shared_path } 40 | _cset(:hash_suffix) { "_hash" } 41 | _cset(:hash_compare_suffix) { "compare" } 42 | _cset(:hashes) { capture("ls -xt #{File.join(hash_directory, '*' + hash_suffix)}").split.reverse } 43 | _cset(:localchanges_excludes) { 44 | { 45 | :deleted => [], 46 | :created => [], 47 | :changed => [], 48 | :any => [] 49 | } 50 | } 51 | 52 | def _snapshot_exists(path) 53 | retcode = (capture("test -f " + Shellwords::escape(path) + "; echo $?").to_s.strip.to_i) 54 | return retcode == 0 ? true : false 55 | end 56 | 57 | def _hash_path(release, extra = "") 58 | File.join(hash_directory, release + hash_suffix + extra) 59 | end 60 | 61 | namespace :localchanges do 62 | task :snapshot_deploy, :except => { :no_release => true } do 63 | set(:snapshot_target, latest_release) 64 | snapshot 65 | unset(:snapshot_target) 66 | end 67 | 68 | desc "Snapshot the current release for later change detection." 69 | task :snapshot, :except => { :no_release => true } do 70 | target_release = File.basename(fetch(:snapshot_target, comparison_target)) 71 | 72 | target_path = File.join(releases_path, target_release) 73 | default_hash_path = _hash_path(target_release) # File.join(shared_path, target_release + "_hash") 74 | hash_path = fetch(:snapshot_hash_path, default_hash_path) 75 | 76 | snapshot_exists = _snapshot_exists(hash_path) 77 | 78 | if snapshot_exists and !fetch(:snapshot_force, false) 79 | logger.info "A snapshot for release #{target_release} already exists." 80 | next 81 | end 82 | 83 | run("find " + Shellwords::escape(target_path) + " -name .git -prune -o -name .svn -prune -o -type f -print0 | ( xargs -0 #{hash_creation} 2>&1 || true ) > " + Shellwords::escape(hash_path)) 84 | 85 | end 86 | 87 | desc "Call this before a deploy to continue despite local changes made on the server." 88 | task :allow_differences do 89 | set(:snapshot_allow_differences, true) 90 | end 91 | 92 | task :forbid_differences do 93 | set(:snapshot_allow_differences, false) 94 | end 95 | 96 | def _do_snapshot_compare() 97 | if releases.length == 0 98 | logger.info "no current release" 99 | return false 100 | end 101 | release_name = File.basename(current_release) 102 | set(:snapshot_target, current_release) 103 | default_hash_path = _hash_path(release_name) # File.join(shared_path, release_name + "_hash") 104 | snapshot_exists = _snapshot_exists(default_hash_path) 105 | if !snapshot_exists 106 | logger.info "no previous snapshot to compare against" 107 | return false 108 | end 109 | set(:snapshot_hash_path, _hash_path(release_name, hash_compare_suffix)) # File.join(shared_path, release_name + "_hash_compare")) 110 | set(:snapshot_force, true) 111 | snapshot 112 | 113 | # Hand-tooled file diffs - handles either shasum-style or ls -ld output 114 | # Hashes store filename => {host => hash, host => hash, host => hash} 115 | left = {} 116 | right = {} 117 | changed = {} 118 | 119 | [default_hash_path, snapshot_hash_path].each do |hashpath| 120 | Dir.mktmpdir do |hashdownload| 121 | download(hashpath, File.join(hashdownload, "$CAPISTRANO:HOST$")) 122 | Dir.foreach(hashdownload) do |servername| 123 | if not File.directory?(File.join(hashdownload, servername)) then 124 | File.open(File.join(hashdownload, servername)) do |serverfile| 125 | serverfile.each_line do |line| 126 | line.strip! 127 | parts = line.split(/\s+/) 128 | if hash_creation.match(/ls -ld/) 129 | # -rw-rw-r-- 1 example example 41 Sep 19 14:58 index.php 130 | hash_result = parts.slice!(0, 8) 131 | else 132 | # 198ed94e9f1e5c69e159e8ba6d4420bb9c039715 index.php 133 | hash_result = parts.slice!(0, 1) 134 | end 135 | 136 | bucket = (hashpath == default_hash_path) ? left : right 137 | filename = parts.join('') 138 | bucket[filename] ||= {} 139 | bucket[filename][servername] = hash_result 140 | end 141 | end 142 | end 143 | end 144 | end 145 | end 146 | 147 | if !(left.empty? && right.empty?) 148 | left.each do |filename, servers| 149 | if right.has_key?(filename) 150 | servers.each do |host, hash_result| 151 | right_hash_result = right[filename].delete(host) 152 | if right_hash_result and right_hash_result != hash_result 153 | changed[filename] ||= {} 154 | changed[filename][host] = true 155 | end 156 | left[filename].delete(host) 157 | end 158 | left.delete(filename) if left[filename].empty? 159 | right.delete(filename) if right[filename].empty? 160 | end 161 | end 162 | end 163 | 164 | excludes = fetch(:localchanges_excludes) 165 | excludes[:any] ||= [] 166 | logger.important "Excluding from #{current_release}: #{excludes.inspect}" 167 | excluded = {:left => {}, :right => {}, :changed => {}} 168 | found_exclusion = false 169 | [[left, :deleted], [right, :created], [changed, :changed]].each do |filegroup, excluder| 170 | excludes[excluder] ||= [] 171 | filegroup.each do |filename, servers| 172 | if servers.respond_to? :keys 173 | servers = servers.keys 174 | end 175 | if excludes[excluder].detect {|f| f == filename or File.join(current_release, f) == filename} or 176 | excludes[:any].detect {|f| f == filename or File.join(current_release, f) == filename} 177 | found_exclusion = true 178 | excluded[excluder] ||= {} 179 | excluded[excluder][filename] ||= [] 180 | excluded[excluder][filename].push(*servers) 181 | excluded[excluder][filename].uniq! 182 | filegroup.delete(filename) 183 | end 184 | end 185 | end 186 | 187 | unset(:snapshot_target) 188 | unset(:snapshot_hash_path) 189 | unset(:snapshot_force) 190 | return {:left => left, :right => right, :changed => changed, :excluded => excluded} 191 | end 192 | 193 | def _do_snapshot_diff(results, format = :full) 194 | if !results 195 | return false 196 | end 197 | if results[:left].empty? && results[:right].empty? && results[:changed].empty? 198 | if results.has_key?(:excluded) 199 | logger.important "excluded: " + results[:excluded].inspect 200 | end 201 | return false 202 | end 203 | 204 | if format == :basic || !(fetch(:strategy).class <= Capistrano::Deploy::Strategy.new(:remote).class) 205 | [[:left, 'deleted'], [:right, 'created'], [:changed, 'changed']].each do |resultgroup, verb| 206 | if !results[resultgroup].empty? 207 | logger.important "#{verb}: " 208 | results[resultgroup].each do |thefile, servers| 209 | filename = thefile 210 | if filename.start_with? current_release 211 | filename = thefile.slice(current_release.length..-1) 212 | end 213 | logger.important "#{File.basename filename} in #{File.dirname filename} (on #{servers.keys.inspect})" 214 | end 215 | end 216 | end 217 | 218 | if results.has_key?(:excluded) 219 | logger.info "excluded: " + results[:excluded].inspect 220 | end 221 | return true 222 | end 223 | 224 | ## TODO: improve diff handling for remote_cache with .git copy_excluded 225 | ## TODO: improve diff handling for remote_cache with .git not copy_excluded 226 | ## TODO: improve diff handling for remote_cache with .svn copy_excluded 227 | ## TODO: improve diff handling for remote_cache with .svn not copy_excluded 228 | logger.important "deleted: " + results[:left].inspect 229 | logger.important "created: " + results[:right].inspect 230 | logger.important "changed: " + results[:changed].inspect 231 | if results.has_key? :excluded 232 | logger.important "excluded: " + results[:excluded].inspect 233 | end 234 | return true 235 | end 236 | 237 | desc "Check the current release for changes made on the server; abort if changes are detected." 238 | task :compare, :except => { :no_release => true } do 239 | results = _do_snapshot_compare() 240 | if _do_snapshot_diff(results, :basic) 241 | abort("Aborting: local changes detected in current release") unless fetch(:snapshot_allow_differences, false) 242 | logger.important "Continuing deploy despite differences!" 243 | end 244 | end 245 | 246 | desc "Check the current release for changes made on the server (and return detailed changes, if using a remote-cached git repo). Does not abort on changes." 247 | task :diff, :except => { :no_release => true } do 248 | results = _do_snapshot_compare() 249 | _do_snapshot_diff(results, :full) 250 | end 251 | 252 | task :cleanup, :except => { :no_release => true } do 253 | count = fetch(:keep_releases, 5).to_i 254 | if count >= hashes.length 255 | logger.info "no old hashes to clean up" 256 | else 257 | logger.info "keeping #{count} of #{hashes.length} release hashes" 258 | hashpaths = (hashes - hashes.last(count)).map{ |thehash| 259 | File.join(thehash) + " " + File.join(thehash + hash_compare_suffix) 260 | }.join(" ") 261 | try_sudo "rm -f #{hashpaths}" 262 | end 263 | end 264 | end 265 | end 266 | end 267 | 268 | -------------------------------------------------------------------------------- /lib/crowdfavorite/tasks/wordpress.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Crowd Favorite, Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'crowdfavorite/tasks' 16 | require 'shellwords' 17 | 18 | module CrowdFavorite::Tasks::WordPress 19 | extend CrowdFavorite::Support::Namespace 20 | def _combine_filehash base, update 21 | if update.respond_to? :has_key? 22 | update = [update] 23 | end 24 | new = {} 25 | new.merge!(base) 26 | update.each do |update_hash| 27 | new.merge!(update_hash) 28 | end 29 | new 30 | end 31 | namespace :cf do 32 | def _cset(name, *args, &block) 33 | unless exists?(name) 34 | set(name, *args, &block) 35 | end 36 | end 37 | 38 | _cset :copy_exclude, [ 39 | ".git", ".gitignore", ".gitmodules", 40 | ".DS_Store", ".svn", 41 | "Capfile", "/config", 42 | "capinfo.json", 43 | ] 44 | 45 | # wp_symlinks are symlinked from the shared directory into the release. 46 | # Key is the shared file; value is the target location. 47 | # :wp_symlinks overwrites :base_wp_symlinks; an empty target location 48 | # means to skip linking the file. 49 | _cset :base_wp_symlinks, { 50 | "cache" => "wp-content/cache", 51 | "uploads" => "wp-content/uploads", 52 | "blogs.dir" => "wp-content/blogs.dir", 53 | } 54 | 55 | _cset :wp_symlinks, {} 56 | 57 | # wp_configs are copied from the shared directory into the release. 58 | # Key is the shared file; value is the target location. 59 | # :wp_configs overwrites :base_wp_configs; an empty target location 60 | # means to skip copying the file. 61 | _cset :base_wp_configs, { 62 | "db-config.php" => "wp-content/", 63 | "advanced-cache.php" => "wp-content/", 64 | "object-cache.php" => "wp-content/", 65 | "*.html" => "/", 66 | } 67 | 68 | _cset :wp_configs, {} 69 | 70 | # stage_specific_overrides are uploaded from the repo's config 71 | # directory, if they exist for that stage. Files are named 72 | # 'STAGE-filename.txt' locally and copied to 'filename.txt' 73 | # on deploy for that stage. 74 | # Key is the local file; value is the target location. 75 | # :stage_specific_overrides overwrites :base_stage_specific_overrides; 76 | # an empty target location means to skip uploading the file. 77 | _cset :base_stage_specific_overrides, { 78 | "local-config.php" => "local-config.php", 79 | ".htaccess" => ".htaccess" 80 | } 81 | 82 | _cset :stage_specific_overrides, {} 83 | 84 | before "deploy:finalize_update", "cf:wordpress:generate_config" 85 | after "deploy:finalize_update", "cf:wordpress:touch_release" 86 | after "cf:wordpress:generate_config", "cf:wordpress:link_symlinks" 87 | after "cf:wordpress:link_symlinks", "cf:wordpress:copy_configs" 88 | after "cf:wordpress:copy_configs", "cf:wordpress:install" 89 | after "cf:wordpress:install", "cf:wordpress:do_stage_specific_overrides" 90 | namespace :wordpress do 91 | 92 | namespace :install do 93 | 94 | desc <<-DESC 95 | [internal] Installs WordPress with a remote svn cache 96 | DESC 97 | task :with_remote_cache, :except => { :no_release => true } do 98 | wp = fetch(:wordpress_version, "trunk") 99 | wp_target = fetch(:wp_path, release_path) 100 | wp_stage = File.join(shared_path, "wordpress", wp) 101 | # check out cache of wordpress code 102 | run Shellwords::shelljoin(["test", "-e", wp_stage]) + 103 | " || " + Shellwords::shelljoin(["svn", "co", "-q", "http://core.svn.wordpress.org/" + wp, wp_stage]) 104 | # update branches or trunk (no need to update tags) 105 | run Shellwords::shelljoin(["svn", "up", "--force", "-q", wp_stage]) unless wp.start_with?("tags/") 106 | # ensure a clean copy 107 | run Shellwords::shelljoin(["svn", "revert", "-R", "-q", wp_stage]) 108 | # trailingslashit for rsync 109 | wp_stage << '/' unless wp_stage[-1..-1] == '/' 110 | # push wordpress into the right place (release_path by default, could be #{release_path}/wp) 111 | run Shellwords::shelljoin(["rsync", "--exclude=.svn", "--ignore-existing", "-a", wp_stage, wp_target]) 112 | end 113 | 114 | desc <<-DESC 115 | [internal] Installs WordPress with a local svn cache/copy, compressing and uploading a snapshot 116 | DESC 117 | task :with_copy, :except => { :no_release => true } do 118 | wp = fetch(:wordpress_version, "trunk") 119 | wp_target = fetch(:wp_path, release_path) 120 | Dir.mktmpdir do |tmp_dir| 121 | tmpdir = fetch(:cf_database_store, tmp_dir) 122 | wp = fetch(:wordpress_version, "trunk") 123 | Dir.chdir(tmpdir) do 124 | if !(wp.start_with?("tags/") || wp.start_with?("branches/") || wp == "trunk") 125 | wp = "branches/#{wp}" 126 | end 127 | wp_stage = File.join(tmpdir, "wordpress", wp) 128 | ["branches", "tags"].each do |wpsvntype| 129 | system Shellwords::shelljoin(["mkdir", "-p", File.join(tmpdir, "wordpress", wpsvntype)]) 130 | end 131 | 132 | puts "Getting WordPress #{wp} to #{wp_stage}" 133 | system Shellwords::shelljoin(["test", "-e", wp_stage]) + 134 | " || " + Shellwords::shelljoin(["svn", "co", "-q", "http://core.svn.wordpress.org/" + wp, wp_stage]) 135 | system Shellwords::shelljoin(["svn", "up", "--force", "-q", wp_stage]) unless wp.start_with?("tags/") 136 | system Shellwords::shelljoin(["svn", "revert", "-R", "-q", wp_stage]) 137 | wp_stage << '/' unless wp_stage[-1..-1] == '/' 138 | Dir.mktmpdir do |copy_dir| 139 | comp = Struct.new(:extension, :compress_command, :decompress_command) 140 | remote_tar = fetch(:copy_remote_tar, 'tar') 141 | local_tar = fetch(:copy_local_tar, 'tar') 142 | type = fetch(:copy_compression, :gzip) 143 | compress = case type 144 | when :gzip, :gz then comp.new("tar.gz", [local_tar, '-c -z --exclude .svn -f'], [remote_tar, '-x -k -z -f']) 145 | when :bzip2, :bz2 then comp.new("tar.bz2", [local_tar, '-c -j --exclude .svn -f'], [remote_tar, '-x -k -j -f']) 146 | when :zip then comp.new("zip", %w(zip -qyr), %w(unzip -q)) 147 | else raise ArgumentError, "invalid compression type #{type.inspect}" 148 | end 149 | compressed_filename = "wp-" + File.basename(fetch(:release_path)) + "." + compress.extension 150 | local_file = File.join(copy_dir, compressed_filename) 151 | puts "Compressing #{wp_stage} to #{local_file}" 152 | Dir.chdir(wp_stage) do 153 | system([compress.compress_command, local_file, '.'].join(' ')) 154 | end 155 | remote_file = File.join(fetch(:copy_remote_dir, '/tmp'), File.basename(local_file)) 156 | puts "Pushing #{local_file} to #{remote_file} to deploy" 157 | upload(local_file, remote_file) 158 | wp_target = fetch(:wp_path, fetch(:release_path)) 159 | run("mkdir -p #{wp_target} && cd #{wp_target} && (#{compress.decompress_command.join(' ')} #{remote_file} || echo 'tar errors for normal conditions') && rm #{remote_file}") 160 | end 161 | 162 | end 163 | end 164 | end 165 | 166 | desc <<-DESC 167 | [internal] Installs WordPress to the application deploy point 168 | DESC 169 | task :default, :except => { :no_release => true } do 170 | wp = fetch(:wordpress_version, false) 171 | if wp.nil? or wp == false or wp.empty? 172 | logger.info "Not installing WordPress" 173 | elsif fetch(:strategy).class <= Capistrano::Deploy::Strategy.new(:remote).class 174 | with_remote_cache 175 | else 176 | with_copy 177 | end 178 | end 179 | end 180 | 181 | desc <<-DESC 182 | [internal] (currently unused) Generate config files if appropriate 183 | DESC 184 | task :generate_config, :except => { :no_release => true } do 185 | # live config lives in wp-config.php; dev config loaded with local-config.php 186 | # this method does nothing for now 187 | end 188 | 189 | desc <<-DESC 190 | [internal] Symlinks specified files (usually uploads/blogs.dir/cache directories) 191 | DESC 192 | task :link_symlinks, :except => { :no_release => true } do 193 | symlinks = _combine_filehash(fetch(:base_wp_symlinks), fetch(:wp_symlinks)) 194 | symlinks.each do |src, targ| 195 | next if targ.nil? || targ == false || targ.empty? 196 | src = File.join(shared_path, src) unless src.include?(shared_path) 197 | targ = File.join(release_path, targ) unless targ.include?(release_path) 198 | run [ 199 | Shellwords::shelljoin(["test", "-e", src]), 200 | Shellwords::shelljoin(["test", "-d", targ]), 201 | Shellwords::shelljoin(["rm", "-rf", targ]) 202 | ].join(' && ') + " || true" 203 | run Shellwords::shelljoin(["test", "-e", src]) + " && " + Shellwords::shelljoin(["ln", "-nsf", src, targ]) + " || true" 204 | end 205 | end 206 | 207 | desc <<-DESC 208 | [internal] Copies specified files (usually advanced-cache, object-cache, db-config) 209 | DESC 210 | task :copy_configs, :except => { :no_release => true } do 211 | configs = _combine_filehash(fetch(:base_wp_configs), fetch(:wp_configs)) 212 | configs.each do |src, targ| 213 | next if targ.nil? || targ == false || targ.empty? 214 | src = File.join(shared_path, src) unless src.include?(shared_path) 215 | targ = File.join(release_path, targ) unless targ.include?(release_path) 216 | run "ls -d #{src} >/dev/null 2>&1 && cp -urp #{src} #{targ} || true" 217 | #run Shellwords::shelljoin(["test", "-e", src]) + " && " + Shellwords::shelljoin(["cp", "-rp", src, targ]) + " || true" 218 | end 219 | end 220 | 221 | desc <<-DESC 222 | [internal] Pushes up local-config.php, .htaccess, others if they exist for that stage 223 | DESC 224 | task :do_stage_specific_overrides, :except => { :no_release => true } do 225 | next unless fetch(:stage, false) 226 | overrides = _combine_filehash(fetch(:base_stage_specific_overrides), fetch(:stage_specific_overrides)) 227 | overrides.each do |src, targ| 228 | next if targ.nil? || targ == false || targ.empty? 229 | src = File.join("config", "#{stage}-#{src}") 230 | targ = File.join(release_path, targ) unless targ.include?(release_path) 231 | if File.exist?(src) 232 | upload(src, targ) 233 | end 234 | end 235 | end 236 | 237 | desc <<-DESC 238 | [internal] Ensure the release path has an updated modified time for deploy:cleanup 239 | DESC 240 | task :touch_release, :except => { :no_release => true } do 241 | run "touch '#{release_path}'" 242 | end 243 | end 244 | 245 | #=========================================================================== 246 | # util / debugging code 247 | 248 | namespace :debugging do 249 | 250 | namespace :release_info do 251 | desc <<-DESC 252 | [internal] Debugging info about releases. 253 | DESC 254 | 255 | task :default do 256 | %w{releases_path shared_path current_path release_path releases previous_release current_revision latest_revision previous_revision latest_release}.each do |var| 257 | puts "#{var}: #{eval(var)}" 258 | end 259 | end 260 | end 261 | end 262 | end 263 | end 264 | 265 | --------------------------------------------------------------------------------