diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a65738a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# No need to share Customfile +/Customfile + +# No need to share custom config files +/config/config.rb +/config/production.rb +/config/staging.rb +/config/local.rb + +# No need to share custom libraries +/lib/custom-hooks.rb +/lib/custom-tasks.rb + +# Ignore contents of /custom folder, except for readme file +/custom/* +!/custom/README.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..751b424 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "wordpress-mu-plugins/stage-wp-plugin-manager"] + path = wordpress-mu-plugins/stage-wp-plugin-manager + url = git@github.com:andrezrv/stage-wp-plugin-manager.git diff --git a/Capfile b/Capfile index f3761f7..78ca1ab 100644 --- a/Capfile +++ b/Capfile @@ -1,12 +1,35 @@ -require 'rubygems' -require 'railsless-deploy' -load 'lib/misc' - -# Multistage -set :stages, ['production', 'staging'] -set :default_stage, 'production' -require 'capistrano/ext/multistage' - -load 'lib/tasks' -load 'lib/deploy' # Loads config/config.rb after -load 'lib/deploy-after' +# Capfile +# +# This is the main file of the package and the first to be executed. +# +# Capistrano reads its instructions from here. In ordinary cases, the Capfile is +# where you will tell Capistrano about the servers you want to connect to and +# the tasks you want to perform on those servers. Here we do somethig different: +# we're just gonna use this file to set up the most basic values and load our +# required libraries, and will configure the rest of our deployment application +# via config/config.rb and some hook files that we're gonna call here and there. + +# Load required modules. +require "rubygems" +require "railsless-deploy" + +# Set up multistage +set :stages, ["production", "staging", "local"] +set :default_stage, "production" + +# Avoid tty fatal error +default_run_options[:pty] = true + +# Load Stage WP libraries +load "lib/misc" +load "lib/tasks" +load "lib/deploy" # Loads config/config.rb after +load "lib/deploy-after" + +# Load a Customfile if we have one +if File.exists?("Customfile") then + loadFile "Customfile" +end + +# Delay loading of multistage module, so we can override :stages if needed +require "capistrano/ext/multistage" diff --git a/Customfile-sample b/Customfile-sample new file mode 100644 index 0000000..8a2312b --- /dev/null +++ b/Customfile-sample @@ -0,0 +1,28 @@ +# Customfile-sample +# +# This is a sample Customfile. It is meant to be loaded at the end of the +# Capfile. In case you need it, copy it as "Customfile" and customize it as it +# suits you. + +# Overridable settings. + +# // Stages that you're gonna be using in your workflow. +set :stages, ["production", "staging", "local"] + +# // Default stage +set :default_stage, "production" + +# // The user that will log to your remote systems. +set :user, "deploy" + +# // Use sudo for Capistrano built-in tasks. +set :use_sudo, false + +# // Deploy method. It can be :remote_cache or :copy. +set :deploy_via, :remote_cache + +# // Filename patters to exclude on your deploys. +set :copy_exclude, [".git", ".gitmodules", ".DS_Store", ".gitignore"] + +# // Number of releases to keep on your remote servers. +set :keep_releases, 5 diff --git a/README.md b/README.md index 3e45078..ce64e0e 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,165 @@ -# WP Stack -A toolkit for creating professional [WordPress][wp] deployments. +# Stage WP -Commissioned by [Knewton](http://www.knewton.com/). +A toolkit for creating professional [WordPress][wp] deployments, forked from Mark Jaquith's [WP-Stack][wpstack]. It uses Capistrano as a code deployment system, and offers a complete set of tasks for you to scale your WordPress project to different servers, both from local and remote environments. -[wp]: http://wordpress.org/ +**Current stable version**: [1.1.1](https://github.com/andrezrv/stage-wp/tree/1.1.1) + +## Why do you need this? + +Because WordPress runs professional sites, and you should have a professional deployment tool to go along with it. Stage WP is a toolkit that helps you manage that. -## Why -WordPress runs professional sites. You should have a professional deployment to go along with it. You should be using: +## Getting Started -* Version control (like Git) -* A code deployment system (like Capistrano) -* A staging environment to test changes before they go live -* CDN for static assets +### Assumptions about your workflow + +* You mantain your code under a version control system (like [Git](http://git-scm.com/)). Stage WP works specially well with [WordPress Barebones][wpbarebones]. +* You develop locally before deploying your code. +* Ideally, you have a staging environment to test changes before they go to production. +* Ideally, you use a CDN for static assets. +* You deploy your code from a local environment. Additionally, you should be able to easily scale out to multiple web servers, if needed. -WP Stack is a toolkit that helps you do all that. +### Capistrano -## WordPress Must-use Plugins +Capistrano is a code deployment tool. When you have code that is ready to go "live", this is what we you're gonna use to do it. -"Must-use" plugins aka `mu-plugins` are WordPress plugins that are dropped into the `{WordPress content dir}/mu-plugins/` directory. They are autoloaded — no need to activate them. WP Stack comes with a number of these plugins for your use: +### Setting Up Your Local System -### CDN +1. Create a `deploy` user on each of your remote web servers (staging and production). This will be the user that is going to perform all the deployment actions on your remote systems. + * `addgroup deploy` + * `adduser --system --shell /bin/bash --ingroup deploy --disabled-password --home /home/deploy deploy` + If you want a user with another name, it must be the same as the one you set for `:user` within `/config/config.rb`. +2. Make sure that `deploy` can pull down your site repo code with `git clone $your_repo` from all your servers. +3. Create (if you don't have one) and upload an SSH key to the list of authorized keys for the `deploy` user in each of your web servers. + * `ssh-keygen` + * `cat ~/.ssh/id_rsa.pub | ssh deploy@$your_server "cat - >> ~/.ssh/authorized_keys"` +3. [Install Ruby][ruby]. + * `sudo apt-get install ruby` +4. Install Capistrano and extras. + * `sudo gem install capistrano -v 2.15.5` + * `sudo gem install capistrano-ext railsless-deploy` -`wp-stack-cdn.php` +If you're using [VVV][vvv], steps 3 and 4 will run automatically upon `vagrant up --provision`. -This is a very simple CDN plugin. Simply configure the constant `WP_STACK_CDN_DOMAIN` in your `wp-config.php` or hook in and override the `wp_stack_cdn_domain` option. Provide a domain name only, like `static.example.com`. The plugin will look for static file URLs on your domain and repoint them to the CDN domain. +### Setting Up Stage WP -### Multisite Uploads +1. Check out Stage WP somewhere on your local system. + * `git clone git@github.com:andrezrv/Stage-WP.git /srv/www/website/stage-wp` +2. Customize and rename `/config/{config|local|production|staging}-sample.rb` +3. Make sure your remote `:production_deploy_to` and `:staging_deploy_to` paths exist and are owned by the deploy user. + * `chown -R deploy:deploy /path/to/your/deployment` +4. Go to your Stage WP directory and run `cap deploy:setup` to setup the initial `shared` and `releases` directories. -`wp-stack-ms-uploads.php` +### Deploying -The way WordPress Multisite serves uploads is not ideal. It streams them through a PHP file. Professional sites should not do this. This plugin allows one nginx rewrite rule to handle all uploads, eliminating the need for PHP streaming. It uses the following URL scheme for uploads: `{scheme}://{domain}/wp-files/{blog_id}/`. By inserting the `$blog_id`, one rewrite rule can make sure file requests go to the correct blog. +1. `cd` to your Stage WP directory. +2. Run `cap production deploy` (to deploy to staging, use `cap staging deploy`). -**Note:** You will need to implement this Nginx rewrite rule for this to work: +### Rolling Back -`rewrite ^/wp-files/([0-9]+)/(.*)$ /wp-content/blogs.dir/$1/files/$2;` +If something went wrong with your deployment, you can always go back to the previous version: -### Manual DB Upgrades +1. `cd` to your Stage WP directory. +2. Run `cap deploy:rollback` -Normally, WordPress redirects `/wp-admin/` requests to the WordPress database upgrade screen. On large sites, or sites with a lot of active authors, this may not be desired. This drop-in prevents the automatic redirect and instead lets you manually go to `/wp-admin/upgrade.php` to upgrade a site. +### About Stages -## Capistrano +There are three "stages": local, production and staging. The first one is your development environment. The other two should be remote systems, and can be completely different servers, or different paths on the same set of servers. -Capistrano is a code deployment tool. When you have code that is ready to go "live", this is what does it. +Stage WP, unlike WP-Stack, is meant to be used from a development environment, performing actions on remote servers from a local system. -### Setup +You can still use it in a remote system, like your production or staging servers, but this comes with a trick: if Stage WP is running on staging or production, your local stage will be the same as your remote one. -1. Create a `deploy` user on your system (Ubuntu: `addgroup deploy; adduser --system --shell /bin/bash --ingroup deploy --disabled-password --home /home/deploy deploy -`). -2. Create an SSH key for `deploy`, make sure it can SSH to all of your web servers, and make sure it can pull down your site repo code. - * Switch to the deploy user (`su deploy`). - * `ssh-keygen` - * `cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys` - * Add the contents of `~/.ssh/id_rsa.pub` to `~/.ssh/authorized_keys` on every server you're deploying to. -3. [Install RubyGems][rubygems]. -4. Install Capistrano and friends: `sudo gem install capistrano capistrano-ext railsless-deploy` -5. Switch to the deploy user (`su deploy`) and check out WP Stack somewhere on your server: `git clone git@github.com:markjaquith/WP-Stack.git ~/deploy` -6. Customize and rename `config/SAMPLE.{config|production|staging}.rb` -7. Make sure your `:deploy_to` path exists and is owned by the deploy user: `chown -R deploy:deploy /path/to/your/deployment` -8. Run `cap deploy:setup` (from your WP Stack directory) to setup the initial `shared` and `releases` directories. +### Database Tasks -[rubygems]: http://rubygems.org/pages/download +With Stage WP, you can create your database, backup, restore and move mysqldumps from the local stage to production and staging with the help of the following commands: -### Deploying +* `cap {staging|production} db:init`: Initializes the WordPress database in a remote stage. +* `cap {local|staging} db:sync`: Syncs the local or staging database and uploads from production. +* `cap {local|staging|production} db:backup`: Performs a mysqldump of your WordPress database and saves it within the selected stage. +* `cap {local|staging|production} db:restore`: Restores the WordPress database from a backup stored in the selected stage. +* `cap {staging|production} db:pull`: Downloads a database backup from the selected stage and stores it locally. +* `cap {staging|production} db:push`: Uploads a local database backup to the selected stage. -1. Switch to the deploy user: `su deploy` -2. `cd` to the WP Stack directory. -3. Run `cap production deploy` (to deploy to staging, use `cap staging deploy`) +### Managing Shared Files -### Rolling Back +You can synchronize your shared files both from local to remote stages and from remote stages to local with the help of the following commands: -1. Switch to the deploy user: `su deploy` -2. `cd` to the WP Stack directory. -3. Run `cap deploy:rollback` +* `cap {staging|production} shared:pull`: Synchronizes your local shared files from the selected stage. +* `cap {staging|production} shared:push`: Synchronizes your selected stage shared files from local. -### About Stages +### Full Synchronization + +You can synchronize both database and shared files from production to staging or local using `cap {local|staging} db:sync`. + +### Some Other Useful Tasks + +If you're using [WordPress Barebones][wpbarebones], you can perform remote backup and maintenance tasks with the following commands: + +* `cap {staging|production} util:backup_application`: Zips and saves all application files to `:application_backup_path`. +* `cap {staging|production} util:backup_db`: Performs a mysqldump of the WordPress database and saves it to `:wpdb[stage][:backups_dir]`. +* `cap {staging|production} util:switch`: Switches the state of your site from active to maintenance and vice versa. +* `cap {staging|production} util:full_backup`: Performs a full backup (files and database). -There are two "stages": production and staging. These can be completely different servers, or different paths on the same set of servers. +Yet more useful tasks: -To sync from production to staging (DB and files), run `cap staging db:sync`. +* `cap {local|staging|production} nginx:restart`: Restarts NGINX. +* `cap {local|staging|production} phpfpm:restart`: Restarts PHP-FPM. +* `cap {local|staging|production} memcached:restart`: Restarts Memcached. +* `cap {local|staging|production} memcached:upload`: Updates the pool of Memcached servers. -## Assumptions made about WordPress +## WordPress Must Use Plugins -If you're not using [WordPress Skeleton](https://github.com/markjaquith/WordPress-Skeleton), you should be aware of these assumptions: +Must Use plugins are WordPress plugins that usually live into the `mu-plugins` directory. They are autoloaded — no need to activate them. Stage WP comes with a number of these plugins for your use: -1. Your `wp-config.php` file exists in your web root. So put it there. -2. WP Stack replaces the following "stubs": +### Stage WP CDN + +This is a very simple CDN plugin. Simply configure the constant `WP_STACK_CDN_DOMAIN` in your `wp-config.php` or hook in and override the `wp_stack_cdn_domain` option. Provide a domain name only, like `static.example.com`. The plugin will look for static file URLs on your domain and repoint them to the CDN domain. Original credits go to [Mark Jaquith][mj]. + +### Stage WP Multisite Uploads + +The way WordPress Multisite serves uploads is not ideal. It streams them through a PHP file. Professional sites should not do this. This plugin allows one NGINX rewrite rule to handle all uploads, eliminating the need for PHP streaming. It uses the following URL scheme for uploads: `{scheme}://{domain}/wp-files/{blog_id}/`. By inserting the `$blog_id`, one rewrite rule can make sure file requests go to the correct blog. Original credits go to [Mark Jaquith][mj]. + +**Note:** You will need to implement this NGINX rewrite rule for this to work: + +`rewrite ^/wp-files/([0-9]+)/(.*)$ /wp-content/blogs.dir/$1/files/$2;` + +### Stage WP Manual DB Upgrades + +Normally, WordPress redirects `/wp-admin/` requests to the WordPress database upgrade screen. On large sites, or sites with a lot of active authors, this may not be desired. This drop-in prevents the automatic redirect and instead lets you manually go to `/wp-admin/upgrade.php` to upgrade a site. + +## Assumptions Made About WordPress + +If you're not using [WordPress Barebones][wpbarebones], you should be aware of these assumptions: + +1. Your application path is defined in `:application_path`, inside `/config/config.rb` +2. Your `wp-config.php` file must exist in the root of your application path. +3. Stage WP replaces the following "stubs": * `%%DB_NAME%%` — Database name. * `%%DB_HOST%%` — Database host. * `%%DB_USER%%` — Database username. * `%%DB_PASSWORD%%` — Database password. * `%%WP_STAGE%%` – will be `production` or `staging` after deploy. -3. WP Stack uses the constants `WP_STAGE` (which should be set to `'%%WP_STAGE%%'`) and `STAGING_DOMAIN`, which should be set to the domain you want to use for staging (something like `staging.example.com`). \ No newline at end of file +4. Stage WP uses the constants `WP_STAGE` (which should be set to `'%%WP_STAGE%%'`) and `STAGING_DOMAIN`, which should be set to the domain you want to use for staging (something like `staging.example.com`). + +## Extending + +You have the following options to add your own functionlity without altering the flow of the repository: + +* Add your custom configurations to `/config/config.rb`, `/config/local-config.rb`, `/config/production-config.rb` and `/config/staging-config.rb`, since they won't be tracked by Git. +* Rename `/Customfile-sample` to `/Customfile`, if you want to hook `/Capfile`. +* Rename `/lib/custom-tasks-sample.rb` to `/lib/custom-tasks.rb`, if you want to add extra tasks. +* Rename `/lib/custom-hooks-sample.rb` to `/lib/custom-hooks.rb`, if you want to add extra hooks. +* Put anything you want into `/custom`. Git won't track those files. + +### Contributing +If you feel like you want to help this project by adding something you think useful, you can make your pull request against the master branch :) + +[wp]: http://wordpress.org/ +[wpstack]: http://github.com/markjaquith/WP-Stack +[wpbarebones]: http://github.com/andrezrv/wordpress-barebones/ +[mj]: http://github.com/markjaquith +[cap]: https://github.com/capistrano/capistrano +[ruby]: https://www.ruby-lang.org/en/installation/#ruby-install +[vvv]: https://github.com/Varying-Vagrant-Vagrants/VVV diff --git a/WordPress-Dropins/wp-stack-cdn.php b/WordPress-Dropins/wp-stack-cdn.php deleted file mode 100644 index 4099276..0000000 --- a/WordPress-Dropins/wp-stack-cdn.php +++ /dev/null @@ -1,73 +0,0 @@ -sanitize_method($h);$b=func_get_args();unset($b[0]);foreach((array)$b as $a){if(is_int($a))$p=$a;else $m=$a;}return add_action($h,array($this,$m),$p,999);}private function sanitize_method($m){return str_replace(array('.','-'),array('_DOT_','_DASH_'),$m);}}} - -// The plugin -class WP_Stack_CDN_Plugin extends WP_Stack_Plugin { - public static $instance; - public $site_domain; - public $cdn_domain; - public $upload_dir; - public $uploads_only; - public $extensions; - - public function __construct() { - self::$instance = $this; - $this->hook( 'plugins_loaded' ); - } - - public function plugins_loaded() { - $domain_set_up = get_option( 'wp_stack_cdn_domain' ) || ( defined( 'WP_STACK_CDN_DOMAIN' ) && WP_STACK_CDN_DOMAIN ); - $production = defined( 'WP_STAGE' ) && WP_STAGE === 'production'; - $staging = defined( 'WP_STAGE' ) && WP_STAGE === 'staging'; - $uploads_only = defined( 'WP_STACK_CDN_UPLOADS_ONLY' ) && WP_STACK_CDN_UPLOADS_ONLY; - if ( $domain_set_up && !$staging && ( $production || $uploads_only ) ) - $this->hook( 'init' ); - } - - public function init() { - $this->uploads_only = apply_filters( 'wp_stack_cdn_uploads_only', defined( 'WP_STACK_CDN_UPLOADS_ONLY' ) ? WP_STACK_CDN_UPLOADS_ONLY : false ); - $this->extensions = apply_filters( 'wp_stack_cdn_extensions', array( 'jpe?g', 'gif', 'png', 'css', 'bmp', 'js', 'ico' ) ); - if ( !is_admin() ) { - $this->hook( 'template_redirect' ); - if ( $this->uploads_only ) - $this->hook( 'wp_stack_cdn_content', 'filter_uploads_only' ); - else - $this->hook( 'wp_stack_cdn_content', 'filter' ); - $this->site_domain = parse_url( get_bloginfo( 'url' ), PHP_URL_HOST ); - $this->cdn_domain = defined( 'WP_STACK_CDN_DOMAIN' ) ? WP_STACK_CDN_DOMAIN : get_option( 'wp_stack_cdn_domain' ); - } - } - - public function filter_uploads_only( $content ) { - $upload_dir = wp_upload_dir(); - $upload_dir = $upload_dir['baseurl']; - $domain = preg_quote( parse_url( $upload_dir, PHP_URL_HOST ), '#' ); - $path = parse_url( $upload_dir, PHP_URL_PATH ); - $preg_path = preg_quote( $path, '#' ); - - // Targeted replace just on uploads URLs - return preg_replace( "#=([\"'])(https?://{$domain})?$preg_path/((?:(?!\\1]).)+)\.(" . implode( '|', $this->extensions ) . ")(\?((?:(?!\\1).)+))?\\1#", '=$1http://' . $this->cdn_domain . $path . '/$3.$4$5$1', $content ); - } - - public function filter( $content ) { - return preg_replace( "#=([\"'])(https?://{$this->site_domain})?/([^/](?:(?!\\1).)+)\.(" . implode( '|', $this->extensions ) . ")(\?((?:(?!\\1).)+))?\\1#", '=$1http://' . $this->cdn_domain . '/$3.$4$5$1', $content ); - } - - public function template_redirect() { - ob_start( array( $this, 'ob' ) ); - } - - public function ob( $contents ) { - return apply_filters( 'wp_stack_cdn_content', $contents, $this ); - } -} - -new WP_Stack_CDN_Plugin; diff --git a/WordPress-Dropins/wp-stack-manual-db-upgrades.php b/WordPress-Dropins/wp-stack-manual-db-upgrades.php deleted file mode 100644 index 9d85a8b..0000000 --- a/WordPress-Dropins/wp-stack-manual-db-upgrades.php +++ /dev/null @@ -1,34 +0,0 @@ -sanitize_method($h);$b=func_get_args();unset($b[0]);foreach((array)$b as $a){if(is_int($a))$p=$a;else $m=$a;}return add_action($h,array($this,$m),$p,999);}private function sanitize_method($m){return str_replace(array('.','-'),array('_DOT_','_DASH_'),$m);}}} - -// The plugin -class WP_Stack_Manual_DB_Upgrades_Plugin extends WP_Stack_Plugin { - public static $instance; - - public function __construct() { - self::$instance = $this; - $this->hook( 'plugins_loaded' ); - } - - public function plugins_loaded() { - $this->hook( 'option_db_version' ); - } - - public function option_db_version( $version ) { - if ( strpos( $_SERVER['REQUEST_URI'], '/wp-admin/upgrade.php' ) === false ) - return $GLOBALS['wp_db_version']; - else - return $version; - } - -} - -new WP_Stack_Manual_DB_Upgrades_Plugin; diff --git a/WordPress-Dropins/wp-stack-ms-uploads.php b/WordPress-Dropins/wp-stack-ms-uploads.php deleted file mode 100644 index 6b6b753..0000000 --- a/WordPress-Dropins/wp-stack-ms-uploads.php +++ /dev/null @@ -1,58 +0,0 @@ -sanitize_method($h);$b=func_get_args();unset($b[0]);foreach((array)$b as $a){if(is_int($a))$p=$a;else $m=$a;}return add_action($h,array($this,$m),$p,999);}private function sanitize_method($m){return str_replace(array('.','-'),array('_DOT_','_DASH_'),$m);}}} - -// The plugin -class WP_Stack_MS_Uploads_Plugin extends WP_Stack_Plugin { - public static $instance; - - public function __construct() { - self::$instance = $this; - if ( is_multisite() ) - $this->hook( 'init' ); - } - - public function init() { - global $blog_id; - if ( $blog_id != 1 ) { - $this->hook( 'option_fileupload_url' ); - $this->hook( 'upload_dir' ); - } - } - - public function upload_dir( $upload ) { - /* - array( - 'subdir' => '/2012/07', - 'basedir' => '/Users/mark/Sites/wp.git/wp-content/uploads', - 'path' => '/Users/mark/Sites/wp.git/wp-content/uploads/2012/07', - 'baseurl' => 'http://wp.git/wp-files/1', - 'url' => 'http://wp.git/wp-content/uploads/2012/07', - 'error' => false - ) - */ - global $blog_id; - $parsed = parse_url( $upload['baseurl'] ); - $upload['baseurl'] = $parsed['scheme'] . '://' . $parsed['host'] . '/wp-files/' . $blog_id; - $upload['url'] = $upload['baseurl'] . $upload['subdir']; - var_export( $upload ); die(); - return $upload; - } - - // Does core even use this anymore? - public function option_fileupload_url( $url ) { - global $blog_id; - $parsed = parse_url( $url ); - $url = $parsed['scheme'] . '://' . $parsed['host'] . '/wp-files/' . $blog_id . '/'; - return $url; - } -} - -new WP_Stack_MS_Uploads_Plugin; diff --git a/WordPress-Dropins/wp-stack-staging.php b/WordPress-Dropins/wp-stack-staging.php deleted file mode 100644 index 3cdc740..0000000 --- a/WordPress-Dropins/wp-stack-staging.php +++ /dev/null @@ -1,32 +0,0 @@ -sanitize_method($h);$b=func_get_args();unset($b[0]);foreach((array)$b as $a){if(is_int($a))$p=$a;else $m=$a;}return add_action($h,array($this,$m),$p,999);}private function sanitize_method($m){return str_replace(array('.','-'),array('_DOT_','_DASH_'),$m);}}} - -// The plugin -class WP_Stack_Staging_Plugin extends WP_Stack_Plugin { - public static $instance; - - public function __construct() { - self::$instance = $this; - if ( !defined( 'WP_STAGE' ) || WP_STAGE !== 'staging' || !defined( 'STAGING_DOMAIN' ) ) - return; - $this->hook( 'option_home', 'replace_domain' ); - $this->hook( 'option_siteurl', 'replace_domain' ); - } - - public function replace_domain ( $url ) { - $current_domain = parse_url( $url, PHP_URL_HOST ); - $url = str_replace( '//' . $current_domain, '//' . STAGING_DOMAIN, $url ); - return $url; - } -} - -new WP_Stack_Staging_Plugin; - diff --git a/config/.gitignore b/config/.gitignore deleted file mode 100644 index 75c107e..0000000 --- a/config/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/config.rb -/production.rb -/staging.rb diff --git a/config/SAMPLE.config.rb b/config/SAMPLE.config.rb deleted file mode 100644 index 6bf9e49..0000000 --- a/config/SAMPLE.config.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Customize this file, and then rename it to config.rb - -set :application, "WP Stack Site" -set :repository, "set your git repository location here" -set :scm, :git -# Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none` - -# Using Git Submodules? -set :git_enable_submodules, 1 - -# This should be the same as :deploy_to in production.rb -set :production_deploy_to, '/srv/www/example.com' - -# The domain name used for your staging environment -set :staging_domain, 'staging.example.com' - -# Database -# Set the values for host, user, pass, and name for both production and staging. -set :wpdb do - { - :production => { - :host => 'PRODUCTION DB HOST', - :user => 'PRODUCTION DB USER', - :password => 'PRODUCTION DB PASS', - :name => 'PRODUCTION DB NAME', - }, - :staging => { - :host => 'STAGING DB HOST', - :user => 'STAGING DB USER', - :password => 'STAGING DB PASS', - :name => 'STAGING DB NAME', - } - } -end - -# You're not done! You must also configure production.rb and staging.rb diff --git a/config/config-sample-placeholded.rb b/config/config-sample-placeholded.rb new file mode 100644 index 0000000..d43006a --- /dev/null +++ b/config/config-sample-placeholded.rb @@ -0,0 +1,124 @@ +# Stage WP configuration file. +# +# This is a sample config file. It manages settings for your remote repository, +# your default domains and your database values. Copy it to config.rb and +# customize it to your own needs. + + +# Application settings. + +# // The name of your application. It can be any string. +set :application, "%%APPLICATION%%" + +# // A shortname to identify your application. +set :application_id, "%%APPLICATION_ID%%" + +# Where is your wp-config.php file located within #{release_path}? +# // Your release path points to the newer copy of your repository, so you must +# // specify where your wp-config.php file is located within your remore repo. +# // WordPress Barebones puts the file in the "app" subfolder, so it needs to +# // be "#{release_path}/app", but if you are using WordPress-Skeleton, just +# // "#{release_path}" should do the trick and this value should be left empty. +set :application_path, "/app" + +# Where are your shell tasks located within #{release_path}? +# // If you're using WordPress Barebones, the tasks should be by default into +# // /app/tasks. If you are using another WordPress starting repo, you should +# // adjust this value to your own configuration. However, all tasks related to +# // this setting will not be executed if following path does'n t exist. +set :tasks_path, "/app/tasks" + +# Repository settings + +# // Location of your remote repository. +set :repository, "%%REPOSITORY%%" + +# // Your preferred method for source control. Supports :accurev, :bzr, :cvs, +# // :darcs, :git, :mercurial, :perforce, :subversion or :none. +set :scm, :git + +# // Using Git submodules? +set :git_enable_submodules, 1 + +# Default deploy directories. + +# // :deploy_to in production.rb will use this value as default +set :production_deploy_to, "%%PRODUCTION_DEPLOY_TO%%" + +# // :deploy_to in staging.rb will use this value as default +set :staging_deploy_to, "%%STAGING_DEPLOY_TO%%" + +# Default domains. + +# // :deploy_to in production.rb will use this value as default +set :production_domain, "%%PRODUCTION_DOMAIN%%" + +# // :deploy_to in staging.rb will use this value as default +set :staging_domain, "%%STAGING_DOMAIN%%" + +# Local stage default settings. + +# // Path to your local shared folder. +set :local_shared_folder, "%%LOCAL_SHARED_FOLDER%%" + +# Backup settings + +# // Where do you want to save your application remote backups? +set :production_backup_path, "%%PRODUCTION_BACKUP_PATH%%" +set :staging_backup_path, "%%STAGING_BACKUP_PATH%%" + +# // How many application backups do you want to save? +set :production_max_backups, "3" +set :staging_max_backups, "3" + +# WordPress database settings. +# +# Set the values for host, user, pass, and name for production, staging and +# local stages. You can also specify a backup directory where your mysqldumps +# should be saved. Note that placeholders like %%DB_NAME%% and %%DB_PASSWORD%% +# in your wp-config.php file will be automatically replaced when `cap deploy` is +# executed, so you only have to configure these values here. +set :wpdb do + { + :production => { + :host => "%%PRODUCTION_HOST%%", + :user => "%%PRODUCTION_USER%%", + :password => "%%PRODUCTION_PASSWORD%%", + :name => "%%PRODUCTION_DB%%", + :backups_dir => "%%PRODUCTION_BACKUPS_DIR%%", + :max_backups => "%%PRODUCTION_MAX_BACKUPS%%", + :dump_suffix => "%%PRODUCTION_DUMP_SUFFIX%%", # A string to differentiate mysqldumps + }, + :staging => { + :host => "%%STAGING_HOST%%", + :user => "%%STAGING_USER%%", + :password => "%%STAGING_PASSWORD%%", + :name => "%%STAGING_DB%%", + :backups_dir => "%%STAGING_BACKUPS_DIR%%", + :max_backups => "%%STAGING_MAX_BACKUPS%%", + :dump_suffix => "%%STAGING_DUMP_SUFFIX%%", # A string to differentiate mysqldumps + }, + :local => { + :host => "%%LOCAL_HOST%%", + :user => "%%LOCAL_USER%%", + :password => "%%LOCAL_PASSWORD%%", + :name => "%%LOCAL_DB%%", + :backups_dir => "%%LOCAL_BACKUPS_DIR%%", + :max_backups => "%%LOCAL_MAX_BACKUPS%%", + :dump_suffix => "%%LOCAL_DUMP_SUFFIX%%", # A string to differentiate mysqldumps + } + } +end + +# Additional hook files. + +# // Load (if exists) a file meant to contain your custom hooks for tasks. +if File.exists?("lib/custom-hooks.rb") then + loadFile "lib/custom-hooks.rb" +end +# // Load (if exists) a file meant to contain your custom tasks. +if File.exists?("lib/custom-hooks.rb") then + loadFile "lib/custom-tasks.rb" +end + +# You're not done! You must also configure production.rb, staging.rb and local.rb diff --git a/config/config-sample.rb b/config/config-sample.rb new file mode 100644 index 0000000..2ac52f5 --- /dev/null +++ b/config/config-sample.rb @@ -0,0 +1,107 @@ +# Stage WP configuration file. +# +# This is a sample config file. It manages settings for your remote repository, +# your default domains and your database values. Copy it to config.rb and +# customize it to your own needs. + + +# Application settings. + +# // The name of your application. It can be any string. +set :application, "Local WordPress Installation" + +# // A shortname to identify your application. +set :application_id, "wordpress" + +# Where is your wp-config.php file located within #{release_path}? +# // Your release path points to the newer copy of your repository, so you must +# // specify where your wp-config.php file is located within your remore repo. +# // WordPress Barebones puts the file in the "app" subfolder, so it needs to +# // be "#{release_path}/app", but if you are using WordPress-Skeleton, just +# // "#{release_path}" should do the trick and this value should be left empty. +set :application_path, "/app" + +# Repository settings + +# // Location of your remote repository. +set :repository, "git://github.com/username/website.git" + +# // Your preferred method for source control. Supports :accurev, :bzr, :cvs, +# // :darcs, :git, :mercurial, :perforce, :subversion or :none. +set :scm, :git + +# // Using Git submodules? +set :git_enable_submodules, 1 + +# Default deploy directories. + +# // :deploy_to in production.rb will use this value as default +set :production_deploy_to, "/srv/www/website/application" + +# // :deploy_to in staging.rb will use this value as default +set :staging_deploy_to, "/srv/www/website/application" + +# Default domains. + +# // :deploy_to in production.rb will use this value as default +set :production_domain, "www.website.com" + +# // :deploy_to in staging.rb will use this value as default +set :staging_domain, "staging.website.com" + +# Local stage default settings. + +# // Path to your local shared folder. +set :local_shared_folder, "/srv/www/website/application/shared" + +# WordPress database settings. +# +# Set the values for host, user, pass, and name for production, staging and +# local stages. You can also specify a backup directory where your mysqldumps +# should be saved. Note that placeholders like %%DB_NAME%% and %%DB_PASSWORD%% +# in your wp-config.php file will be automatically replaced when `cap deploy` is +# executed, so you only have to configure these values here. +set :wpdb do + { + :production => { + :host => "localhost", + :user => "root", + :password => "root", + :name => "production_db", + :backups_dir => "/srv/www/website/backups/dumps", + :max_backups => "3", + :dump_suffix => "production", # A string to differentiate mysqldumps + }, + :staging => { + :host => "localhost", + :user => "root", + :password => "root", + :name => "staging_db", + :backups_dir => "/srv/www/website/backups/dumps", + :max_backups => "3", + :dump_suffix => "staging", # A string to differentiate mysqldumps + }, + :local => { + :host => "localhost", + :user => "root", + :password => "root", + :name => "local_db", + :backups_dir => "/srv/www/website/backups/dumps", + :max_backups => "3", + :dump_suffix => "local", # A string to differentiate mysqldumps + } + } +end + +# Additional hook files. + +# // Load (if exists) a file meant to contain your custom hooks for tasks. +if File.exists?("lib/custom-hooks.rb") then + loadFile "lib/custom-hooks.rb" +end +# // Load (if exists) a file meant to contain your custom tasks. +if File.exists?("lib/custom-hooks.rb") then + loadFile "lib/custom-tasks.rb" +end + +# You're not done! You must also configure production.rb, staging.rb and local.rb diff --git a/config/deploy/local.rb b/config/deploy/local.rb new file mode 100644 index 0000000..b3400c9 --- /dev/null +++ b/config/deploy/local.rb @@ -0,0 +1,2 @@ +# This file will load the required values for your local stage. +loadFile 'config/local.rb' diff --git a/config/deploy/production.rb b/config/deploy/production.rb index ac3cde1..9fe7906 100644 --- a/config/deploy/production.rb +++ b/config/deploy/production.rb @@ -1 +1,2 @@ -loadFile 'config/production.rb' \ No newline at end of file +# This file will load the required values for your production stage. +loadFile 'config/production.rb' diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb index bd3279d..f833e20 100644 --- a/config/deploy/staging.rb +++ b/config/deploy/staging.rb @@ -1 +1,2 @@ -loadFile 'config/staging.rb' \ No newline at end of file +# This file will load the required values for your staging stage. +loadFile 'config/staging.rb' diff --git a/config/local-sample.rb b/config/local-sample.rb new file mode 100644 index 0000000..4cbbd77 --- /dev/null +++ b/config/local-sample.rb @@ -0,0 +1,9 @@ +# local-sample.rb +# +# This file is only loaded for the local stage. It contains values that +# will be present when you run tasks related to local. +# +# Customize this file to your own needs and copy it as local.rb. + +# Where is your local shared folder? +set :shared_folder, local_shared_folder diff --git a/config/SAMPLE.staging.rb b/config/production-sample.rb similarity index 62% rename from config/SAMPLE.staging.rb rename to config/production-sample.rb index fa2081b..e8ded66 100644 --- a/config/SAMPLE.staging.rb +++ b/config/production-sample.rb @@ -1,14 +1,19 @@ -# This file is only loaded for the staging environment -# Customize it and rename it as staging.rb +# production-sample.rb +# +# This file is only loaded for the production stage. It contains values that +# will be present when you run tasks related to production. +# +# Customize this file to your own needs and copy it as production.rb. # Where should the site deploy to? -set :deploy_to, "/srv/www/example.com" +set :deploy_to, production_deploy_to # Now configure the servers for this environment # OPTION 1 +# Your web servers IP addresses or hostnamen go here -# role :web, "your web server IP address or hostname here" +role :web, production_domain # role :web, "second web server here" # role :web, "third web server here, etc" diff --git a/config/SAMPLE.production.rb b/config/staging-sample.rb similarity index 63% rename from config/SAMPLE.production.rb rename to config/staging-sample.rb index cd2cf17..0061af3 100644 --- a/config/SAMPLE.production.rb +++ b/config/staging-sample.rb @@ -1,14 +1,19 @@ -# This file is only loaded for the production environment -# Customize it and rename it as production.rb +# staging-sample.rb +# +# This file is only loaded for the staging stage. It contains values that +# will be present when you run tasks related to staging. +# +# Customize this file to your own needs and copy it as staging.rb. # Where should the site deploy to? -set :deploy_to, "/srv/www/example.com" +set :deploy_to, staging_deploy_to # Now configure the servers for this environment # OPTION 1 +# Your web servers IP addresses or hostnamen go here -# role :web, "your web server IP address or hostname here" +role :web, staging_domain # role :web, "second web server here" # role :web, "third web server here, etc" diff --git a/custom/README.md b/custom/README.md new file mode 100644 index 0000000..3223473 --- /dev/null +++ b/custom/README.md @@ -0,0 +1,3 @@ +## Custom folder + +Include here all the files you need that are not considered by the other folders within this package. Everything you put here will be ignored by Git, and should be loaded only from files that are also being ignored, such as Customfile or /config/config.rb. \ No newline at end of file diff --git a/lib/custom-hooks-sample.rb b/lib/custom-hooks-sample.rb new file mode 100644 index 0000000..871850a --- /dev/null +++ b/lib/custom-hooks-sample.rb @@ -0,0 +1,9 @@ +# custom-hooks-sample.rb +# +# This is a sample file for custom task hooks. Copy this file to +# custom-hooks.rb and add your own hooks or use the following ones. +# custom-hooks.rb is not required, and if it exists, it will be loaded +# automatically from /config/config.rb. + +# Backup remote database before making symbolic link to current release. +before "deploy:create_symlink", "db:backup" diff --git a/lib/custom-tasks-sample.rb b/lib/custom-tasks-sample.rb new file mode 100644 index 0000000..91582d9 --- /dev/null +++ b/lib/custom-tasks-sample.rb @@ -0,0 +1,27 @@ +# custom-tasks-sample.rb +# +# This is a sample file for custom tasks. Copy this file to custom-tasks.rb and +# add your own tasks or use the following ones. custom-tasks.rb is not required, +# and if it exists, it will be loaded automatically from /config/config.rb. + +namespace :static do + desc "Upload non-repository files" + task :upload, :roles => :web do + if stage == :local then + puts "[ERROR] You must run static:upload from staging with cap staging static:upload or from production with cap production static:upload" + else + uploads = [ + [ "/example/source", "/example/destination" ], + ] + uploads.each do |upload| + if File.exists? upload[0] then + current_host = capture("echo $CAPISTRANO:HOST$").strip + command = "scp -r #{upload[0]} #{user}@#{current_host}:#{upload[1]}" + system command + else + puts "The path #{upload[0]} does not exists" + end + end + end + end +end \ No newline at end of file diff --git a/lib/deploy-after.rb b/lib/deploy-after.rb index 7453918..e16da92 100644 --- a/lib/deploy-after.rb +++ b/lib/deploy-after.rb @@ -1 +1,8 @@ -before( "deploy", "git:submodule_tags" ) if git_enable_submodules +# deploy-after.rb +# +# This file is loaded after deploy.rb, and includes hooks to be executed when +# all the config settings are loaded. + + +# Clone Git submodules before making symbolic link to current release. +before "deploy:create_symlink", "git:submodule_tags" if git_enable_submodules diff --git a/lib/deploy.rb b/lib/deploy.rb index 2998f1e..84e1526 100644 --- a/lib/deploy.rb +++ b/lib/deploy.rb @@ -1,15 +1,23 @@ +# deploy.rb # +# This file includes default settings and hooks for the deployment process. + +# Set some default values set :user, "deploy" set :use_sudo, false set :deploy_via, :remote_cache -set :copy_exclude, [".git", ".gitmodules", ".DS_Store", ".gitignore"] +set :copy_exclude, [".git", ".gitmodules", ".DS_Store", ".gitignore", "shared", "vvv"] set :keep_releases, 5 +# Put some hooks before deploy tasks after "deploy:update", "deploy:cleanup" after "deploy:update_code", "shared:make_shared_dir" after "deploy:update_code", "shared:make_symlinks" after "deploy:update_code", "db:make_config" after "deploy", "memcached:update" +# Put some hooks after deploy tasks +before "deploy:setup", "db:init" + # Pull in the config file loadFile 'config/config.rb' diff --git a/lib/tasks.rb b/lib/tasks.rb index 4765b18..5d3105f 100644 --- a/lib/tasks.rb +++ b/lib/tasks.rb @@ -1,29 +1,58 @@ +# tasks.rb +# +# This file contains the core tasks for Stage WP. You can override any of these +# tasks and add your own into custom-tasks.rb. + namespace :shared do + desc "Create shared folder" task :make_shared_dir do run "if [ ! -d #{shared_path}/files ]; then mkdir #{shared_path}/files; fi" end + desc "Create symlink to shared folder" task :make_symlinks do - run "if [ ! -h #{release_path}/shared ]; then ln -s #{shared_path}/files/ #{release_path}/shared; fi" - run "for p in `find -L #{release_path} -type l`; do t=`readlink $p | grep -o 'shared/.*$'`; sudo mkdir -p #{release_path}/$t; sudo chown www-data:www-data #{release_path}/$t; done" + run "if [ ! -h #{release_path}/shared ]; then ln -s #{shared_path} #{release_path}/shared; fi" + run "for p in `find -L #{release_path} -type l`; do t=`readlink $p | grep -o 'shared/.*$'`; sudo mkdir -p #{release_path}/$t; done" + end + desc "Pulls shared files from remote location" + task :pull do + if stage == :local then + puts "[Error] You must run shared:pull from staging or production with cap staging shared:pull or cap production shared:pull" + else + current_host = capture("echo $CAPISTRANO:HOST$").strip + system "rsync -avz --delete #{user}@#{current_host}:#{shared_path}/files/ #{local_shared_folder}/files/" + end + end + desc "Pushes shared files to remote location" + task :push do + if stage == :local then + puts "[Error] You must run shared:pull from staging or production with cap staging shared:pull or cap production shared:pull" + else + current_host = capture("echo $CAPISTRANO:HOST$").strip + system "rsync -avz #{local_shared_folder}/files/ #{user}@#{current_host}:#{shared_path}/shared/files/" + end end end namespace :nginx do - desc "Restarts nginx" + desc "Restart nginx" task :restart do run "sudo /etc/init.d/nginx reload" end end namespace :phpfpm do - desc" Restarts PHP-FPM" + desc "Restart PHP-FPM" task :restart do - run "sudo /etc/init.d/php-fpm restart" + begin # For non-Ubuntu systems + run "sudo /etc/init.d/php-fpm restart" + rescue Exception => e # For Ubuntu systems + run "sudo /etc/init.d/php5-fpm restart" + end end end namespace :git do - desc "Updates git submodule tags" + desc "Update Git submodule tags" task :submodule_tags do run "if [ -d #{shared_path}/cached-copy/ ]; then cd #{shared_path}/cached-copy/ && git submodule foreach --recursive git fetch origin --tags; fi" end @@ -44,33 +73,220 @@ end namespace :db do + desc "Initialize the WordPress database in a remote stage" # This is a tricky one + task :init do + if stage == :local then + puts "[Error] You must run db:init from staging or production with cap staging db:init or cap production db:init" + else + s = (stage == :production) ? wpdb[:production] : wpdb[:staging] + # Obtain MySQL admin credentials + admin_name = Capistrano::CLI.ui.ask "Please provide the name of your MySQL admin user [root] " + admin_name = "root" if admin_name.empty? + admin_password = Capistrano::CLI.password_prompt "Please provide the password of your MySQL admin user [root] " + admin_password = "root" if admin_password.empty? + # Store query strings + create = "CREATE DATABASE IF NOT EXISTS #{s[:name]};" + grant = "GRANT ALL PRIVILEGES ON #{s[:name]}.* TO '#{s[:user]}'@'#{s[:host]}' IDENTIFIED BY '#{s[:password]}';" + show = "SHOW DATABASES;" + # Initialize output + output = "" + # Check if database already exists + run "mysql -u #{admin_name} -p -e \"#{show}\"" do |channel, stream, data| + if data =~ /^Enter password: / + # Securely pass admin password + channel.send_data "#{admin_password}\n" + end + output += data + end + if output.include? s[:name] then # If database exists ... + puts "Database already exists." + else # In case database does not exist ... + output = "" + # Try to create database + run "mysql -u #{admin_name} -p -e \"#{create} #{grant} #{show}\"" do |channel, stream, data| + if data =~ /^Enter password: / + # Securely pass admin password + channel.send_data "#{admin_password}\n" + end + output += data + end + if output.include? s[:name] then # If database was created ... + puts "Remote database was initialized." + else # If database wasn't created ... + puts "Remote database could not be created." + end + end + end + end desc "Syncs the staging database (and uploads) from production" - task :sync, :roles => :web do - if stage != :staging then - puts "[ERROR] You must run db:sync from staging with cap staging db:sync" + task :sync do + if stage == :production then + puts "[Error] You must run db:sync from staging with cap staging db:sync or from local with cap local db:sync" + else + if stage == :staging then + puts "Hang on... this might take a while." + random = rand(10 ** 5).to_s.rjust(5, '0') + p = wpdb[:production] + s = wpdb[:staging] + run "ssh #{user}@#{production_domain} \"mysqldump -u #{p[:user]} --result-file=/tmp/stagewp-#{random}.sql -h #{p[:host]} -p#{p[:password]} #{p[:name]}\"" + run "scp #{user}@#{production_domain}:/tmp/stagewp-#{random}.sql /tmp/" + run "mysql -u #{s[:user]} -h #{s[:host]} -p#{s[:password]} #{s[:name]} < /tmp/stagewp-#{random}.sql && rm /tmp/stagewp-#{random}.sql" + puts "Database synced to staging" + # Now to copy files + find_servers( :roles => :web ).each do |server| + run "rsync -avz --delete #{user}@#{production_domain}:#{production_deploy_to}/shared/files/ #{staging_deploy_to}/shared/files/" + end + end + if stage == :local then + puts "Hang on... this might take a while." + random = rand(10 ** 5).to_s.rjust(5, '0') + p = wpdb[:production] + l = wpdb[:local] + system "ssh #{user}@#{production_domain} \"mysqldump -u #{p[:user]} --result-file=/tmp/stagewp-#{random}.sql -h #{p[:host]} -p#{p[:password]} #{p[:name]}\"" + system "scp #{user}@#{production_domain}:/tmp/stagewp-#{random}.sql /tmp/" + system "mysql -u #{l[:user]} -h #{l[:host]} -p#{l[:password]} #{l[:name]} < /tmp/stagewp-#{random}.sql && rm /tmp/stagewp-#{random}.sql" + puts "Database synced to local" + # memcached.restart + puts "Memcached flushed" + # Now to copy files + system "rsync -avz --delete #{user}@#{production_domain}:#{production_deploy_to}/shared/files/ #{local_shared_folder}/files/" + end + end + end + desc "Backup database" + task :backup do + if stage == :local then + l = wpdb[:local] + puts "Backing up MySQL local database..." + filename = "#{l[:backups_dir]}/#{release_name}-#{l[:dump_suffix]}.sql" + # Create folder for dumps, in case that it doesn't exist + system "sudo mkdir -p #{l[:backups_dir]}" + system "sudo mysqldump -u #{l[:user]} -p#{l[:password]} #{l[:name]} > #{filename}" + if File.exists? filename then + puts "MySQL local database saved to #{filename}" + else + puts "MySQL local database could not be saved." + end + end + if stage == :production || stage == :staging then + s = (stage == :production) ? wpdb[:production] : wpdb[:staging] + puts "Backing up remote MySQL database..." + filename = "#{s[:backups_dir]}/#{release_name}-#{s[:dump_suffix]}.sql" + # Create folder for dumps, in case that it doesn't exist + run "mkdir -p #{s[:backups_dir]}" + begin + run "mysqldump -u #{s[:user]} -p #{s[:name]} > #{filename}" do |channel, stream, data| + if data =~ /^Enter password: / + channel.send_data "#{s[:password]}\n" + end + end + puts "Remote MySQL database saved to #{filename}" + rescue Exception => Error + puts "Remote MySQL database could not be saved." + end + end + end + desc "Restore database from backup" + task :restore do + if stage == :local then + l = wpdb[:local] + puts "Searching for available local backups..." + # List contents from dumps folder + backups = `ls -1 #{l[:backups_dir]}/`.split("\n") + # Define default backup + default_backup = backups.last + puts "Available backups: " + puts backups + backup = Capistrano::CLI.ui.ask "Which backup would you like to restore? [#{default_backup}] " + backup_file = backup + backup_file = default_backup if backup_file.empty? + if system "mysql -u #{l[:user]} -p#{l[:password]} #{l[:name]} < #{l[:backups_dir]}/#{backup_file}" then + puts "Local database restored to backup saved in #{l[:backups_dir]}/#{backup_file}." + else + puts "Local database could not be restored from backup." + end + else + env = (stage == :production) ? wpdb[:production] : wpdb[:staging] + puts "Searching for available remote backups..." + # List contents from dumps folder + backups = capture("ls -1 #{env[:backups_dir]}").split("\n") + # Define default backup + default_backup = backups.last.scan(/[a-zA-Z0-9_.\-]+/i) + puts "Available backups: " + puts backups + backup = Capistrano::CLI.ui.ask "Which backup would you like to restore? [#{default_backup}] " + backup_file = default_backup if backup.empty? + backup_file = "#{env[:backups_dir]}/#{backup_file}" + begin + run "mysql -u #{env[:user]} -p#{env[:password]} #{env[:name]} < #{backup_file}" do |channel, stream, data| + if data =~ /^Enter password: / + channel.send_data "#{p[:password]}\n" + end + end + puts "Remote database restored to backup saved in #{backup_file}." + rescue Exception => Error + puts "Remote database could not be restored from backup." + end + end + end + desc "Pull a remote database backup to local" + task :pull do + if stage == :local then + puts "[Error] You must run db:pull from staging or production with cap staging db:pull or cap production db:pull" + else + env = (stage == :production) ? wpdb[:production] : wpdb[:staging] + l = wpdb[:local] + puts "Searching for available remote backups..." + # List contents from dumps folder + backups = capture("ls -1 #{env[:backups_dir]}").split("\n") + # Define default backup + default_backup = backups.last.scan(/[a-zA-Z0-9_.\-]+/i) + puts "Available backups: " + puts backups + backup = Capistrano::CLI.ui.ask "Which backup would you like to pull? [#{default_backup}] " + backup_file = default_backup if backup.empty? + current_host = capture("echo $CAPISTRANO:HOST$").strip + if system "scp #{user}@#{current_host}:#{env[:backups_dir]}/#{backup_file} #{l[:backups_dir]}" then + puts "Remote database saved to local host at #{l[:backups_dir]}/#{backup_file}." + else + puts "Remote database could not be pulled from backup." + end + end + end + desc "Push a remote database backup to remote from local" + task :push do + if stage == :local then + puts "[Error] You must run db:push from staging or production with cap staging db:push or cap production db:push" else - puts "Hang on... this might take a while." - random = rand( 10 ** 5 ).to_s.rjust( 5, '0' ) - p = wpdb[ :production ] - s = wpdb[ :staging ] - puts "db:sync" - puts stage - system "mysqldump -u #{p[:user]} --result-file=/tmp/wpstack-#{random}.sql -h #{p[:host]} -p#{p[:password]} #{p[:name]}" - system "mysql -u #{s[:user]} -h #{s[:host]} -p#{s[:password]} #{s[:name]} < /tmp/wpstack-#{random}.sql && rm /tmp/wpstack-#{random}.sql" - puts "Database synced to staging" - # memcached.restart - puts "Memcached flushed" - # Now to copy files - find_servers( :roles => :web ).each do |server| - system "rsync -avz --delete #{production_deploy_to}/shared/files/ #{server}:#{shared_path}/files/" + env = (stage == :production) ? wpdb[:production] : wpdb[:staging] + l = wpdb[:local] + puts "Searching for available local backups..." + # List contents from dumps folder + backups = `ls -1 #{l[:backups_dir]}/`.split("\n") + # Define default backup + default_backup = backups.last + puts "Available backups: " + puts backups + backup = Capistrano::CLI.ui.ask "Which backup would you like to push? [#{default_backup}] " + backup_file = default_backup if backup.empty? + current_host = capture("echo $CAPISTRANO:HOST$").strip + if system "scp #{l[:backups_dir]}/#{backup_file} #{user}@#{current_host}:#{env[:backups_dir]}" then + puts "Local database uploaded to remote host at #{env[:backups_dir]}/#{backup_file}." + else + puts "Local database could not be pushed from backup." end end end - desc "Sets the database credentials (and other settings) in wp-config.php" + desc "Set the database credentials (and other settings) in wp-config.php" task :make_config do - set :staging_domain, '' unless defined? staging_domain - {:'%%WP_STAGING_DOMAIN%%' => staging_domain, :'%%WP_STAGE%%' => stage, :'%%DB_NAME%%' => wpdb[stage][:name], :'%%DB_USER%%' => wpdb[stage][:user], :'%%DB_PASSWORD%%' => wpdb[stage][:password], :'%%DB_HOST%%' => wpdb[stage][:host]}.each do |k,v| - run "sed -i 's/#{k}/#{v}/' #{release_path}/wp-config.php", :roles => :web + {:'%%WP_STAGING_DOMAIN%%' => (stage == :production) ? production_domain : staging_domain, + :'%%WP_STAGE%%' => stage, + :'%%DB_NAME%%' => wpdb[stage][:name], + :'%%DB_USER%%' => wpdb[stage][:user], + :'%%DB_PASSWORD%%' => wpdb[stage][:password], + :'%%DB_HOST%%' => wpdb[stage][:host] + }.each do |k, v| + run "sed -i 's/#{k}/#{v}/' #{release_path}#{application_path}/wp-config.php", :roles => :web end end end diff --git a/vvv/vvv-init.sh b/vvv/vvv-init.sh new file mode 100644 index 0000000..77732c1 --- /dev/null +++ b/vvv/vvv-init.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# vvv-init.sh +# +# This file will install Capistrano in VVV virtual machines. + +# Capture a basic ping result to Google's primary DNS server to determine if +# outside access is available to us. If this does not reply after 2 attempts, +# we try one of Level3's DNS servers as well. If neither IP replies to a ping, +# then we'll skip a few things further in provisioning rather than creating a +# bunch of errors. +echo -e "\nStarting Capistrano installation process..." +ping_result="$(ping -c 2 8.8.4.4 2>&1)" +if [[ $ping_result != *bytes?from* ]]; then + ping_result="$(ping -c 2 4.2.2.2 2>&1)" +fi + +if [[ $ping_result == *bytes?from* ]]; then + gem_exists="$(which gem)" # check if rubygems is installed + cap_exists="$(which cap)" # check if capistrano is installed + + # ruby install, required by Capistrano + if [[ ! $gem_exists ]]; then + sudo apt-get -y install ruby + # Clean up apt caches + sudo apt-get clean + else + echo " * ruby is already installed." + fi + + # Capistrano install + if [[ ! $cap_exists ]]; then + # We're specifying an older Capistrano version, because recent versions + # may cause errors running some cap commands. + sudo gem install capistrano -v 2.15.5 + # Install Capistrano friends + sudo gem install capistrano-ext railsless-deploy + else + echo " * Capistrano is already installed." + fi + +else + echo -e "\nNo network connection available, skipping package installation" +fi diff --git a/wordpress-mu-plugins/stage-wp-bitbucket-connector.php b/wordpress-mu-plugins/stage-wp-bitbucket-connector.php new file mode 100644 index 0000000..c045e48 --- /dev/null +++ b/wordpress-mu-plugins/stage-wp-bitbucket-connector.php @@ -0,0 +1,399 @@ + + * @license GPL2 + * @link http://github.com/andrezrv/stage-wp-bitbucket-connector/ + * @copyright 2015 Andrés Villarreal + * + * @wordpress-plugin + * Plugin name: Stage WP Bitbucket Connector + * Plugin URI: http://github.com/andrezrv/stage-wp/ + * Description: Connect the current site with Bitbucket through a POST hook and deploy using Stage WP. + * Author: Andrés Villarreal + * Author URI: http://about.me/andrezrv + * Version: 1.0.0 + * License: GPL2 + */ + +if ( ! class_exists( 'Stage_WP_Bitbucket_Connector' ) ) : +/** + * Class Stage_WP_Bitbucket_Connector + * + * Setup connection between this WordPress website and Bitbucket POST hook. + * + * @since 1.0.0 + */ +final class Stage_WP_Bitbucket_Connector { + /** + * Internal options for this class. + * + * @var array|mixed|void + * @since 1.0.0 + */ + private $options = array(); + + /** + * Option ID to retrieve an array of options using the Options API. + * + * @var string + * @since 1.0.0 + */ + private static $options_id = 'stage_wp_bitbucket_connector_options'; + + /** + * ID used to initialize and retrieve results when interacting with the + * Settings API. + * + * @var string + * @since 1.0.0 + */ + private $settings_id = 'stage-wp-bitbucket-connector-settings'; + + /** + * Text domain for this plugin. + * + * @var string + * @since 1.0.0 + */ + private $text_domain = 'stage_wp'; + + /** + * Initialize internal options and plugin hooks. + * + * @since 1.0.0 + */ + private function __construct() { + // Return early if running in AJAX context. + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + return; + } + + // Obtain internal options. + $this->options = self::get_options(); + + // Schedule internal hooks. + $this->setup_hooks(); + } + + /** + * Make sure this class is instantiated only once. + * + * @return Stage_WP_Bitbucket_Connector Self instance of this class. + * @since 1.0 + */ + final public static function get_instance() { + static $instances = array(); + + // PHP >= 5.3 required. + $called_class = get_called_class(); + + if ( ! isset( $instances[$called_class] ) ) { + $instances[$called_class] = new $called_class(); + } + + return $instances[$called_class]; + } + + /** + * Schedule plugin hooks. + * + * @since 1.0.0 + */ + private function setup_hooks() { + // Register a sub-menu page. + add_action( 'admin_menu', array( $this, 'add_submenu_page' ) ); + + // Register plugin settings. + add_action( 'admin_init', array( $this, 'register_settings' ) ); + } + + /** + * Initialize internal options. + * + * @since 1.0.0 + */ + private static function initialize_options() { + $options = array( + 'key' => md5( time() ), + 'log' => '', + 'branch' => 'master', + 'stage_wp_path' => '', + ); + + update_option( self::$options_id, $options ); + } + + /** + * Obtain internal options. + * + * @since 1.0.0 + * @return mixed|void + */ + private static function get_options() { + $options = get_option( self::$options_id ); + + // If we don't have any options, initialize them and get back here again. + if ( ! $options ) { + self::initialize_options(); + $options = self::get_options(); + } + + return $options; + } + + /** + * Perform deployment process. + * + * @since 1.0.0 + */ + public function deploy() { + // Return early if the deployment can't be authorized. + if ( ! $this->allow_deployment() ) { + return; + } + + // Fire deployment process through Stage WP. + $this->prepare_deployment(); + + // Set a message with the date and time of the process. + $message = sprintf( __( 'Deployment performed at %s', $this->text_domain ), date( 'Y-m-d H:i:s' ) ); + + // Log previous message. + $this->log( $message ); + + // Die showing message. + die( $message ); + } + + /** + * Check if the deployment process can be fired. + * + * @since 1.0.0 + * @return bool + */ + private function allow_deployment() { + if ( ! $this->requested_deployment() || ! $this->request_has_valid_key() || ! $this->request_has_valid_branch() ) { + return false; + } + + return true; + } + + /** + * Check if the deployment process was correctly requested. + * + * @since 1.0.0 + * @return bool + */ + private function requested_deployment() { + if ( empty( $_REQUEST['deploy'] ) || 'true' != $_REQUEST['deploy'] ) { + return false; + } + + return true; + } + + /** + * Check if the deployment process was requested using a valid key. + * + * @since 1.0.0 + * @return bool + */ + private function request_has_valid_key() { + if ( empty( $_REQUEST['key'] ) || $this->options['key'] != urldecode( $_REQUEST['key'] ) ) { + return false; + } + + return true; + } + + /** + * Check if at least one of the pushed commits belongs to the deployment branch. + * + * @since 1.0.0 + * @return bool + */ + private function request_has_valid_branch() { + if ( empty( $_REQUEST['payload'] ) ) { + return false; + } + + $payload = json_decode( $_REQUEST['payload'], true ); + + if ( empty( $payload['commits'] ) ) { + return false; + } + + $has_allowed_branch = false; + foreach ( $payload['commits'] as $commit ) { + if ( $this->options['branch'] == $commit['branch'] ) { + $has_allowed_branch = true; + break; + } + } + + return $has_allowed_branch; + } + + private function prepare_deployment() { + // Log received data. + $this->log( 'Preparing deployment with the following data: ' . serialize( $_REQUEST ) ); + + // Construct command to run through Stage WP. + $cmd = 'cd ' . $this->options['stage_wp_path'] . '; cap deploy 2>&1'; + + // Execute command. + exec( $cmd, $output, $return_var ); + + // Log results of deployment process. + $this->log( 'Deployment results: ' . "\n" . implode( "\n", $output ) ); + } + + /** + * Add a submenu page to WP admin. + * + * @since 1.0.0 + */ + public function add_submenu_page() { + // Avoid this function from running inside any other action. + if ( 'admin_menu' != current_action() ) { + return false; + } + + add_submenu_page( + 'options-general.php', + __( 'Stage WP Bitbucket Connector' ), + __( 'Deployment Settings' ), + 'manage_options', + 'stage-wp-bitbucket-connector', + array( $this, 'settings_page' ) + ); + } + + /** + * Output HTML for settings page. + * + * @since 1.0.0 + */ + public function settings_page() { + // Avoid this function from running inside any other action. + if ( 'settings_page_stage-wp-bitbucket-connector' != current_action() ) { + return false; + } + + // Check that the user is allowed to update options. + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( 'You do not have sufficient permissions to access this page.' ); + } + + $options = self::get_options(); + ?> +
STAGE_WP_CDN_DOMAIN in your wp-config.php or hook in and override the stage_wp_cdn_domain option. Provide a domain name only, like static.example.com. The plugin will look for static file URLs on your domain and repoint them to the CDN domain. Original credits go to Mark Jaquith.
+ * Author: Andrés Villarreal
+ * Author URI: http://about.me/andrezrv
+ * Version: 1.1
+ * License: GPL2
+ */
+// Load required class.
+if ( !class_exists( 'Stage_WP_Plugin' ) ) {
+ if ( defined( 'STAGE_WP_PLUGIN_LOCATION' ) && file_exists( $file = STAGE_WP_PLUGIN_LOCATION ) ) {
+ require( $file );
+ } elseif ( file_exists( $file = dirname( __FILE__ ) . '/stage-wp-plugin.class.php' ) ) {
+ require( $file );
+ }
+}
+
+// Once class is loaded, extend it.
+if ( class_exists( 'Stage_WP_Plugin' ) ) {
+
+ class Stage_WP_CDN_Plugin extends Stage_WP_Plugin {
+
+ public static $instance;
+ public $site_domain;
+ public $cdn_domain;
+ public $upload_dir;
+ public $uploads_only;
+ public $extensions;
+
+ public function __construct() {
+ self::$instance = $this;
+ $this->hook( 'plugins_loaded' );
+ }
+
+ public function plugins_loaded() {
+
+ $domain_set_up = get_option( 'stage_wp_cdn_domain' ) || ( defined( 'STAGE_WP_CDN_DOMAIN' ) && STAGE_WP_CDN_DOMAIN );
+ $production = defined( 'WP_STAGE' ) && WP_STAGE === 'production';
+ $staging = defined( 'WP_STAGE' ) && WP_STAGE === 'staging';
+ $uploads_only = defined( 'STAGE_WP_CDN_UPLOADS_ONLY' ) && STAGE_WP_CDN_UPLOADS_ONLY;
+
+ if ( $domain_set_up && !$staging && ( $production || $uploads_only ) ) {
+ $this->hook( 'init' );
+ }
+
+ }
+
+ public function init() {
+
+ $this->uploads_only = apply_filters( 'stage_wp_cdn_uploads_only', defined( 'STAGE_WP_CDN_UPLOADS_ONLY' ) ? STAGE_WP_CDN_UPLOADS_ONLY : false );
+ $this->extensions = apply_filters( 'stage_wp_cdn_extensions', array( 'jpe?g', 'gif', 'png', 'css', 'bmp', 'js', 'ico' ) );
+
+ if ( !is_admin() ) {
+
+ $this->hook( 'template_redirect' );
+
+ if ( $this->uploads_only ) {
+ $this->hook( 'stage_wp_cdn_content', 'filter_uploads_only' );
+ } else {
+ $this->hook( 'stage_wp_cdn_content', 'filter' );
+ }
+
+ $this->site_domain = parse_url( get_bloginfo( 'url' ), PHP_URL_HOST );
+ $this->cdn_domain = defined( 'STAGE_WP_CDN_DOMAIN' ) ? STAGE_WP_CDN_DOMAIN : get_option( 'stage_wp_cdn_domain' );
+
+ }
+
+ }
+
+ public function filter_uploads_only( $content ) {
+
+ $upload_dir = wp_upload_dir();
+ $upload_dir = $upload_dir['baseurl'];
+ $domain = preg_quote( parse_url( $upload_dir, PHP_URL_HOST ), '#' );
+ $path = parse_url( $upload_dir, PHP_URL_PATH );
+ $preg_path = preg_quote( $path, '#' );
+
+ // Targeted replace just on uploads URLs
+ return preg_replace( "#=([\"'])(https?://{$domain})?$preg_path/((?:(?!\\1]).)+)\.(" . implode( '|', $this->extensions ) . ")(\?((?:(?!\\1).)+))?\\1#", '=$1//' . $this->cdn_domain . $path . '/$3.$4$5$1', $content );
+
+ }
+
+ public function filter( $content ) {
+ return preg_replace( "#=([\"'])(https?://{$this->site_domain})?/([^/](?:(?!\\1).)+)\.(" . implode( '|', $this->extensions ) . ")(\?((?:(?!\\1).)+))?\\1#", '=$1//' . $this->cdn_domain . '/$3.$4$5$1', $content );
+ }
+
+ public function template_redirect() {
+ ob_start( array( $this, 'ob' ) );
+ }
+
+ public function ob( $contents ) {
+ return apply_filters( 'stage_wp_cdn_content', $contents, $this );
+ }
+ }
+
+ new Stage_WP_CDN_Plugin;
+
+}
diff --git a/wordpress-mu-plugins/stage-wp-manual-db-upgrades.php b/wordpress-mu-plugins/stage-wp-manual-db-upgrades.php
new file mode 100644
index 0000000..e875439
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-manual-db-upgrades.php
@@ -0,0 +1,68 @@
+/wp-admin/ requests to the WordPress
+ * database upgrade screen. On large sites, or sites with a lot of active
+ * authors, this may not be desired. This plugin prevents the automatic
+ * redirect and instead lets you manually go to
+ * /wp-admin/upgrade.php to upgrade a site.
+ *
+ * Original credits go to Mark Jaquith: {@link http://github.com/markjaquith/WP-Stack}
+ *
+ * @package Stage_WP_Manual_Database_Upgrades
+ * @author Andrés Villarreal /wp-admin/ requests to the WordPress database upgrade screen. On large sites, or sites with a lot of active authors, this may not be desired. This plugin prevents the automatic redirect and instead lets you manually go to /wp-admin/upgrade.php to upgrade a site. Original credits go to Mark Jaquith.
+ * Author: Andrés Villarreal
+ * Author URI: http://about.me/andrezrv
+ * Version: 1.0
+ * License: GPL2
+ */
+// Load required class.
+if ( !class_exists( 'Stage_WP_Plugin' ) ) {
+ if ( defined( 'STAGE_WP_PLUGIN_LOCATION' ) && file_exists( $file = STAGE_WP_PLUGIN_LOCATION ) ) {
+ require( $file );
+ } elseif ( file_exists( $file = dirname( __FILE__ ) . '/stage-wp-plugin.class.php' ) ) {
+ require( $file );
+ }
+}
+
+// Once class is loaded, extend it.
+if ( class_exists( 'Stage_WP_Plugin' ) ) {
+
+ class Stage_WP_Manual_DB_Upgrades_Plugin extends Stage_WP_Plugin {
+
+ public static $instance;
+
+ public function __construct() {
+ self::$instance = $this;
+ $this->hook( 'plugins_loaded' );
+ }
+
+ public function plugins_loaded() {
+ $this->hook( 'option_db_version' );
+ }
+
+ public function option_db_version( $version ) {
+
+ if ( strpos( $_SERVER['REQUEST_URI'], '/wp-admin/upgrade.php' ) === false ) {
+ return $GLOBALS['wp_db_version'];
+ }
+
+ return $version;
+
+ }
+
+ }
+
+ new Stage_WP_Manual_DB_Upgrades_Plugin;
+
+}
diff --git a/wordpress-mu-plugins/stage-wp-ms-uploads.php b/wordpress-mu-plugins/stage-wp-ms-uploads.php
new file mode 100644
index 0000000..b6cf533
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-ms-uploads.php
@@ -0,0 +1,97 @@
+
+ * @license GPL2
+ * @link http://github.com/andrezrv/stage-wp/
+ * @copyright 2014 Andrés Villarreal
+ *
+ * @wordpress-plugin
+ * Plugin name: Stage WP Multisite Uploads
+ * Plugin URI: http://github.com/andrezrv/stage-wp/
+ * Description: The way WordPress Multisite serves uploads is not ideal. It streams them through a PHP file. Professional sites should not do this. This plugin allows one NGINX rewrite rule to handle all uploads, eliminating the need for PHP streaming. It uses the following URL scheme for uploads: {scheme}://{domain}/wp-files/{blog_id}/. By inserting the $blog_id, one rewrite rule can make sure file requests go to the correct blog. Note: You will need to implement this NGINX rewrite rule for this to work: rewrite ^/wp-files/([0-9]+)/(.*)$ /wp-content/blogs.dir/$1/files/$2; Original credits go to Mark Jaquith.
+ * Author: Andrés Villarreal
+ * Author URI: http://about.me/andrezrv
+ * Version: 1.0
+ * License: GPL2
+ */
+// Load required class.
+if ( !class_exists( 'Stage_WP_Plugin' ) ) {
+ if ( defined( 'STAGE_WP_PLUGIN_LOCATION' ) && file_exists( $file = STAGE_WP_PLUGIN_LOCATION ) ) {
+ require( $file );
+ } elseif ( file_exists( $file = dirname( __FILE__ ) . '/stage-wp-plugin.class.php' ) ) {
+ require( $file );
+ }
+}
+
+// Once class is loaded, extend it.
+if ( class_exists( 'Stage_WP_Plugin' ) ) {
+
+ class Stage_WP_MS_Uploads_Plugin extends Stage_WP_Plugin {
+
+ public static $instance;
+
+ public function __construct() {
+ self::$instance = $this;
+ if ( is_multisite() ) {
+ $this->hook( 'init' );
+ }
+ }
+
+ public function init() {
+ global $blog_id;
+ if ( $blog_id != 1 ) {
+ $this->hook( 'option_fileupload_url' );
+ $this->hook( 'upload_dir' );
+ }
+ }
+
+ public function upload_dir( $upload ) {
+
+ /**
+ * $upload expected format:
+ * $upload = array( 'subdir' => '/2012/07',
+ * 'basedir' => '/Users/mark/Sites/wp.git/wp-content/uploads',
+ * 'path' => '/Users/mark/Sites/wp.git/wp-content/uploads/2012/07',
+ * 'baseurl' => 'http://wp.git/wp-files/1',
+ * 'url' => 'http://wp.git/wp-content/uploads/2012/07',
+ * 'error' => false
+ * );
+ */
+
+ global $blog_id;
+ $parsed = parse_url( $upload['baseurl'] );
+ $upload['baseurl'] = $parsed['scheme'] . '://' . $parsed['host'] . '/wp-files/' . $blog_id;
+ $upload['url'] = $upload['baseurl'] . $upload['subdir'];
+ var_export( $upload ); die();
+ return $upload;
+
+ }
+
+ // Does core even use this anymore?
+ public function option_fileupload_url( $url ) {
+ global $blog_id;
+ $parsed = parse_url( $url );
+ $url = $parsed['scheme'] . '://' . $parsed['host'] . '/wp-files/' . $blog_id . '/';
+ return $url;
+ }
+
+ }
+
+ new Stage_WP_MS_Uploads_Plugin;
+
+}
diff --git a/wordpress-mu-plugins/stage-wp-plugin-manager b/wordpress-mu-plugins/stage-wp-plugin-manager
new file mode 160000
index 0000000..cb80cd1
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-plugin-manager
@@ -0,0 +1 @@
+Subproject commit cb80cd1edb9e8aeb51ef3b9b98a391d7a22d9729
diff --git a/wordpress-mu-plugins/stage-wp-plugin-manager.php b/wordpress-mu-plugins/stage-wp-plugin-manager.php
new file mode 100644
index 0000000..36ccf3e
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-plugin-manager.php
@@ -0,0 +1,30 @@
+
+ * @license GPL-2.0
+ * @link http://github.com/andrezrv/stage-wp-plugin-manager
+ * @copyright 2014 Andrés Villarreal
+ *
+ * @wordpress-plugin
+ * Plugin Name: Stage WP Plugin Manager
+ * Plugin URI: http://wordpress.org/extend/plugins/stage-wp-plugin-manager/
+ * Description: Gives you the option to define which plugins must be active for a particular stage only. You can activate plugins separatedly for local, staging and production stages. To use this plugin, you need to add a constant named WP_ENV_PLUGINS to your wp-config.php file, with one of the following values: local, staging, production. This plugin supports WordPress Multisite, and can manage network activated plugins.
+ * Author: Andrés Villarreal
+ * Author URI: http://www.andrezrv.com
+ * Version: 1.0
+ */
+require dirname( __FILE__ ) . '/stage-wp-plugin-manager/stage-wp-plugin-manager.php';
diff --git a/wordpress-mu-plugins/stage-wp-plugin.class.php b/wordpress-mu-plugins/stage-wp-plugin.class.php
new file mode 100644
index 0000000..77b11ae
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-plugin.class.php
@@ -0,0 +1,49 @@
+
+ * @license GPL2
+ * @link http://github.com/andrezrv/stage-wp/
+ * @copyright 2014 Andrés Villarreal
+ *
+ * @wordpress-plugin
+ * Plugin name: Stage WP Plugin Class
+ * Plugin URI: http://github.com/andrezrv/wordpress-barebones/
+ * Description: Just a helper class for Stage WP must-use plugins. Original credits go to Mark Jaquith.
+ * Author: Andrés Villarreal
+ * Author URI: http://about.me/andrezrv
+ * Version: 1.0
+ * License: GPL2
+ */
+class Stage_WP_Plugin {
+
+ function hook( $h ) {
+
+ $p = 10;
+ $m = $this->sanitize_method( $h );
+ $b = func_get_args();
+
+ unset( $b[0] );
+
+ foreach( ( array )$b as $a ){
+ if ( is_int( $a ) ) {
+ $p = $a;
+ } else {
+ $m = $a;
+ }
+ }
+
+ return add_action( $h, array( $this, $m ), $p, 999 );
+ }
+
+ private function sanitize_method( $m ){
+ return str_replace( array( '.', '-' ), array( '_DOT_', '_DASH_' ), $m );
+ }
+
+}
diff --git a/wordpress-mu-plugins/stage-wp-staging.php b/wordpress-mu-plugins/stage-wp-staging.php
new file mode 100644
index 0000000..3951ba0
--- /dev/null
+++ b/wordpress-mu-plugins/stage-wp-staging.php
@@ -0,0 +1,63 @@
+
+ * @license GPL2
+ * @link http://github.com/andrezrv/stage-wp/
+ * @copyright 2014 Andrés Villarreal
+ *
+ * @wordpress-plugin
+ * Plugin name: Stage WP Staging
+ * Plugin URI: http://github.com/andrezrv/stage-wp/
+ * Description: For staging environments, this plugin will replace your production domain with the staging one. Original credits go to Mark Jaquith.
+ * Author: Andrés Villarreal
+ * Author URI: http://about.me/andrezrv
+ * Version: 1.0
+ * License: GPL2
+ */
+// Load required class.
+if ( !class_exists( 'Stage_WP_Plugin' ) ) {
+ if ( defined( 'STAGE_WP_PLUGIN_LOCATION' ) && file_exists( $file = STAGE_WP_PLUGIN_LOCATION ) ) {
+ require( $file );
+ } elseif ( file_exists( $file = dirname( __FILE__ ) . '/stage-wp-plugin.class.php' ) ) {
+ require( $file );
+ }
+}
+
+// Once class is loaded, extend it.
+if ( class_exists( 'Stage_WP_Plugin' ) ) {
+
+ class Stage_WP_Staging_Plugin extends Stage_WP_Plugin {
+
+ public static $instance;
+
+ public function __construct() {
+
+ self::$instance = $this;
+
+ if ( !defined( 'WP_STAGE' ) || WP_STAGE !== 'staging' || !defined( 'STAGING_DOMAIN' ) ) {
+ return;
+ }
+
+ $this->hook( 'option_home', 'replace_domain' );
+ $this->hook( 'option_siteurl', 'replace_domain' );
+
+ }
+
+ public function replace_domain ( $url ) {
+ $current_domain = parse_url( $url, PHP_URL_HOST );
+ $url = str_replace( '//' . $current_domain, '//' . STAGING_DOMAIN, $url );
+ return $url;
+ }
+ }
+
+ new Stage_WP_Staging_Plugin;
+
+}