├── .gitignore ├── README.md ├── composer.json ├── deploy.php ├── helpers.php └── runner.php /.gitignore: -------------------------------------------------------------------------------- 1 | php_error.log 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Project maintainers welcomed 2 | *April, 2016* 3 | 4 | It just so happened that in the last year, my development interests shifted away from WordPress. As such, I ended up not writing a single line of PHP in over twelve months. 5 | 6 | I believe that the tools we build are best when we use them ourselves, and the reality is I don't use `wp-cli-deploy` anymore, so I decided to hand off maintenance of this project to someone else. 7 | 8 | If you’re interested in the gig, ping [me on Twitter](http://twitter.com/ciobi) and we can take it from there. 9 | 10 | ### Update from new project maintainer 11 | *May, 2018* 12 | 13 | I look over this project thinking it would be something I could maintain, but I have recently been using different tools and have left wp-cli-deploy to gather dust. I do plan to incorporate the new tools into the plugin and bring it back up to speed at some point soon. 14 | 15 | In the mean time, if someone else wants to take the reigns, you can contact [robrecord on Twitter](http://twitter.com/robrecord) to discuss. 16 | 17 | 18 | ## WP-Cli Deploy 19 | 20 | __Current Version__: 1.0.0 (stable), 1.1.0-beta (in dev) 21 | 22 | Deploys the local WordPress database or uploads directory. 23 | 24 | The tool requires defining a set of constants in your wp-config.php file. 25 | The constants should be prefixed with the environment handle which you will use as the first parameter for your desired subcommand. An example configuration for a "dev" environment: 26 | 27 | ```php 28 | `wp deploy push dev ...` 46 | 47 | Not all commands / subcommands require all constants to be defined. To test what 48 | a subcommand requires, execute it with a non-existing environment handle. e.g. 49 | `wp deploy dump johndoe`. 50 | 51 | You can define as many constant groups as deployment enviroments you wish to have. 52 | 53 | __Examples__ 54 | 55 | # Deploy the local db to the staging environment 56 | wp deploy push staging --what=db 57 | 58 | # Pull both the production database and uploads 59 | wp deploy pull production --what=db && wp deploy pull production --what=uploads 60 | 61 | # Pull both the production themes and plugins 62 | wp deploy pull production --what=themes && wp deploy pull production --what=plugins 63 | 64 | # Dump the local db with the siteurl replaced 65 | wp deploy dump andrew 66 | 67 | ### Installation 68 | 69 | * Clone this repository in your WordPress directory. 70 | * Create a `wp-cli.yml` file in the root of you WordPress directory with: 71 | ```yml 72 | require: "relative/path/to/deploy.php" 73 | ``` 74 | * You can now use the deploy command. Type `wp help deploy` to see if it 75 | works. 76 | 77 | ### Configuration 78 | 79 | In order to be able to use the deploy command, you need to define certain 80 | constants in your `wp-config.php` file. 81 | 82 | #### Configuration Dependecies 83 | 84 | Subcommands depend on different constants in order to work. 85 | Here's the dependency list: 86 | 87 | * __`wp deploy push`__: In order to push to your server, you need to define the 88 | ssh credentials, and a path to a writable directory on the server. _These 89 | constants are needed whatever the arguments passed to the `push` subcommand_: 90 | * `%%ENV%%_USER` 91 | * `%%ENV%%_HOST` 92 | * `%%ENV%%_PATH` 93 | 94 | * __`wp deploy push %%env%% --what=db`__: In order to deploy the database to your 95 | server, you need to define the url of your WordPress website, the path to 96 | the WordPress code on your server, and the credentials to the database on 97 | the server: 98 | * `%%ENV%%_URL` 99 | * `%%ENV%%_WP_PATH` 100 | * `%%ENV%%_DB_HOST` 101 | * `%%ENV%%_DB_NAME` 102 | * `%%ENV%%_DB_USER` 103 | * `%%ENV%%_DB_PASSWORD` 104 | 105 | * __`wp deploy push %%env%% --what=uploads`__: In order to push the uploads directory, 106 | you need to define the path to the uploads directory on your server: 107 | * `%%ENV%%_UPLOADS_PATH` 108 | 109 | __`wp deploy pull`__: In order to pull to your server, you need to define the 110 | sh credentials constants. _These constants are needed whatever the arguments 111 | assed to the `pull` subcommand_: 112 | * `%%ENV%%_USER` 113 | * `%%ENV%%_HOST` 114 | 115 | * __`wp deploy pull %%env%% --what=db`__: In order to pull the database to from your 116 | server, you need to define the url of your remote WordPress website, the 117 | path to the WordPress code on your server, and the credentials to the 118 | database on the server: 119 | * `%%ENV%%_PATH` 120 | * `%%ENV%%_URL` 121 | * `%%ENV%%_WP_PATH` 122 | * `%%ENV%%_DB_HOST` 123 | * `%%ENV%%_DB_NAME` 124 | * `%%ENV%%_DB_USER` 125 | * `%%ENV%%_DB_PASSWORD` 126 | 127 | * __`wp deploy push %%env%% --what=uploads`__: As in the `push` command's case, in 128 | order to pull the remote server uploads, we need their path on the server. 129 | * `%%ENV%%_UPLOADS_PATH` 130 | 131 | * __`wp deploy push %%env%% --what=themes`__: As in the `push` command's case, in 132 | order to pull the remote server themes, we need their path on the server. 133 | * `%%ENV%%_THEMES_PATH` 134 | 135 | * __`wp deploy push %%env%% --what=plugins`__: As in the `push` command's case, in 136 | order to pull the remote server plugins, we need their path on the server. 137 | * `%%ENV%%_PLUGINS_PATH` 138 | 139 | * __`wp dump %%env%%`__: This subcommand only requires the path to the target 140 | WordPress path and its URL. 141 | 142 | #### `%%ENV%%_POST_HOOK` 143 | 144 | You can __optionally__ define a constant with bash code which is called at the 145 | end of the subcommand execution. 146 | 147 | You can refer to environment variables using placeholders. Some of the 148 | available environment variables are: 149 | * `env`: The environment handle 150 | * `command`: The subcommand (Currently `push`, `pull`, or `dump`). 151 | * `what`: The what argument value for the `push` or `pull` subcommand. 152 | * `wd`: The path to the working directory for the deploy command. This is 153 | the directory where the database is pulled, and other temporary files are 154 | created. 155 | * `timestamp`: The date formatted with "Y_m_d-H_i" 156 | * `tmp_path`: The path to the temporary files directory used by the deploy 157 | tool. 158 | * `bk_path`: The path to the backups directory used by the deploy tool. 159 | * `local_uploads`: The path to the local WordPress instance uploads 160 | directory. 161 | * `ssh`: The ssh server handle in the `user@host` format. 162 | 163 | 164 | __Example__ 165 | 166 | Here's an example of a `DEV_POST_HOOK` that posts a message to a hipchat 167 | room after a `pull` or a `push` is performed using the HipChat REST API 168 | (https://github.com/hipchat/hipchat-cli). 169 | For pushes, it also clears the cache. 170 | 171 | ```php 172 | =5.4", 17 | "wp-cli/wp-cli": ">=0.14" 18 | }, 19 | "autoload": { 20 | "files": [ "deploy.php" ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /deploy.php: -------------------------------------------------------------------------------- 1 | `wp deploy push dev ...` 35 | * 36 | * Not all commands / subcommands require all constants to be defined. To test what 37 | * a subcommand requires, execute it with a non-existing environment handle. e.g. 38 | * `wp deploy dump johndoe`. 39 | * 40 | * You can define as many constant groups as deployment eviroments you wish to have. 41 | * 42 | * __Examples__ 43 | * 44 | * # Deploy the local db to the staging environment 45 | * wp deploy push staging --what=db 46 | * 47 | * # Pull both the production database and uploads 48 | * wp deploy pull production --what=db && wp deploy pull production --what=uploads 49 | * 50 | * # Dump the local db with the siteurl replaced 51 | * wp deploy dump andrew 52 | * 53 | * ### Configuration 54 | * 55 | * In order to be able to use the deploy command, you need to define certain 56 | * constants in your `wp-config.php` file. 57 | * 58 | * #### Configuration Dependecies 59 | * 60 | * Subcommands depend on different constants in order to work. 61 | * Here's the dependency list: 62 | * 63 | * * __`wp deploy push`__: In order to push to your server, you need to define the 64 | * ssh credentials, and a path to a writable directory on the server. _These 65 | * constants are needed whatever the arguments passed to the `push` subcommand_: 66 | * * `%%ENV%%_USER` 67 | * * `%%ENV%%_HOST` 68 | * * `%%ENV%%_PATH` 69 | * 70 | * * __`wp deploy push %%env%% --what=db`__: In order to deploy the database to your 71 | * server, you need to define the url of your WordPress website, the path to 72 | * the WordPress code on your server, and the credentials to the database on 73 | * the server: 74 | * * `%%ENV%%_URL` 75 | * * `%%ENV%%_WP_PATH` 76 | * * `%%ENV%%_DB_HOST` 77 | * * `%%ENV%%_DB_NAME` 78 | * * `%%ENV%%_DB_USER` 79 | * * `%%ENV%%_DB_PASSWORD` 80 | * 81 | * * __`wp deploy push %%env%% --what=uploads`__: In order to push the uploads directory, 82 | * you need to define the path to the uploads directory on your server: 83 | * * `%%ENV%%_UPLOADS_PATH` 84 | * 85 | * __`wp deploy pull`__: In order to pull to your server, you need to define the 86 | * sh credentials constants. _These constants are needed whatever the arguments 87 | * assed to the `pull` subcommand_: 88 | * * `%%ENV%%_USER` 89 | * * `%%ENV%%_HOST` 90 | * 91 | * * __`wp deploy pull %%env%% --what=db`__: In order to pull the database to from your 92 | * server, you need to define the url of your remote WordPress website, the 93 | * path to the WordPress code on your server, and the credentials to the 94 | * database on the server: 95 | * * `%%ENV%%_PATH` 96 | * * `%%ENV%%_URL` 97 | * * `%%ENV%%_WP_PATH` 98 | * * `%%ENV%%_DB_HOST` 99 | * * `%%ENV%%_DB_NAME` 100 | * * `%%ENV%%_DB_USER` 101 | * * `%%ENV%%_DB_PASSWORD` 102 | * 103 | * * __`wp deploy push %%env%% --what=uploads`__: As in the `push` command's case, in 104 | * order to pull the remote server uploads, we need their path on the server. 105 | * * `%%ENV%%_UPLOADS_PATH` 106 | * 107 | * * __`wp dump %%env%%`__: This subcommand only requires the path to the target 108 | * WordPress path and its URL. 109 | * 110 | * #### `%%ENV%%_POST_HOOK` 111 | * 112 | * You can __optionally__ define a constant with bash code which is called at the 113 | * end of the subcommand execution. 114 | * 115 | * You can refer to environment variables using placeholders. Some of the 116 | * available environment variables are: 117 | * * `env`: The environment handle 118 | * * `command`: The subcommand (Currently `push`, `pull`, or `dump`). 119 | * * `what`: The what argument value for the `push` or `pull` subcommand. 120 | * * `wd`: The path to the working directory for the deploy command. This is 121 | * the directory where the database is pulled, and other temporary files are 122 | * created. 123 | * * `timestamp`: The date formatted with "Y_m_d-H_i" 124 | * * `tmp_path`: The path to the temporary files directory used by the deploy 125 | * tool. 126 | * * `bk_path`: The path to the backups directory used by the deploy tool. 127 | * * `local_uploads`: The path to the local WordPress instance uploads 128 | * directory. 129 | * * `ssh`: The ssh server handle in the `user@host` format. 130 | * 131 | * 132 | * __Example__ 133 | * 134 | * Here's an example of a `DEV_POST_HOOK` that posts a message to a hipchat 135 | * room after a `pull` or a `push` is performed using the HipChat REST API 136 | * (https://github.com/hipchat/hipchat-cli). 137 | * For pushes, it also clears the cache. 138 | * 139 | * ```php 140 | * array( 186 | 'global' => array( 187 | 'user', 188 | 'host', 189 | 'path', 190 | ), 191 | 'db' => array( 192 | 'url', 193 | 'wp_path', 194 | 'db_host', 195 | 'db_name', 196 | 'db_user', 197 | 'db_password', 198 | ), 199 | 'uploads' => array( 'uploads_path' ), 200 | 'themes' => array( 'themes_path' ), 201 | 'plugins' => array( 'plugins_path' ) 202 | ), 203 | 'pull' => array( 204 | 'global' => array( 205 | 'user', 206 | 'host', 207 | ), 208 | 'db' => array( 209 | 'path', 210 | 'url', 211 | 'wp_path', 212 | 'db_host', 213 | 'db_name', 214 | 'db_user', 215 | 'db_password', 216 | ), 217 | 'uploads' => array( 'uploads_path' ), 218 | 'themes' => array( 'themes_path' ), 219 | 'plugins' => array( 'plugins_path' ) 220 | ), 221 | 'dump' => array( 222 | 'wp_path', 223 | 'url' 224 | ), 225 | 'optional' => array( 226 | 'port', 227 | 'post_hook', 228 | 'excludes' 229 | ) 230 | ); 231 | 232 | /** 233 | * Depending paths need to be under the 234 | * paths they depend on. 235 | */ 236 | self::$config = array( 237 | 'env' => '%%env%%', 238 | 239 | /** Constants which refer to remote. */ 240 | 'host' => '%%host%%', 241 | 'user' => '%%user%%', 242 | 'path' => '%%path%%', 243 | 'url' => '%%url%%', 244 | 'wp' => '%%wp_path%%', 245 | 'uploads' => '%%uploads_path%%', 246 | 'themes' => '%%themes_path%%', 247 | 'plugins' => '%%plugins_path%%', 248 | 'db_host' => '%%db_host%%', 249 | 'db_name' => '%%db_name%%', 250 | 'db_user' => '%%db_user%%', 251 | 'db_password' => '%%db_password%%', 252 | 253 | /** Optional */ 254 | 'port' => '%%port%%', 255 | 'post_hook' => '%%post_hook%%', 256 | 'safe_mode' => '%%safe_mode%%', /** TODO */ 257 | 'excludes' => '%%excludes%%', 258 | 259 | /** Helpers which refer to local. */ 260 | 'command' => '%%command%%', 261 | 'what' => '%%what%%', 262 | 'abspath' => '%%abspath%%', 263 | 'wd' => '%%abspath%%/%%env%%_%%hash%%', 264 | 'timestamp' => '%%pretty_date%%', 265 | 'tmp_path' => '%%wd%%/tmp', 266 | 'bk_path' => '%%wd%%/bk', 267 | 'tmp' => '%%tmp_path%%/%%rand%%', 268 | 'local_hostname' => '%%hostname%%', 269 | 'ssh' => '%%user%%@%%host%%', 270 | 'local_uploads' => '%%local_uploads%%', 271 | 'local_themes' => '%%local_themes%%', 272 | 'local_plugins' => '%%local_plugins%%', 273 | 'siteurl' => '%%siteurl%%', 274 | ); 275 | } 276 | 277 | /** 278 | * Displays information about the current version of the deploy command. 279 | * 280 | * ## EXAMPLE 281 | * 282 | * wp deploy info 283 | */ 284 | public function info( $args, $assoc_args ) { 285 | 286 | WP_CLI::line( 'WP-Cli Deploy Command: https://github.com/c10b10/wp-cli-deploy' ); 287 | WP_CLI::line( 'Supported subcommands: push, pull, dump' ); 288 | WP_CLI::line( 'Version: 1.1.0-alpha' ); 289 | WP_CLI::line( 'Author: Alex Ciobica / @ciobi' ); 290 | WP_CLI::line( 'Run "wp help deploy" for the documentation' ); 291 | } 292 | 293 | /** 294 | * Pushes the local database and / or uploads from local to remote. 295 | * 296 | * ## OPTIONS 297 | * 298 | * 299 | * : The handle of of the environment. This is the prefix of the constants 300 | * defined in wp-config. 301 | * 302 | * `--what`= 303 | * : What needs to be deployed on the server. Valid options are: 304 | * db: pushes the database to the remote server 305 | * uploads: pushes the uploads to the remote server 306 | * 307 | * [`--v`=] 308 | * : Verbosity level. Default 1. 0 is highest and 2 is lowest. 309 | * 310 | * ## EXAMPLE 311 | * 312 | * # Push the database and the uploads for to "staging" environment. 313 | * # You must have STAGING_* constants defined for this to work. 314 | * 315 | * wp deploy push staging --what=db,uploads 316 | * 317 | * @synopsis --what= [--v=] 318 | */ 319 | public function push( $args, $assoc_args ) { 320 | 321 | $args = self::sanitize_args( __FUNCTION__, $args, $assoc_args ); 322 | 323 | if ( isset( $args->error ) ) { 324 | WP_Cli::line( $args->error ); 325 | return false; 326 | } 327 | 328 | call_user_func( __CLASS__ . "::push_" . self::$config->what ); 329 | 330 | self::run_post_hook(); 331 | 332 | self::wow(); 333 | } 334 | 335 | /** 336 | * Pulls the database and / or uploads from remote to local. After pulling 337 | * the uploads, they need to copied to the correct location. 338 | * 339 | * 340 | * : The name of the environment. This is the prefix of the constants defined in 341 | * wp-config. 342 | * 343 | * `--what`= 344 | * : What needs to be pulled. Valid options are: 345 | * db: pushes the database to the remote server 346 | * uploads: pushes the uploads to the remote server 347 | * 348 | * [`--v`=] 349 | * : Verbosity level. Default 1. 0 is highest and 2 is lowest. 350 | * 351 | * ## EXAMPLES 352 | * 353 | * # Pulls database and uploads folder 354 | * wp deploy pull staging --what=db,uploads 355 | * 356 | * # Pull the remote db without prior local backup 357 | * wp deploy pull staging --what=db --backup=false 358 | * 359 | * @synopsis --what= [--cleanup] [--backup=] [--v=] 360 | */ 361 | public function pull( $args, $assoc_args ) { 362 | 363 | $args = self::sanitize_args( __FUNCTION__, $args, $assoc_args ); 364 | 365 | if ( isset( $args->error ) ) { 366 | WP_Cli::line( $args->error ); 367 | return false; 368 | } 369 | 370 | call_user_func( __CLASS__ . "::pull_" . self::$config->what ); 371 | 372 | self::run_post_hook(); 373 | 374 | self::wow(); 375 | } 376 | 377 | /** 378 | * Dumps the local database and / or uploads from local to remote. The 379 | * database will be prepared for upload to the specified environment. 380 | * 381 | * ## OPTIONS 382 | * 383 | * 384 | * : The name of the environment. This is the prefix of the constants 385 | * defined in wp-config.php. 386 | * 387 | * [`--v`=] 388 | * : Verbosity level. Default 1. 0 is highest and 2 is lowest. 389 | * 390 | * ## EXAMPLE 391 | * 392 | * # Dumps database for to "staging" environment. 393 | * wp deploy dump staging 394 | * 395 | * @synopsis [--file=] [--v=] 396 | */ 397 | public function dump( $args, $assoc_args ) { 398 | 399 | $args = self::sanitize_args( __FUNCTION__, $args, $assoc_args ); 400 | 401 | if ( isset( $args->error ) ) { 402 | WP_Cli::line( $args->error ); 403 | return false; 404 | } 405 | 406 | self::dump_db(); 407 | 408 | self::run_post_hook(); 409 | 410 | self::wow(); 411 | } 412 | 413 | /** Pushes the database to the server. */ 414 | private function push_db() { 415 | 416 | $c = self::$config; 417 | 418 | $dump_file = self::dump_db( array( 419 | 'wd' => $c->tmp_path, 420 | 'name' => basename( $c->tmp ), 421 | ) ); 422 | $server_file = "{$c->local_hostname}_{$c->env}.sql"; 423 | 424 | $runner = self::$runner; 425 | 426 | $runner->add( 427 | Util::get_rsync( 428 | $dump_file, 429 | "$c->ssh:$c->path/$server_file", 430 | $c->port 431 | ), 432 | "Uploaded the database file to '$c->path/$server_file' on the server.", 433 | 'Failed to upload the database to the server' 434 | ); 435 | 436 | /** Removing the dump file after upload. */ 437 | $runner->add( "rm -f $dump_file" ); 438 | 439 | $runner->add( 440 | "ssh $c->ssh -p $c->port 'cd $c->path;" 441 | . " mysql --user=$c->db_user --password=" . escapeshellarg( $c->db_password ) ." --host=$c->db_host" 442 | . " $c->db_name < $server_file'", 443 | 'Deployed the database on server.', 444 | 'Failed deploying the db on server.' 445 | ); 446 | 447 | $runner->run(); 448 | } 449 | 450 | /** Pushes the uploads to the server. */ 451 | private function push_uploads() { 452 | 453 | $c = self::$config; 454 | 455 | $runner = self::$runner; 456 | 457 | /** TODO safe mode */ 458 | $path = isset( $c->safe_mode ) ? $c->path : $c->uploads; 459 | 460 | $runner->add( 461 | Util::get_rsync( 462 | // When pushing safe, we push the dir, hence no trailing slash 463 | "$c->local_uploads/", 464 | "$c->ssh:$path", 465 | $c->port, 466 | true, 467 | true, 468 | $c->excludes 469 | ), 470 | "Synced local uploads to '$path' on '$c->host'.", 471 | 'Failed to upload the database to the server' 472 | ); 473 | 474 | $runner->run(); 475 | } 476 | 477 | /** Pushes the themes to the server. */ 478 | private function push_themes() { 479 | 480 | $c = self::$config; 481 | 482 | $runner = self::$runner; 483 | 484 | /** TODO safe mode */ 485 | $path = isset( $c->safe_mode ) ? $c->path : $c->themes; 486 | 487 | $runner->add( 488 | Util::get_rsync( 489 | // When pushing safe, we push the dir, hence no trailing slash 490 | "$c->local_themes/", 491 | "$c->ssh:$path", 492 | $c->port, 493 | true, 494 | true, 495 | "$c->excludes" 496 | ), 497 | "Synced local themes to '$path' on '$c->host'.", 498 | 'Failed to upload the database to the server' 499 | ); 500 | 501 | $runner->run(); 502 | } 503 | 504 | /** Pushes the plugins to the server. */ 505 | private function push_plugins() { 506 | 507 | $c = self::$config; 508 | 509 | $runner = self::$runner; 510 | 511 | /** TODO safe mode */ 512 | $path = isset( $c->safe_mode ) ? $c->path : $c->plugins; 513 | 514 | $runner->add( 515 | Util::get_rsync( 516 | // When pushing safe, we push the dir, hence no trailing slash 517 | "$c->local_plugins/", 518 | "$c->ssh:$path", 519 | $c->port, 520 | true, 521 | true, 522 | "$c->excludes" 523 | ), 524 | "Synced local plugins to '$path' on '$c->host'.", 525 | 'Failed to upload the database to the server' 526 | ); 527 | 528 | $runner->run(); 529 | } 530 | 531 | /** Pulls the database from the server. */ 532 | private function pull_db() { 533 | 534 | $c = self::$config; 535 | 536 | $server_file = "{$c->env}_{$c->timestamp}.sql"; 537 | 538 | $runner = self::$runner; 539 | 540 | $runner->add( 541 | "ssh $c->ssh -p $c->port 'mkdir -p $c->path; cd $c->path;" 542 | . " mysqldump --user=$c->db_user --password=" . escapeshellarg( $c->db_password ) . " --host=$c->db_host" 543 | . " --single-transaction" 544 | . " --add-drop-table $c->db_name > $server_file'", 545 | "Dumped the remote database to '$c->path/$server_file' on the server.", 546 | 'Failed dumping the remote database.' 547 | ); 548 | 549 | $runner->add( 550 | Util::get_rsync( 551 | "$c->ssh:$c->path/$server_file", 552 | "$c->wd/$server_file", 553 | $c->port, 554 | false, false // No delete or compression 555 | ), 556 | "Copied the database from the server to '$c->wd/$server_file'." 557 | ); 558 | 559 | $runner->add( 560 | "ssh $c->ssh -p $c->port 'cd $c->path; rm -f $server_file'", 561 | 'Deleted the server dump.' 562 | ); 563 | 564 | /** TODO Finalize safe mode. */ 565 | $runner->add( 566 | ! isset( $c->safe_mode ), 567 | "wp db export $c->bk_path/$c->timestamp.sql", 568 | "Backed up local database to '$c->bk_path/$c->timestamp.sql'" 569 | ); 570 | 571 | $runner->add( 572 | "wp db import $c->wd/$server_file", 573 | 'Imported the remote database.' 574 | ); 575 | 576 | $runner->add( 577 | ( $c->siteurl != $c->url ), 578 | "wp search-replace --all-tables $c->url $c->siteurl", 579 | "Replaced '$c->url' with '$c->siteurl' on the imported database." 580 | ); 581 | 582 | $runner->add( 583 | ( $c->abspath != $c->wp ), 584 | "wp search-replace --all-tables $c->wp $c->abspath", 585 | "Replaced '$c->wp' with '$c->abspath' on local database." 586 | ); 587 | 588 | $runner->run(); 589 | } 590 | 591 | /** Pulls the uploads from the server. */ 592 | private static function pull_uploads() { 593 | 594 | $c = self::$config; 595 | 596 | $runner = self::$runner; 597 | 598 | /** TODO Finalize safe mode. */ 599 | $runner->add( 600 | isset( $c->safe_mode ), 601 | "cp -rf $c->local_uploads $c->bk_path/uploads_$c->timestamp", 602 | 'Backed up local uploads.' 603 | ); 604 | 605 | $runner->add( 606 | Util::get_rsync( 607 | "$c->ssh:$c->uploads/", 608 | $c->local_uploads, 609 | $c->port, 610 | true, 611 | true, 612 | $c->excludes 613 | ), 614 | "Pulled the '$c->env' uploads locally." 615 | ); 616 | 617 | 618 | $runner->run(); 619 | } 620 | 621 | /** Pulls the themes from the server. */ 622 | private static function pull_themes() { 623 | 624 | $c = self::$config; 625 | 626 | $runner = self::$runner; 627 | 628 | /** TODO Finalize safe mode. */ 629 | $runner->add( 630 | isset( $c->safe_mode ), 631 | "cp -rf $c->local_themes $c->bk_path/themes_$c->timestamp", 632 | 'Backed up local themes.' 633 | ); 634 | 635 | $runner->add( 636 | Util::get_rsync( 637 | "$c->ssh:$c->themes/", 638 | $c->local_themes, 639 | $c->port, 640 | true, 641 | true, 642 | $c->excludes 643 | ), 644 | "Pulled the '$c->env' themes locally." 645 | ); 646 | 647 | 648 | $runner->run(); 649 | } 650 | 651 | /** Pulls the plugins from the server. */ 652 | private static function pull_plugins() { 653 | 654 | $c = self::$config; 655 | 656 | $runner = self::$runner; 657 | 658 | /** TODO Finalize safe mode. */ 659 | $runner->add( 660 | isset( $c->safe_mode ), 661 | "cp -rf $c->local_plugins $c->bk_path/plugins_$c->timestamp", 662 | 'Backed up local plugins.' 663 | ); 664 | 665 | $runner->add( 666 | Util::get_rsync( 667 | "$c->ssh:$c->plugins/", 668 | $c->local_plugins, 669 | $c->port, 670 | true, 671 | true, 672 | $c->excludes 673 | ), 674 | "Pulled the '$c->env' plugins locally." 675 | ); 676 | 677 | $runner->run(); 678 | } 679 | 680 | /** Dumps the local database after performing search-replace. */ 681 | private static function dump_db( $args = array() ) { 682 | 683 | $c = self::$config; 684 | 685 | $args = wp_parse_args( $args, array( 686 | 'name' => "{$c->env}_{$c->timestamp}", 687 | 'wd' => $c->wd 688 | ) ); 689 | $path = "{$args['wd']}/{$args['name']}.sql"; 690 | 691 | $runner = self::$runner; 692 | 693 | $runner->add( 694 | ( $c->abspath != $c->wp ) || ( $c->url != $c->siteurl ), 695 | "wp db export $c->tmp", 696 | "Exported a local backup of the database to '$c->tmp'." 697 | ); 698 | 699 | $runner->add( 700 | ( $c->siteurl != $c->url ), 701 | "wp search-replace --all-tables $c->siteurl $c->url", 702 | "Replaced '$c->siteurl' with '$c->url' in local database." 703 | ); 704 | 705 | $runner->add( 706 | ( $c->abspath != $c->wp ), 707 | "wp search-replace --all-tables $c->abspath $c->wp", 708 | "Replaced '$c->abspath' with with '$c->wp' in local database." 709 | ); 710 | 711 | $runner->add( 712 | "wp db export $path", 713 | "Dumped the database to '$path'." 714 | ); 715 | 716 | $runner->add( 717 | ( $c->abspath != $c->wp ) || ( $c->url != $c->siteurl ), 718 | "wp db import $c->tmp", 719 | 'Imported the local backup.' 720 | ); 721 | 722 | $runner->add( 723 | ( $c->abspath != $c->wp ) || ( $c->url != $c->siteurl ), 724 | "rm -f $c->tmp", 725 | 'Cleaned up.' 726 | ); 727 | 728 | $runner->run(); 729 | 730 | return $path; 731 | } 732 | 733 | /** Sanitizes the arguments, and sets the configuration. */ 734 | private static function sanitize_args( $command, $args, $assoc_args = null ) { 735 | 736 | self::$env = $args[0]; 737 | 738 | /** If what is available, it needs to refer to an existing method. */ 739 | $what = ''; 740 | if ( isset( $assoc_args['what'] ) ) { 741 | $what = $assoc_args['what']; 742 | if ( ! method_exists( __CLASS__, "{$command}_{$what}" ) ) { 743 | WP_Cli::error( "Using unknown '$what' parameter for --what argument." ); 744 | } 745 | } 746 | 747 | /** 748 | * Eeeek! So ugly. 749 | * TODO. Fix this. 750 | */ 751 | $verbosity = self::$default_verbosity; 752 | if ( isset( $assoc_args['v'] ) && in_array( $assoc_args['v'], range( 0, 2 ) ) ) 753 | $verbosity = $assoc_args['v']; 754 | self::$runner = new Runner( $verbosity ); 755 | 756 | /** Get the environmental and set the tool config. */ 757 | $constants = self::validate_config( $command, $what, self::$env ); 758 | self::$config = self::expand( self::$config, $constants, $command, $what ); 759 | 760 | /** Create paths. */ 761 | Runner::get_result( 'mkdir -p ' . self::$config->tmp_path . ';' ); 762 | Runner::get_result( 'mkdir -p ' . self::$config->bk_path . ';' ); 763 | 764 | return self::$config; 765 | } 766 | 767 | /** Determines the verbosity level: 1, 2, or 3 */ 768 | private static function get_verbosity( $string, $default ) { 769 | $number = count_chars_unicode( $string, 'v' ); 770 | if ( $number ) 771 | return min( $number, 2 ); 772 | return $default; 773 | } 774 | 775 | /** 776 | * Verifies that all required constants are defined. 777 | * Constants must be of the form: "%ENV%_%NAME%" 778 | */ 779 | private static function validate_config( $command, $what, $env ) { 780 | 781 | /** Get the required contstants from the dependency array. */ 782 | $deps = self::$config_dependencies; 783 | $required = $deps[$command]; 784 | if ( ! empty( $what ) ) { 785 | $required = array_unique( array_merge( 786 | $deps[$command][$what], 787 | $deps[$command]['global'] 788 | ) ); 789 | } 790 | 791 | /** Get all definable constants. */ 792 | $all_const = array(); 793 | foreach( $deps as $comm_deps ) { 794 | foreach ( $comm_deps as $item ) { 795 | $const = is_array( $item ) ? $item : array( $item ); 796 | $all_const = array_merge( $all_const, $const ); 797 | } 798 | } 799 | $all_const = array_unique( $all_const ); 800 | 801 | $get_const = function ( $const ) use ( $env ) { 802 | return strtoupper( $env . '_' . $const ); 803 | }; 804 | 805 | $errors = array(); 806 | $constants = array(); 807 | foreach ( $all_const as $short_name ) { 808 | /** The constants template */ 809 | $constant = $get_const( $short_name ); 810 | if ( in_array( $short_name, $required ) && ! defined( $constant ) ) { 811 | $errors[] = "Required constant $constant is not defined."; 812 | } elseif ( defined( $constant ) ) { 813 | $constants[$short_name] = constant( $constant ); 814 | } 815 | } 816 | 817 | if ( count( $errors ) ) { 818 | foreach ( $errors as $error ) { 819 | WP_Cli::line( "$error" ); 820 | } 821 | WP_Cli::error( "The missing constants are required in order to run this subcommand.\nType `wp help deploy` for more information." ); 822 | } 823 | 824 | /** Add the optional constants. */ 825 | foreach ( $deps['optional'] as $optional ) { 826 | $const = $get_const( $optional ); 827 | if ( defined( $const ) ) 828 | $constants[$optional] = constant( $const ); 829 | } 830 | 831 | return $constants; 832 | } 833 | 834 | /** Replaces the placeholders in the paths with actual data. */ 835 | private static function expand( $config, $constants, $command, $what ) { 836 | 837 | $data = array( 838 | 'env' => self::$env, 839 | 'command' => $command, 840 | 'what' => $what, 841 | 'excludes' => ( isset( $constants['excludes'] ) && is_string( $constants['excludes'] ) ? $constants['excludes'] : false ), 842 | 'port' => ( isset( $constants['port'] ) ? $constants['port'] : '22' ), 843 | 'hash' => Util::get_hash(), 844 | 'abspath' => untrailingslashit( ABSPATH ), 845 | 'pretty_date' => date( 'Y_m_d-H_i' ), 846 | 'rand' => substr( sha1( time() ), 0, 8 ), 847 | 'hostname' => Runner::get_result( "hostname" ), 848 | 'local_uploads' => call_user_func( function() { 849 | $uploads_dir = wp_upload_dir(); 850 | return untrailingslashit( Runner::get_result( 851 | "cd {$uploads_dir['basedir']}; pwd -P;" 852 | ) ); 853 | } ), 854 | 'local_themes' => call_user_func( function() { 855 | $themes_dir = get_theme_root(); 856 | return untrailingslashit( Runner::get_result( 857 | "cd {$themes_dir}; pwd -P;" 858 | ) ); 859 | } ), 860 | 'local_plugins' => call_user_func( function() { 861 | $plugins_dir = WP_PLUGIN_DIR; // TODO: get the plugin directory in a better manner 862 | return untrailingslashit( Runner::get_result( 863 | "cd {$plugins_dir}; pwd -P;" 864 | ) ); 865 | } ), 866 | 'siteurl' => untrailingslashit( Util::trim_url( 867 | get_option( 'siteurl' ), 868 | true 869 | ) ), 870 | 'object' => (object) array_map( 'untrailingslashit', $constants ), 871 | ); 872 | 873 | foreach ( $config as &$item ) { 874 | $item = Util::unplaceholdit( $item, array_merge( 875 | /** This ensures we can have dependecies. */ 876 | $config, 877 | $data 878 | ) ); 879 | } 880 | 881 | if ( isset( $constants['post_hook'] ) ) { 882 | $config['post_hook'] = Util::unplaceholdit( $config['post_hook'], array_merge( $config, $data ) ); 883 | } 884 | 885 | /** Remove unset config items (constants). */ 886 | $config = array_filter( $config, function ( $item ) { 887 | return strpos( $item, '%%' ) === false; 888 | } ); 889 | 890 | /** Return the config in object form. */ 891 | return (object) $config; 892 | } 893 | 894 | public function run_post_hook() { 895 | 896 | if ( isset( self::$config->post_hook ) ) { 897 | $result = Runner::get_result( self::$config->post_hook ); 898 | if ( ! empty( $result ) ) { 899 | var_dump( $result ); 900 | } 901 | WP_Cli::line( "Ran post hook." ); 902 | } 903 | } 904 | 905 | private static function wow() { 906 | $doge = array( 'wow', 'many', 'such', 'so' ); 907 | $words = array( 'finish', 'done', 'end', 'deploy' ); 908 | WP_CLI::line(''); 909 | WP_Cli::success( 910 | $doge[array_rand( $doge, 1 )] . ' ' . 911 | $words[array_rand( $words, 1 )] . '!' 912 | ); 913 | } 914 | } 915 | 916 | WP_CLI::add_command( 'deploy', 'WP_Deploy_Command' ); 917 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | ( $compress ? 'z' : '' ), 26 | 'delete' => ( $delete ? ' --delete' : '' ), 27 | 'src' => $source, 28 | 'port' => $port, 29 | 'dest' => $dest, 30 | 'exclude' => '--exclude ' . implode( 31 | ' --exclude ', 32 | array_map( 'escapeshellarg', $exclude ) 33 | ) 34 | ) 35 | ); 36 | 37 | return $rsync; 38 | } 39 | 40 | static function unplaceholdit( $template, $content, $object_key = 'object' ) { 41 | 42 | /** Early bailout? */ 43 | if ( strpos( $template, '%%' ) === false ) 44 | return $template; 45 | 46 | /** First, get a list of all placeholders. */ 47 | $matches = $replaces = array(); 48 | preg_match_all( '/%%([^%]+)%%/u', $template, $matches, PREG_SET_ORDER ); 49 | 50 | $searches = wp_list_pluck( $matches, 0 ); 51 | 52 | /* Cast the object */ 53 | $object = array_key_exists( $object_key, $content ) ? (array) $content[$object_key] : false; 54 | 55 | foreach ( $matches as $match ) { 56 | /** 57 | * 0 => %%template_tag%% 58 | * 1 => variable_name 59 | */ 60 | if( $object && isset( $object[$match[1]] ) ) 61 | array_push( $replaces, $object[$match[1]] ); 62 | else if( isset( $content[$match[1]] ) ) 63 | array_push( $replaces, $content[$match[1]] ); 64 | else 65 | array_push( $replaces, $match[0] ); 66 | } 67 | 68 | return str_replace( $searches, $replaces, $template ); 69 | } 70 | 71 | static function trim_url( $url, $path = false ) { 72 | 73 | /** In case scheme relative URI is passed, e.g., //www.google.com/ */ 74 | $url = trim( $url, '/' ); 75 | 76 | /** If scheme not included, prepend it */ 77 | if ( ! preg_match( '#^http(s)?://#', $url ) ) { 78 | $url = 'http://' . $url; 79 | } 80 | 81 | /** Remove www. */ 82 | $url_parts = parse_url( $url ); 83 | $domain = preg_replace( '/^www\./', '', $url_parts['host'] ) . ( ! empty( $url_parts['port'] ) ? ':' . $url_parts['port'] : '' ); 84 | 85 | /** Add directory path if needed **/ 86 | if ( $path && $url_parts['path'] ) 87 | $domain .= $url_parts['path']; 88 | 89 | return $domain; 90 | } 91 | 92 | /** Returns and unique hash to identify the environment. */ 93 | static function get_hash() { 94 | $siteurl = self::trim_url( get_option( 'siteurl' ) ); 95 | return substr( sha1( DB_NAME . $siteurl ), 0, 8 ); 96 | } 97 | 98 | /** 99 | * Create a bar that spans with width of the console 100 | * 101 | * ## OPTIONS 102 | * 103 | * [] 104 | * : The character(s) to make the bar with. Default = 105 | * 106 | * [--c=] 107 | * : Color for bar. Default %p 108 | * 109 | * 110 | * ## EXAMPLES 111 | * 112 | * wp bar 113 | * 114 | * wp bar '-~' --c='%r' 115 | * 116 | * wp bar '+-' --c='%r%3' 117 | */ 118 | function bar( $args = array(), $assoc_args = array() ) { 119 | $char = isset( $args[0] ) ? $args[0] : '='; 120 | $cols = \cli\Shell::columns(); 121 | $line = substr( str_repeat($char, $cols), 0, $cols ); 122 | 123 | if ( ! isset( $assoc_args['c'] ) ) { 124 | $color = '%p'; // https://github.com/jlogsdon/php-cli-tools/blob/master/lib/cli/Colors.php#L113 125 | } else { 126 | $color = $assoc_args['c']; 127 | $color = '%'. trim( $color, '%' ); 128 | } 129 | 130 | WP_CLI::line( WP_CLI::colorize( $color . $line .'%n' ) ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /runner.php: -------------------------------------------------------------------------------- 1 | commands = array(); 11 | self::$verbosity = $v; 12 | } 13 | 14 | /** 15 | * Accepted arguments: 16 | * 17 | * [condition] : bool (Default: true) 18 | * command / array( command, cwd ) : string / array( string, string ) 19 | * [exit_on_fail] : bool (Default: true) 20 | * [success_message] : string (Default: none) 21 | * [fail_message] : string (Default: none) 22 | * 23 | * - If present condition needs to be first. 24 | * - Command can either be a string, or an array containing the 25 | * command string and the working directory in which it will be exec 26 | * - Exit on failure needs to be boolean. If missing, it's set as true 27 | * - The success and fail messages need to be strings and always in 28 | * this order. 29 | * 30 | * [args] (bracketed arguments) are optional. 31 | * 32 | */ 33 | function add() { 34 | 35 | $args = func_get_args(); 36 | 37 | if ( is_bool( $args[0] ) ) { 38 | $condition = array_shift( $args ); 39 | if ( ! $condition ) 40 | return; 41 | } 42 | 43 | if ( empty( $args ) ) 44 | return; 45 | 46 | $command = $args[0]; 47 | 48 | $messages = array_filter( 49 | array_slice( $args, 1 ), 50 | function( $a ) { 51 | return is_string( $a ); 52 | } ); 53 | 54 | $exit_on_failure = function( $default ) use ( $args ) { 55 | foreach ( $args as $arg ) { 56 | if ( is_bool( $arg ) ) 57 | return $arg; 58 | } 59 | return $default; 60 | }; 61 | 62 | $meta = array( 63 | 'command' => ( is_array( $command ) ? $command[0] : $command ), 64 | 'cwd' => ( is_array( $command ) ? $command[1] : false ), 65 | 'exit' => $exit_on_failure( true ), 66 | 'messages' => ( count( $messages ) ? $messages : false ), 67 | ); 68 | 69 | $this->commands[] = $meta; 70 | } 71 | 72 | function run() { 73 | foreach ( $this->commands as $key => $command ) { 74 | if ( defined( 'WP_DEPLOY_DEBUG' ) && ( WP_DEPLOY_DEBUG == 'all' ) ) { 75 | ini_set( 'error_reporting', E_ALL & ~E_STRICT ); 76 | ini_set( 'display_errors', 'STDERR' ); 77 | var_dump( $command ); //['command'] ); 78 | } else { 79 | self::launch( $command ); 80 | } 81 | /** Remove command from queue. */ 82 | unset( $this->commands[$key] ); 83 | } 84 | } 85 | 86 | private static function launch( $meta ) { 87 | 88 | $command = $meta['command']; 89 | 90 | $verbosity = self::$verbosity; 91 | $descriptors = call_user_func( function() use ( $verbosity, $command ) { 92 | $options = array( 93 | 0 => array( STDIN, STDOUT, STDERR ), 94 | 1 => array( STDIN, array( 'pipe', 'r' ), STDERR ), 95 | 2 => array( STDIN, array( 'pipe', 'r' ), STDERR ), 96 | ); 97 | /** For level 1, make an exception of blocking for rsync. */ 98 | if ( strpos( $command, 'rsync' ) !== false ) 99 | $options[1] = array( STDIN, STDOUT, STDERR ); 100 | 101 | return $options[$verbosity]; 102 | } ); 103 | 104 | $cwd = $meta['cwd'] ? $meta['cwd'] : null; 105 | 106 | $code = proc_close( proc_open( $command, $descriptors, $pipes, $cwd ) ); 107 | 108 | if ( $code && $meta['exit'] ) 109 | exit( $code ); 110 | 111 | if ( ! $meta['messages'] ) 112 | return; 113 | 114 | $success = array_shift( $meta['messages'] ); 115 | $fail = ! empty( $messages ) ? array_shift( $meta['messages'] ) : $success; 116 | 117 | if ( $code ) { 118 | \WP_CLI::warning( $fail ); 119 | } else { 120 | \WP_CLI::success( $success ); 121 | } 122 | } 123 | 124 | public static function get_result( $command ) { 125 | $cwd = null; 126 | $descriptors = array( 127 | 0 => array( 'pipe', 'r' ), 128 | 1 => array( 'pipe', 'w' ), 129 | ); 130 | $pipes = array(); 131 | $handler = proc_open( $command, $descriptors, $pipes, $cwd ); 132 | 133 | $output = stream_get_contents( $pipes[1] ); 134 | 135 | proc_close( $handler ); 136 | 137 | return trim( $output ); 138 | } 139 | 140 | } 141 | --------------------------------------------------------------------------------