├── .gitignore ├── README.md ├── deploy.php ├── lib ├── command.php ├── puller.php └── pusher.php └── test.php /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demental/wp-deploy-flow/0420871071f4ae93f852b5c75ac2bbc7a90a5c11/.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wp-deploy-flow 2 | 3 | 4 | A wp-cli command to deploy your wordpress instance. 5 | 6 | ## Dependencies 7 | 8 | 9 | * Wordpress 10 | * wp-cli : http://wp-cli.org 11 | * rsync 12 | 13 | ## Install - Usage 14 | 15 | 16 | Check here : http://demental.info/blog/2013/04/09/a-decent-wordpress-deploy-workflow/ 17 | 18 | ### Setting up environments 19 | 20 | You can create as many environments as you want, all the environments must be setup in your wp-config.php file, with prefixed constants. 21 | 22 | For example if you want to create a staging environment, create all the necessary constants to configure it such as : STAGING_DB_HOST, STAGING_DB_USER, STAGING_URL and so on .... And copy all those configuration constats to all the other environments you want to interact with. 23 | 24 | ### Available constants: 25 | 26 | 27 | #### [ENV]_DB_HOST / USER / NAME / PORT / PASSWORD 28 | * Database dsn for the environment 29 | * _Mandatory_: Yes except for port (default 3306) 30 | 31 | #### [ENV]_SSH_DB_HOST / USER / PATH / PORT 32 | * If you need to connect to the destination database through SSH 33 | * _Mandatory_: No, port defaults to 22 34 | 35 | #### [ENV]_SSH_HOST / USER / PORT 36 | * SSH host to sync with Rsync 37 | * _Mandatory_: No, port defaults to 22 38 | 39 | #### [ENV]_PATH 40 | * Server path for the environment (used to reconfigure the Wordpress database) 41 | * _Mandatory_: Yes 42 | 43 | #### [ENV]_URL 44 | * Url of the Wordpress install for this environment (used to reconfigure the Wordpress database) 45 | * _Mandatory_: Yes 46 | 47 | #### [ENV]_EXCLUDES 48 | * Add files to exclude from rsync (a good idea is - temporarily I hope - to remove .htaccess to avoid manual flush rewrite). List must be separated buy semicolons. 49 | * _Mandatory_: No 50 | 51 | 52 | ### Local deployment 53 | 54 | wp-deploy-flow command is a nice tool to have a draft copy of your website, play with your draft, do whatever mistake and roll back from production to staging, or for preparing a big update and deploy in a snap. 55 | Although it's best to have separate servers for each environments, you still can have your draft environment on the same HTTP server, in a subfolder or a subdomain. 56 | For same-server environments, the configuration is much simpler : you just need to fill the PATH, URL, DB_HOST / USER / NAME / PASSWORD for each environment, SSH will not be used in this case. 57 | If one environemnt is in a subfolder of the other, it will be automatically excluded from rsync copy to avoid infinite recursion. 58 | 59 | ### Usage 60 | 61 | wp-deploy-flow comes includes: 62 | * four subcommands : pull, pull_files, push and push_files 63 | * one flag : --dry-run as you can guess this flag allows you to see what SSH commands will be executed before actually launching them. 64 | 65 | All subcommands have the same signature : 66 | 67 | ``` 68 | wp deploy [--dry-run] 69 | ``` 70 | 71 | 72 | ### Testing 73 | 74 | 75 | Shame on me... No automated tests, this is manually tested, but I recently redesigned the code so it should be easier now to cover the project (at least the core classes : puller and pusher). 76 | 77 | If you want to contribute, be kind, send a PR I will be happy to review and merge ! -------------------------------------------------------------------------------- /deploy.php: -------------------------------------------------------------------------------- 1 | [--dry-run] 17 | */ 18 | public function push( $args = array(), $flags = array() ) { 19 | $this->_push_command('commands', $args, $flags); 20 | } 21 | 22 | /** 23 | * Push local to remote, only filesystem 24 | * 25 | * @synopsis [--dry-run] 26 | */ 27 | public function push_files( $args = array(), $flags = array() ) { 28 | $this->_push_command('commands_for_files', $args, $flags); 29 | } 30 | 31 | /** 32 | * Pull local from remote, both system and database 33 | * 34 | * @synopsis [--dry-run] 35 | */ 36 | public function pull( $args = array(), $flags = array() ) { 37 | $this->_pull_command('commands', $args, $flags); 38 | } 39 | 40 | /** 41 | * Pull local from remote, only filesystem 42 | * 43 | * @synopsis [--dry-run] 44 | */ 45 | public function pull_files( $args = array(), $flags = array() ) { 46 | $this->_pull_command('commands_for_files', $args, $flags); 47 | } 48 | 49 | protected function _push_command($command_name, $args, $flags) 50 | { 51 | $this->params = self::_prepare_and_extract( $args ); 52 | $this->flags = $flags; 53 | extract($this->params); 54 | 55 | if ( $locked === true ) { 56 | return WP_CLI::error( "$env environment is locked, you cannot push to it" ); 57 | } 58 | require 'pusher.php'; 59 | 60 | $reflectionMethod = new ReflectionMethod('WP_Deploy_Flow_Pusher', $command_name); 61 | $commands = $reflectionMethod->invoke(new WP_Deploy_Flow_Pusher($this->params)); 62 | 63 | $this->_execute_commands($commands, $args); 64 | } 65 | 66 | protected function _pull_command($command_name, $args, $flags) 67 | { 68 | $this->params = self::_prepare_and_extract( $args ); 69 | $this->flags = $flags; 70 | extract($this->params); 71 | 72 | $const = strtoupper( ENVIRONMENT ) . '_LOCKED'; 73 | if ( constant( $const ) === true ) { 74 | return WP_CLI::error( ENVIRONMENT . ' env is locked, you can not pull to your local copy' ); 75 | } 76 | 77 | require 'puller.php'; 78 | 79 | $reflectionMethod = new ReflectionMethod('WP_Deploy_Flow_Puller', $command_name); 80 | $commands = $reflectionMethod->invoke(new WP_Deploy_Flow_Puller($this->params)); 81 | 82 | $this->_execute_commands($commands, $args); 83 | } 84 | 85 | protected function _execute_commands($commands) 86 | { 87 | if($this->flags['dry-run']) { 88 | WP_CLI::line('DRY RUN....'); 89 | } 90 | 91 | foreach ( $commands as $command_info ) { 92 | list( $command, $exit_on_error ) = $command_info; 93 | WP_CLI::line( $command ); 94 | if(!$this->flags['dry-run']) WP_CLI::launch( $command, $exit_on_error ); 95 | } 96 | } 97 | 98 | protected static function _prepare_and_extract( $args ) { 99 | $out = array(); 100 | self::$_env = $args[0]; 101 | $errors = self::_validate_config(); 102 | if ( $errors !== true ) { 103 | foreach ( $errors as $error ) { 104 | WP_Cli::error( $error ); 105 | } 106 | return false; 107 | } 108 | $out = self::config_constants_to_array(); 109 | $out['env'] = self::$_env; 110 | $out['db_user'] = escapeshellarg( $out['db_user'] ); 111 | $out['db_host'] = escapeshellarg( $out['db_host'] ); 112 | $out['db_password'] = escapeshellarg( $out['db_password'] ); 113 | $out['ssh_port'] = ( isset($out['ssh_port']) ) ? intval( $out['ssh_port']) : 22; 114 | $out['excludes'] = explode(':', $out['excludes']); 115 | return $out; 116 | } 117 | 118 | protected static function _validate_config() { 119 | $errors = array(); 120 | foreach ( array( 'path', 'url', 'db_host', 'db_user', 'db_name', 'db_password' ) as $postfix ) { 121 | $required_constant = self::config_constant( $postfix ); 122 | if ( ! defined( $required_constant ) ) { 123 | $errors[] = "$required_constant is not defined"; 124 | } 125 | } 126 | if ( count( $errors ) == 0 ) return true; 127 | return $errors; 128 | } 129 | 130 | public static function config_constant( $postfix ) { 131 | return strtoupper( self::$_env.'_'.$postfix ); 132 | } 133 | 134 | protected static function config_constants_to_array() { 135 | $out = array(); 136 | foreach ( array( 'locked', 'path', 'ssh_db_path', 'url', 'db_host', 'db_user', 'db_port', 'db_name', 'db_password', 'ssh_db_host', 'ssh_db_user', 'ssh_db_path', 'ssh_host', 'ssh_user', 'ssh_port', 'excludes' ) as $postfix ) { 137 | $out[$postfix] = defined( self::config_constant( $postfix ) ) ? constant( self::config_constant( $postfix ) ) : null; 138 | } 139 | return $out; 140 | } 141 | 142 | private static function _trim_url( $url ) { 143 | 144 | /** In case scheme relative URI is passed, e.g., //www.google.com/ */ 145 | $url = trim( $url, '/' ); 146 | 147 | /** If scheme not included, prepend it */ 148 | if ( ! preg_match( '#^http(s)?://#', $url ) ) { 149 | $url = 'http://' . $url; 150 | } 151 | 152 | $url_parts = parse_url( $url ); 153 | 154 | /** Remove www. */ 155 | $domain = preg_replace( '/^www\./', '', $url_parts['host'] ); 156 | 157 | return $domain; 158 | } 159 | 160 | /** 161 | * Help function for this command 162 | */ 163 | public static function help() { 164 | WP_CLI::line( <<params = $params; 8 | } 9 | 10 | public function commands() 11 | { 12 | $commands = array(); 13 | 14 | if($this->params['ssh_db_host']) { 15 | $this->_commands_for_database_import_thru_ssh($commands); 16 | } else { 17 | $this->_commands_for_database_import_locally($commands); 18 | } 19 | $this->_commands_for_database_dump($commands); 20 | 21 | $commands[]= array('rm dump.sql', true); 22 | 23 | $this->_commands_for_files( $commands ); 24 | return $commands; 25 | } 26 | 27 | public function commands_for_files() { 28 | $commands = array(); 29 | $this->_commands_for_files($commands); 30 | return $commands; 31 | } 32 | 33 | protected function _commands_for_files(&$commands) { 34 | extract( $this->params ); 35 | 36 | $dir = wp_upload_dir(); 37 | $dist_path = constant( WP_Deploy_Flow_Command::config_constant( 'path' ) ) . '/'; 38 | $remote_path = $dist_path; 39 | $local_path = ABSPATH; 40 | 41 | $excludes = array_merge( 42 | $excludes, 43 | array( 44 | '.git', 45 | '.sass-cache', 46 | 'wp-content/cache', 47 | 'wp-content/_wpremote_backups', 48 | 'wp-config.php', 49 | ) 50 | ); 51 | 52 | if(!$ssh_host) { 53 | // in case the source env is in a subfolder of the destination env, we exclude the relative path to the source to avoid infinite loop 54 | $remote_local_path = realpath($local_path); 55 | if($remote_local_path) { 56 | $remote_path = realpath($remote_path); 57 | $remote_local_path = str_replace($remote_path . '/', '', $remote_local_path); 58 | $excludes[]= $remote_locale_path; 59 | } 60 | } 61 | $excludes = array_reduce( $excludes, function($acc, $value) { $acc.= "--exclude \"$value\" "; return $acc; } ); 62 | 63 | if ( $ssh_host ) { 64 | $commands[]= array("rsync -avz -e 'ssh -p $ssh_port' $ssh_user@$ssh_host:$remote_path $local_path $excludes", true); 65 | } else { 66 | $commands[]= array("rsync -avz $remote_path $local_path $excludes", true); 67 | } 68 | } 69 | 70 | protected function _commands_for_database_import_thru_ssh(&$commands) 71 | { 72 | extract( $this->params ); 73 | $host = $db_host . ':' . $db_port; 74 | 75 | $dist_path = constant( WP_Deploy_Flow_Command::config_constant( 'path' ) ) . '/'; 76 | $commands[]= array("ssh $ssh_user@$ssh_host -p $ssh_port \"cd $dist_path;wp db export dump.sql;\"", true); 77 | $commands[]= array("scp $ssh_user@$ssh_host:$dist_path/dump.sql .", true); 78 | $commands[]= array("ssh $ssh_user@$ssh_host -p $ssh_port \"cd $dist_path; rm dump.sql;\"", true); 79 | } 80 | 81 | protected function _commands_for_database_import_locally(&$commands) 82 | { 83 | extract( $this->params ); 84 | 85 | $host = $db_host . ':' . $db_port; 86 | $wpdb = new wpdb( $db_user, $db_password, $db_name, $host ); 87 | $path = ABSPATH; 88 | $url = get_bloginfo( 'url' ); 89 | $dist_path = constant( WP_Deploy_Flow_Command::config_constant( 'path' ) ) . '/'; 90 | $commands[]= array("wp migrate to $path $url dump.sql", true); 91 | } 92 | 93 | protected function _commands_for_database_dump(&$commands) { 94 | extract( $this->params ); 95 | $commands[]= array('wp db export db_bk.sql', true); 96 | 97 | $commands[]= array('wp db import dump.sql', true); 98 | 99 | $siteurl = get_option( 'siteurl' ); 100 | $searchreplaces = array($url => $siteurl, untrailingslashit( $path ) => untrailingslashit( ABSPATH )); 101 | 102 | foreach($searchreplaces as $search => $replace) { 103 | $commands[]= array( "wp search-replace $search $replace", true ); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /lib/pusher.php: -------------------------------------------------------------------------------- 1 | params = $params; 8 | } 9 | 10 | public function commands() 11 | { 12 | $commands = array(); 13 | $this->_commands_for_database_dump($commands); 14 | 15 | if($this->params['ssh_db_host']) { 16 | $this->_commands_for_database_import_thru_ssh($commands); 17 | } else { 18 | $this->_commands_for_database_import_locally($commands); 19 | } 20 | 21 | $commands[]= array('rm dump.sql', true); 22 | 23 | $this->_commands_for_files( $commands ); 24 | $this->_commands_post_push($commands); 25 | return $commands; 26 | } 27 | 28 | public function commands_for_files() { 29 | $commands = array(); 30 | $commands[]= array('rm dump.sql', true); 31 | $this->_commands_for_files( $commands ); 32 | $this->_commands_post_push( $commands ); 33 | return $commands; 34 | 35 | } 36 | 37 | protected function _commands_for_files(&$commands) { 38 | extract( $this->params ); 39 | 40 | $remote_path = $path . '/'; 41 | $local_path = ABSPATH; 42 | $excludes = array_merge( 43 | $excludes, 44 | array( 45 | '.git', 46 | '.sass-cache', 47 | 'wp-content/cache', 48 | 'wp-content/_wpremote_backups', 49 | 'wp-config.php', 50 | ) 51 | ); 52 | if(!$ssh_host) { 53 | // in case the destination env is in a subfolder of the source env, we exclude the relative path to the destination to avoid infinite loop 54 | $local_remote_path = realpath($remote_path); 55 | if($local_remote_path) { 56 | 57 | $local_path = realpath($local_path) . '/'; 58 | $local_remote_path = str_replace($local_path . '/', '', $local_remote_path); 59 | $excludes[]= $local_remote_path; 60 | $remote_path = realpath($remote_path). '/'; 61 | } 62 | } 63 | $excludes = array_reduce( $excludes, function($acc, $value) { $acc.= "--exclude \"$value\" "; return $acc; } ); 64 | 65 | if ( $ssh_host ) { 66 | $command = "rsync -avz -e 'ssh -p $ssh_port' --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r $local_path $ssh_user@$ssh_host:$remote_path $excludes"; 67 | } else { 68 | $command = "rsync -avz $local_path $remote_path $excludes"; 69 | } 70 | $commands[]= array($command, true); 71 | } 72 | 73 | protected function _commands_post_push(&$commands) { 74 | extract( $this->params ); 75 | $const = strtoupper( $env ) . '_POST_SCRIPT'; 76 | if ( defined( $const ) ) { 77 | $subcommand = constant( $const ); 78 | $commands[]= array("ssh $ssh_user@$ssh_host -p $ssh_port \"$subcommand\"", true); 79 | } 80 | } 81 | 82 | protected function _commands_for_database_import_thru_ssh(&$commands) 83 | { 84 | extract( $this->params ); 85 | $commands[]= array( "scp -P $ssh_port dump.sql $ssh_db_user@$ssh_db_host:$ssh_db_path", true ); 86 | $commands[]= array( "ssh $ssh_db_user@$ssh_db_host -p $ssh_port \"cd $ssh_db_path; mysql --user=$db_user --password=$db_password --host=$db_host $db_name < dump.sql; rm dump.sql\"", true ); 87 | } 88 | 89 | protected function _commands_for_database_import_locally(&$commands) 90 | { 91 | extract( $this->params ); 92 | $commands[]= array( "mysql --user=$db_user --password=$db_password --host=$db_host $db_name < dump.sql;", true ); 93 | } 94 | 95 | protected function _commands_for_database_dump(&$commands) 96 | { 97 | extract( $this->params ); 98 | 99 | $siteurl = get_option( 'siteurl' ); 100 | $searchreplaces = array($siteurl => $url, untrailingslashit( ABSPATH ) => untrailingslashit( $path )); 101 | $commands = array( 102 | array( 'wp db export db_bk.sql', true ) 103 | ); 104 | foreach($searchreplaces as $search => $replace) { 105 | $commands[]= array( "wp search-replace $search $replace", true ); 106 | } 107 | $commands[]= array( 'wp db dump dump.sql', true ); 108 | $commands[]= array( 'wp db import db_bk.sql', true ); 109 | $commands[]= array( 'rm db_bk.sql', true ); 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 |