├── img ├── cicd-gitlab-webhook.png └── sequence-diagram.plantuml ├── test ├── deployer └── config.inc.php ├── src ├── views │ └── help.php ├── default-config.inc.php ├── ShellConsole.php ├── GetOpt.php ├── App.php └── Deployer.php ├── composer.json ├── deployer ├── LICENSE ├── Installer.php ├── config.inc.php ├── webhook ├── index.html ├── bitbucket │ └── index.php └── gitlab │ └── index.php ├── tools ├── mirror ├── README.md └── deployer └── README.md /img/cicd-gitlab-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidas/deployer-php-cli/HEAD/img/cicd-gitlab-webhook.png -------------------------------------------------------------------------------- /test/deployer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 3 | run($configList, $argv); 27 | 28 | -------------------------------------------------------------------------------- /src/views/help.php: -------------------------------------------------------------------------------- 1 | Usage: 2 | deployer [options] [arguments] 3 | ./deployer [options] [arguments] 4 | 5 | Options: 6 | -h --help Display this help message 7 | --version Show the current version of the application 8 | -p, --project Project key by configuration for deployment 9 | --config Show the seleted project configuration 10 | --configuration 11 | --skip-git Force to skip Git process 12 | --skip-composer Force to skip Composer process 13 | --git-reset Git reset to given commit with --hard option 14 | -v, --verbose Increase the verbosity of messages -------------------------------------------------------------------------------- /img/sequence-diagram.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor "User" as user 3 | participant "Stage Server" as stage 4 | participant "Git Repository" as repo 5 | participant "Production Server Group" as real 6 | 7 | 8 | alt Automation 9 | user -> repo: Push released branch 10 | repo -> stage: Trigger webhook 11 | else Manual 12 | user -> stage: SSH tunnel 13 | stage -> stage: Run by command line 14 | end 15 | 16 | group Pipeline 17 | stage -> stage: Git, Composer, test, tasks before 18 | stage -> real: Rsync 19 | real --> stage: Result 20 | stage -> stage: Tasks after 21 | end group 22 | 23 | alt Log Mode enabled 24 | stage -> stage: Save log file 25 | user -> stage: Browse result web page \n(Secret token) 26 | stage --> user: Result report 27 | end 28 | 29 | @enduml 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yidas/deployer-php-cli", 3 | "description": "Code deployment tool based on RSYNC running by PHP-CLI script", 4 | "keywords": ["deployment", "continuous-integration", "rsync" ,"php-cli"], 5 | "homepage": "https://github.com/yidas/deployer-php-cli", 6 | "type": "project", 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/yidas/deployer-php-cli/issues", 10 | "source": "https://github.com/yidas/deployer-php-cli" 11 | }, 12 | "minimum-stability": "stable", 13 | "require": { 14 | "php": ">=5.4.0" 15 | }, 16 | "autoload": { 17 | "classmap": ["Installer.php"] 18 | }, 19 | "scripts": { 20 | "post-create-project-cmd": [ 21 | "Installer::postCreateProject" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /deployer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 3 | 11 | * @filesource PHP 5.4.0+ 12 | * @filesource RSYNC commander 13 | * @filesource Git commander 14 | * @filesource Composer commander 15 | * 16 | * @param string $argv[1] Project 17 | * @example 18 | * $ ./deployer // Interactive Project Select 19 | * $ ./deployer --project="default" // Non-Interactive Project Select 20 | */ 21 | 22 | // App loader 23 | require __DIR__. '/src/App.php'; 24 | 25 | 26 | /* Bootstrap */ 27 | 28 | // error_reporting(E_ALL); 29 | // ini_set("display_errors", 1); 30 | 31 | /* Config List Handler */ 32 | $configList = require __DIR__. '/config.inc.php'; 33 | // print_r($configList); 34 | 35 | $argv = isset($argv) ? $argv : []; 36 | 37 | $app = new App; 38 | $app->run($configList, $argv); 39 | 40 | -------------------------------------------------------------------------------- /test/config.inc.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'servers' => [ 6 | '127.0.0.1', 7 | ], 8 | 'user' => [ 9 | 'local' => '', 10 | 'remote' => '', 11 | ], 12 | 'source' => __DIR__, 13 | 'destination' => __DIR__, 14 | 'exclude' => [ 15 | '.git', 16 | ], 17 | 'git' => [ 18 | 'enabled' => true, 19 | 'path' => './', 20 | 'checkout' => true, 21 | 'branch' => 'master', 22 | ], 23 | 'composer' => [ 24 | 'enabled' => false, 25 | 'path' => './', 26 | 'command' => 'composer install', 27 | ], 28 | 'rsync' => [ 29 | 'params' => '-av --delete', 30 | 'sleepSeconds' => 0, 31 | ], 32 | 'commands' => [ 33 | 'before' => [ 34 | '', 35 | ], 36 | ], 37 | 'verbose' => false, 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Tsai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/default-config.inc.php: -------------------------------------------------------------------------------- 1 | [ 7 | '127.0.0.1', 8 | ], 9 | 'user' => [ 10 | 'local' => '', 11 | 'remote' => '', 12 | ], 13 | 'source' => '', 14 | 'destination' => '', 15 | 'exclude' => [ 16 | '.git', 17 | ], 18 | 'git' => [ 19 | 'enabled' => false, 20 | 'path' => './', 21 | 'checkout' => true, 22 | 'branch' => 'master', 23 | 'submodule' => false, 24 | ], 25 | 'composer' => [ 26 | 'enabled' => false, 27 | 'path' => './', 28 | 'command' => 'composer -n install', 29 | ], 30 | 'rsync' => [ 31 | 'enabled' => true, 32 | 'params' => '-av --delete', 33 | 'sleepSeconds' => 0, 34 | 'timeout' => 60, 35 | 'identityFile' => null, 36 | ], 37 | 'commands' => [ 38 | 'before' => [ 39 | '', 40 | ], 41 | ], 42 | 'webhook' => [ 43 | 'enabled' => false, 44 | 'provider' => 'gitlab', 45 | 'project' => '', 46 | 'token' => '', 47 | ], 48 | 'verbose' => false, 49 | ]; -------------------------------------------------------------------------------- /src/ShellConsole.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | trait ShellConsole 9 | { 10 | /** 11 | * Command 12 | * 13 | * @param string $cmd 14 | * @return mixed Response 15 | */ 16 | // private function _exec($cmd) 17 | // { 18 | // return shell_exec($cmd); 19 | // } 20 | 21 | /** 22 | * Execute command line with status returning 23 | * 24 | * @param string $cmd 25 | * @param string $resultText 26 | * @param array $output 27 | * @param integer $errorCode 28 | * @return boolean Last command success or not 29 | */ 30 | private function _exec($cmd, &$resultText='', &$output='', &$errorCode='') 31 | { 32 | $cmd = trim($cmd); 33 | $cmd = rtrim($cmd, ';'); 34 | 35 | // stdout 36 | $cmd = "{$cmd} 2>&1;"; 37 | exec($cmd, $output, $errorCode); 38 | 39 | // Build result text 40 | foreach ($output as $key => $string) { 41 | $resultText .= "{$string}\r\n"; 42 | } 43 | 44 | return (!$errorCode) ? true : false; 45 | } 46 | 47 | /** 48 | * Get username 49 | * 50 | * @return string User 51 | */ 52 | private function _getUser() 53 | { 54 | $this->_exec('echo $USER;', $user); 55 | 56 | return trim($user); 57 | } 58 | 59 | /** 60 | * Response 61 | * 62 | * @param string $string 63 | */ 64 | private function _print($string) 65 | { 66 | echo "{$string}\n"; 67 | } 68 | } -------------------------------------------------------------------------------- /Installer.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'servers' => [ 11 | '127.0.0.1', 12 | ], 13 | 'source' => '/home/user/project', 14 | 'destination' => '/var/www/html/prod/', 15 | ], 16 | // This project config processes Git and Composer before deployment 17 | 'advanced' => [ 18 | 'servers' => [ 19 | '127.0.0.1', 20 | ], 21 | 'user' => [ 22 | 'local' => '', 23 | 'remote' => '', 24 | ], 25 | 'source' => '/home/user/project-advanced', 26 | 'destination' => '/var/www/html/prod/', 27 | 'exclude' => [ 28 | '.git', 29 | 'tmp/*', 30 | ], 31 | 'git' => [ 32 | 'enabled' => true, 33 | 'path' => './', 34 | 'checkout' => true, 35 | 'branch' => 'master', 36 | 'submodule' => false, 37 | ], 38 | 'composer' => [ 39 | 'enabled' => true, 40 | 'path' => './', 41 | // 'path' => ['./', './application/'], 42 | // If You use Xdebug on php-fpm 43 | // 'command' => 'COMPOSER_ALLOW_XDEBUG=1 composer -n install', 44 | 'command' => 'composer -n install', 45 | ], 46 | 'test' => [ 47 | 'enabled' => false, 48 | 'name' => 'PHPUnit', 49 | 'type' => 'phpunit', 50 | // CodeIgniter 3 for example (https://github.com/yidas/codeigniter-phpunit) 51 | 'command' => './application/vendor/bin/phpunit', 52 | 'configuration' => './application/phpunit.xml', 53 | ], 54 | 'rsync' => [ 55 | 'enabled' => true, 56 | 'params' => '-av --delete', 57 | // 'sleepSeconds' => 0, 58 | // 'timeout' => 60, 59 | // 'identityFile' => '/home/deployer/.ssh/id_rsa', 60 | ], 61 | 'commands' => [ 62 | 'before' => [ 63 | '', 64 | ], 65 | ], 66 | 'webhook' => [ 67 | 'enabled' => false, 68 | 'provider' => 'gitlab', 69 | 'project' => 'yidas/deployer-php-cli', 70 | 'token' => 'thisistoken', 71 | ], 72 | 'verbose' => false, 73 | ], 74 | ]; 75 | -------------------------------------------------------------------------------- /webhook/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Deployer-PHP-CLI 5 | 12 | 13 | 14 |

Deployer PHP-CLI

15 |

Webhook Interface Home Page

16 | 17 |

Documentation: 18 | https://github.com/yidas/deployer-php-cli.
19 | 20 |

By YIDAS

21 | 22 | -------------------------------------------------------------------------------- /src/GetOpt.php: -------------------------------------------------------------------------------- 1 | 9 | * @version 1.0.0 10 | * @see http://php.net/manual/en/function.getopt.php#refsect1-function.getopt-parameters 11 | * @param string options 12 | * @param array longopts 13 | * @param int optind 14 | * @example 15 | * $getOpt = new GetOpt('h:v', ['host:', 'verbose']); 16 | * $hostname = $getOpt->get(['project', 'p']); // String or null 17 | * $debugOn = $getOpt->has(['verbose', 'v']); // Bool 18 | */ 19 | class GetOpt 20 | { 21 | /** 22 | * @var array Cached options 23 | */ 24 | private $_options; 25 | 26 | function __construct($options, array $longopts=[], $optind=null) 27 | { 28 | // $optind for PHP 7.1.0 29 | $this->_options = ($optind) 30 | ? getopt($options, $longopts, $optind) 31 | : getopt($options, $longopts); 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Get Option Value 38 | * 39 | * @param string|array Option priority key(s) for same purpose 40 | * @return mixed Result of purpose option value by getopt(), return null while not set 41 | * @example 42 | * $verbose = $this->get(['verbose', 'v']); 43 | */ 44 | public function get($options) 45 | { 46 | // String Key 47 | if (is_string($options)) { 48 | 49 | return (isset($this->_options[$options])) ? $this->_options[$options] : null; 50 | } 51 | // Array Keys 52 | if (is_array($options)) { 53 | // Maping loop 54 | foreach ($options as $key => $option) { 55 | // First match 56 | if (isset($this->_options[$option])) { 57 | 58 | return $this->_options[$option]; 59 | } 60 | } 61 | } 62 | 63 | return null; 64 | } 65 | 66 | /** 67 | * Get Option Value 68 | * 69 | * @param string|array Option priority key(s) for same purpose 70 | * @return mixed Result of purpose option value by getopt(), return null while not set 71 | * @example 72 | * $verbose = $this->get(['verbose', 'v']); 73 | */ 74 | public function has($options) 75 | { 76 | // String Key 77 | if (is_string($options)) { 78 | 79 | return (isset($this->_options[$options])) ? true : false; 80 | } 81 | // Array Keys 82 | if (is_array($options)) { 83 | // Maping loop 84 | foreach ($options as $key => $option) { 85 | // First match 86 | if (isset($this->_options[$option])) { 87 | 88 | return true; 89 | } 90 | } 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Get Options 98 | * 99 | * @return array $this->$_options 100 | */ 101 | public function getOptions() 102 | { 103 | return $this->_options; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tools/mirror: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 3 | 13 | * @filesource PHP >= 5.4 (Support >= 5.0 if removing Short-Array-Syntax) 14 | * @param string $argv[1] File/directory in current path for rsync 15 | * @param string $argv[2] (Optional) Target servers group key of remoteServers 16 | * @example 17 | * $ ~/mirror file.php // Rsync file.php to servers with same path 18 | * $ ~/mirror folderA // Rsync whole folderA to servers 19 | * $ ~/mirror ./ // Rsync current whole folder 20 | * $ ~/mirror ./ stage // Rsync to servers in stage group 21 | * $ ~/mirror ./ prod // Rsync to servers in prod group 22 | */ 23 | 24 | 25 | /* Configuration */ 26 | 27 | /** 28 | * @var array Distant server host list 29 | */ 30 | $config['remoteServers'] = [ 31 | 'default' => [ 32 | '110.1.1.1', 33 | '110.1.2.1', 34 | ], 35 | 'stage' => [ 36 | '110.1.1.1', 37 | ], 38 | 'prod' => [ 39 | '110.1.2.1', 40 | ], 41 | ]; 42 | 43 | /** 44 | * @var string Remote server user 45 | */ 46 | $config['remoteUser'] = 'www-data'; 47 | 48 | /** 49 | * @var string Addition params of rsync command 50 | */ 51 | $config['rsyncParams'] = '-av --delete'; 52 | 53 | /** 54 | * @var int Seconds waiting of each rsync connections 55 | */ 56 | $config['sleepSeconds'] = 0; 57 | 58 | /* /Configuration */ 59 | 60 | 61 | ob_implicit_flush(); 62 | 63 | // File input 64 | $file = (isset($argv[1])) ? $argv[1] : NULL; 65 | 66 | // Target server group list for rsync 67 | $serverEnv = (isset($argv[2])) ? $argv[2] : 'default'; 68 | 69 | // Directory of destination same as source 70 | $dir = trim(shell_exec("pwd -P")); 71 | 72 | try { 73 | 74 | // File existence check 75 | if (strlen(trim($file))==0) 76 | throw new Exception('None of file input'); 77 | 78 | // Check $argv likes asterisk 79 | if (isset($argv[3])) 80 | throw new Exception('Invalid arguments input'); 81 | 82 | /** 83 | * Validating file name input 84 | * 85 | * @var sstring $reg Regular patterns 86 | * @example 87 | * \w\/ // folderA/ 88 | * \* // * or *.* 89 | * ^\/ // / or /etc 90 | * 91 | */ 92 | $reg = '/(\w\/|\*|^\/)/'; 93 | 94 | preg_match($reg,$file,$matches); 95 | 96 | if ($matches) { 97 | 98 | //print_r($matches); 99 | 100 | throw new Exception('Invalid file name input'); 101 | } 102 | 103 | // Check for server list 104 | if (!isset($config['remoteServers'][$serverEnv]) 105 | || !$config['remoteServers'][$serverEnv]) { 106 | 107 | throw new Exception("No server host in group: {$serverEnv}"); 108 | } 109 | 110 | // File or directory of source definition 111 | $this_file = $dir.'/'.$file; 112 | 113 | // Check for type of link 114 | if (is_link($this_file)) 115 | throw new Exception('File input is symblic link'); 116 | 117 | // Check for type of file / directory 118 | if (!is_file($this_file) && !is_dir($this_file) ) 119 | throw new Exception('File input is not a file or directory'); 120 | 121 | // Check for syntax if is PHP 122 | if ( preg_match("/\.php$/i",$file) 123 | && !preg_match("/No syntax errors detected/i", shell_exec("php -l ".$this_file)) ) { 124 | 125 | throw new Exception('PHP syntax error!'); 126 | } 127 | 128 | // Rsync each servers 129 | foreach ($config['remoteServers'][$serverEnv] as $key => $server) { 130 | 131 | // Info display 132 | echo '/* --- Process Start --- */'."\n"; 133 | echo '[Process]: '.($key+1)."\n"; 134 | echo '[Group ]: '.$serverEnv."\n"; 135 | echo '[Server ]: '.$server."\n"; 136 | echo '[User ]: '.$config['remoteUser']."\n"; 137 | 138 | 139 | /* Command builder */ 140 | 141 | $cmd = 'rsync ' . $config['rsyncParams']; 142 | 143 | // Rsync shell command 144 | $cmd = sprintf("%s %s %s@%s:%s", 145 | $cmd, 146 | $file, 147 | $config['remoteUser'], 148 | $server, 149 | $dir 150 | ); 151 | 152 | echo '[Command]: '.$cmd."\n"; 153 | 154 | // Shell execution 155 | $result = shell_exec($cmd); 156 | 157 | echo '[Message]: '."\n".$result; 158 | 159 | echo '/* --- /Process End --- */'."\n"; 160 | echo "\r\n"; 161 | 162 | sleep($config['sleepSeconds']); 163 | } 164 | 165 | echo "\r\n"; 166 | 167 | } catch (Exception $e) { 168 | 169 | die('ERROR:'.$e->getMessage()."\n"); 170 | } 171 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | Deployer *by PHP-CLI* 2 | ===================== 3 | 4 | Code deployment tool based on RSYNC running by PHP-CLI script 5 | 6 | FEATURES 7 | -------- 8 | 9 | ***1. Deploy to multiple servers by groups*** 10 | 11 | ***2. Git supported for source project*** 12 | 13 | ***3. Composer supported for source project*** 14 | 15 | ***4. Filter for excluding specified files supported*** 16 | 17 | These rsync php scripts are helping developers to deploy codes from local instance to remote instances. 18 | 19 | --- 20 | 21 | DEMONSTRATION 22 | ------------- 23 | 24 | Deploy local project to remote servers by just executing the deployer in command: 25 | 26 | ``` 27 | $ ./deployer 28 | ``` 29 | Or you can call it by PHP-CLI: 30 | ``` 31 | $ php ./deployer 32 | ``` 33 | 34 | The result could like be: 35 | ``` 36 | /* --- Git Process Start --- */ 37 | Already up-to-date. 38 | /* --- Git Process End --- */ 39 | 40 | /* --- Rsync Process Start --- */ 41 | [Process]: 1 42 | [Group ]: default 43 | [Server ]: 127.0.0.1 44 | [User ]: nick_tsai 45 | [Source ]: /home/www/projects/deployer-php-cli 46 | [Remote ]: /var/www/html/projects/ 47 | [Command]: rsync -av --delete --exclude "web/upload" --exclude "runtime/log" /home/www/projects/deployer-php-cli nick_tsai@127.0.0.1:/var/www/html/projects/ 48 | [Message]: 49 | sending incremental file list 50 | deployer-php-cli/index.php 51 | 52 | sent 149,506 bytes received 814 bytes 60,128.00 bytes/sec 53 | total size is 45,912,740 speedup is 305.43 54 | /* --- Rsync Process End --- */ 55 | ``` 56 | 57 | --- 58 | 59 | INSTALLATION 60 | ------------ 61 | 62 | - **[deployer](#deployer)**   63 | 64 | ``` 65 | wget https://raw.githubusercontent.com/yidas/deployer-php-cli/master/src/deployer 66 | ``` 67 | 68 | - **[mirror](#mirror)**   69 | 70 | ``` 71 | wget https://raw.githubusercontent.com/yidas/deployer-php-cli/master/src/mirror 72 | ``` 73 | 74 | After download, you could add excute property to that file by `chmod +x`. 75 | 76 | The scripts including shell script for running php at the first line: 77 | ``` 78 | #!/usr/bin/php -q 79 | ``` 80 | You can customize it for correct php bin path in your environment, saving the file with [binary encode](#save-bin-file). 81 | 82 | --- 83 | 84 | CONFIGURATION 85 | ------------- 86 | 87 | ### Servers Setting: 88 | 89 | You need to set up the target servers' hostname or IP into the script file: 90 | 91 | ``` 92 | $config['remoteServers'] = [ 93 | 'default' => [ 94 | '110.1.1.1', 95 | '110.1.2.1', 96 | ], 97 | 'stage' => [ 98 | '110.1.1.1', 99 | ], 100 | 'prod' => [ 101 | '110.1.2.1', 102 | ], 103 | ]; 104 | ``` 105 | 106 | Also, the remote server user need to be assigned: 107 | 108 | ``` 109 | $config['remoteUser'] = 'www-data'; 110 | ``` 111 | 112 | ### Config Options 113 | 114 | |Key|Description| 115 | |:-|:-| 116 | |**remoteServers**|Distant server host list| 117 | |**remoteUser**|Remote server user| 118 | |**sourceFile**|Local directory for deploy | 119 | |**remotePath**|Remote path for synchronism| 120 | |rsyncParams|Addition params of rsync command| 121 | |**excludeFiles**|Excluded files based on sourceFile path| 122 | |sleepSeconds|Seconds waiting of each rsync connections| 123 | |gitEnabled|Enabled git or not| 124 | |gitCheckoutEnabled|Execute git checkout -- . before git pull | 125 | |gitBranch|Branch name for git pull, pull default branch if empty | 126 | |composerEnabled|Enabled Composer or not| 127 | |composerCommand|Composer command line for update or install| 128 | |commandsBeforeDeploy|Array of commands executing before deployment| 129 | 130 | --- 131 | 132 | SCRIPT FILES 133 | ------------ 134 | 135 | - **[deployer](#deployer)**   136 | Rsync a specified source folder to remote servers under the folder by setting path, supporting filtering files from excludeFiles. 137 | 138 | You need to do more setting for p2p directories in `rsyncStatic.php`: 139 | ``` 140 | $config['sourceFile'] = '/home/www/www.project.com/webroot'; 141 | $config['remotePath'] = '/home/www/www.project.com/'; 142 | ``` 143 | 144 | - **[mirror](#mirror)**   145 | Rsync a file or a folder from current local path to destination servers with the same path automatically, the current path is base on Linux's "pwd -P" command. 146 | 147 | --- 148 | 149 | USAGE 150 | ----- 151 | 152 | ### deployer 153 | 154 | For `deployer`, you need to set project folder path into the file with source & destination directory, then you can run it: 155 | ``` 156 | $ ./deployer // Rsync to servers in default group 157 | $ ./deployer stage // Rsync to servers in stage group 158 | $ ./deployer prod // Rsync to servers in prod group 159 | ``` 160 | 161 | 162 | ### mirror 163 | 164 | For `mirror`, you can put scripts in your home directory, and cd into the pre-sync file directory: 165 | 166 | ``` 167 | $ ~/mirror file.php // Rsync file.php to servers with same path 168 | $ ~/mirror folderA // Rsync whole folderA to servers 169 | $ ~/mirror ./ // Rsync current whole folder 170 | $ ~/mirror ./ stage // Rsync to servers in stage group 171 | $ ~/mirror ./ prod // Rsync to servers in prod group 172 | ``` 173 | 174 | --- 175 | 176 | ADDITION 177 | -------- 178 | 179 | ### Rsync without Password: 180 | 181 | You can put your local user's SSH public key to destination server user for authorization. 182 | ``` 183 | .ssh/id_rsa.pub >> .ssh/authorized_keys 184 | ``` 185 | 186 | ### Save Binary Encode File: 187 | 188 | While excuting script, if you get the error like `Exception: Zend Extension ./deployer does not exist`, you may save the script file with binary encode, which could done by using `vim`: 189 | 190 | ``` 191 | :set ff=unix 192 | ``` 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class App 10 | { 11 | const VERSION = '1.12.0'; 12 | 13 | function __construct() 14 | { 15 | // Loader 16 | require __DIR__. '/ShellConsole.php'; 17 | require __DIR__. '/Deployer.php'; 18 | require __DIR__. '/GetOpt.php'; 19 | } 20 | 21 | /** 22 | * @param array $configList 23 | */ 24 | public function run(Array $configList, Array $argv) 25 | { 26 | // $projectKey = (isset($argv[1])) ? $argv[1] : 'default'; 27 | 28 | /** 29 | * Options definition 30 | */ 31 | $shortopts = ""; 32 | $shortopts .= "h"; 33 | $shortopts .= "p:"; 34 | $shortopts .= "v"; 35 | 36 | $longopts = array( 37 | "project:", 38 | "skip-git", 39 | "skip-composer", 40 | "git-reset:", 41 | "verbose", 42 | "config", 43 | "configuration", 44 | "help", 45 | "version", 46 | ); 47 | 48 | try { 49 | 50 | // GetOpt 51 | $getOpt = new GetOpt($shortopts, $longopts); 52 | // var_dump($getOpt->getOptions()); 53 | 54 | $projectKey = $getOpt->get(['project', 'p']); 55 | $showConfig = $getOpt->has(['config', 'configuration']); 56 | $showHelp = $getOpt->has(['help', 'h']); 57 | $showVersion = $getOpt->has(['version']); 58 | 59 | /** 60 | * Exception before App 61 | */ 62 | // Help 63 | if ($showHelp) { 64 | // Version first 65 | $this->_echoVersion(); 66 | echo "\r\n"; 67 | // Load view with CLI auto display 68 | require __DIR__. '/views/help.php'; 69 | echo "\r\n"; 70 | return; 71 | } 72 | // Version 73 | if ($showVersion) { 74 | // Get version 75 | $this->_echoVersion(); 76 | return; 77 | } 78 | 79 | // Check project config 80 | if (!isset($configList[$projectKey])) { 81 | 82 | // Welcome information 83 | // Get app root path 84 | $fileLocate = dirname(__DIR__); 85 | $this->_echoVersion(); 86 | echo " Bootstrap directory: {$fileLocate}. \r\n"; 87 | echo " Usage manual: `deployer --help`\r\n"; 88 | echo "\r\n"; 89 | 90 | // First time flag 91 | $isFirstTime = ($projectKey===null) ? true : false; 92 | 93 | while (!isset($configList[$projectKey])) { 94 | 95 | // Not in the first round 96 | if (!$isFirstTime) { 97 | echo "ERROR: The `{$projectKey}` project doesn't exist in your configuration.\n\n"; 98 | } 99 | 100 | // Available project list 101 | echo "Your available projects in configuration:\n"; 102 | $projectKeyMap = []; 103 | foreach ($configList as $key => $project) { 104 | 105 | $projectKeyMap[] = $key; 106 | // Get map key 107 | end($projectKeyMap); 108 | $num = key($projectKeyMap); 109 | 110 | echo " [{$num}] {$key}\n"; 111 | } 112 | echo "\r\n"; 113 | // Get project input 114 | echo " Please select a project [number or project, Ctrl+C to quit]:"; 115 | $projectKey = trim(fgets(STDIN)); 116 | echo "\r\n"; 117 | 118 | // Number input finding by $projectKeyMap 119 | if (is_numeric($projectKey)) { 120 | $projectKey = isset($projectKeyMap[$projectKey]) 121 | ? $projectKeyMap[$projectKey] 122 | : $projectKey; 123 | } 124 | 125 | $isFirstTime = false; 126 | } 127 | } 128 | 129 | // Config initialized 130 | $defaultConfig = require __DIR__. '/default-config.inc.php'; 131 | $config = array_replace_recursive($defaultConfig, $configList[$projectKey]); 132 | // Add `projectKey` key to the current config 133 | $config['projectKey'] = $projectKey; 134 | 135 | // Rewrite config 136 | $config['git']['enabled'] = ($getOpt->has('skip-git')) 137 | ? false : $this->_val($config, ['git', 'enabled']); 138 | $config['composer']['enabled'] = ($getOpt->has('skip-composer')) 139 | ? false : $this->_val($config, ['composer', 'enabled']); 140 | $config['verbose'] = ($getOpt->has(['verbose', 'v'])) 141 | ? true : $this->_val($config, ['verbose']); 142 | // Other config 143 | $config['git']['reset'] = $getOpt->get('git-reset'); 144 | 145 | // Initial Deployer 146 | $deployer = new Deployer($config); 147 | 148 | /** 149 | * Exception before Deployer run 150 | */ 151 | if ($showConfig) { 152 | echo "The `{$projectKey}` project's configuration is below:\n"; 153 | print_r($deployer->getConfig()); 154 | return; 155 | } 156 | 157 | // Run Deployer 158 | $deployer->run(); 159 | 160 | } catch (Exception $e) { 161 | 162 | die("ERROR:{$e->getMessage()}\n"); 163 | } 164 | } 165 | 166 | /** 167 | * Echo a line of version info 168 | */ 169 | protected function _echoVersion() 170 | { 171 | $version = self::VERSION; 172 | echo "Deployer-PHP-CLI version {$version} \r\n"; 173 | } 174 | 175 | /** 176 | * Var checker 177 | * 178 | * @param mixed Variable 179 | * @param array Variable array level ['level1', 'key'] 180 | * @return mixed value of specified variable 181 | */ 182 | protected function _val($var, $arrayLevel=[]) 183 | { 184 | if (!isset($var)) { 185 | 186 | return null; 187 | } 188 | 189 | foreach ($arrayLevel as $key => $level) { 190 | 191 | if (!isset($var[$level])) { 192 | 193 | return null; 194 | } 195 | 196 | $var = &$var[$level]; 197 | } 198 | 199 | return $var; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /webhook/bitbucket/index.php: -------------------------------------------------------------------------------- 1 | isset($_GET['token']) ? $_GET['token'] : null, 13 | 'project' => isset($_GET['log']) ? $_GET['log'] : null, 14 | 'branch' => isset($_GET['branch']) ? $_GET['branch'] : 'master', 15 | ]; 16 | 17 | if (!$logMode) { 18 | $body = file_get_contents('php://input'); 19 | $data = json_decode($body, true); 20 | 21 | // Push info 22 | $info = [ 23 | 'token' => $token, 24 | 'project' => $data['repository']['full_name'], 25 | 'projectUrl' => $data['repository']['links']['html']['href'], 26 | 'branch' => $data['push']['changes'][0]['new']['name'], 27 | ]; 28 | } 29 | 30 | try { 31 | // Check 32 | $configList = require __DIR__.'/../../config.inc.php'; 33 | 34 | $matchedConfig = []; 35 | $errorInfo = null; 36 | 37 | foreach ($configList as $key => $config) { 38 | // Webhook setting check 39 | if (!isset($config['webhook']['enabled']) || !$config['webhook']['enabled']) { 40 | continue; 41 | } 42 | // provider check 43 | elseif (!isset($config['webhook']['provider']) || $config['webhook']['provider'] != 'bitbucket') { 44 | continue; 45 | } 46 | // Last mapping for project name 47 | elseif (!isset($config['webhook']['project']) || $config['webhook']['project'] != $info['project']) { 48 | continue; 49 | } 50 | // Webhook branch check 51 | elseif (isset($config['webhook']['branch']) && $config['webhook']['branch'] != $info['branch']) { 52 | $errorInfo[] = "Branch `{$info['branch']}` could not be matched from webhook config"; 53 | continue; 54 | } 55 | // Use Git branch setting while no branch setting in Webhook 56 | elseif (!isset($config['webhook']['branch']) && isset($config['git']['branch']) && $config['git']['branch'] != $info['branch']) { 57 | $errorInfo[] = "Branch `{$info['branch']}` could not be matched from Git config"; 58 | continue; 59 | } 60 | 61 | // match config 62 | $matchedConfig = $config; 63 | // For Deployer config 64 | $matchedConfig['projectKey'] = $key; 65 | 66 | break; 67 | } 68 | } catch (\Exception $e) { 69 | responseWithPack(null, $e->getCode(), $e->getMessage()); 70 | exit; 71 | } 72 | 73 | // Matched config check 74 | if (empty($matchedConfig)) { 75 | responseWithPack($errorInfo, 404, 'No matched config found'); 76 | exit; 77 | } 78 | // Authorization while setting token 79 | elseif (isset($matchedConfig['webhook']['token']) && $info['token'] != $matchedConfig['webhook']['token']) { 80 | responseWithPack(['inputToken' => $info['token']], 403, 'Token is invalid'); 81 | exit; 82 | } 83 | 84 | // Log mode 85 | if ($logMode) { 86 | if (!isset($matchedConfig['webhook']['log'])) { 87 | die('Log setting is disabled'); 88 | } 89 | 90 | $logFile = is_string($matchedConfig['webhook']['log']) 91 | ? $matchedConfig['webhook']['log'] 92 | : $defaultLogFile; 93 | 94 | if (!file_exists($logFile)) { 95 | die('Log file not found'); 96 | } 97 | 98 | // Read log 99 | $oldList = json_decode(file_get_contents($logFile), true); 100 | $logList = is_array($oldList) ? $oldList : []; 101 | 102 | // Output 103 | if ($logList) { 104 | foreach ($logList as $key => $row) { 105 | echo "{$row['datetime']}
".$row['response'].'
'; 106 | } 107 | } else { 108 | echo 'No record yet'; 109 | } 110 | 111 | exit; 112 | } 113 | 114 | /** 115 | * Fast response for webhook. 116 | */ 117 | $data = []; 118 | // Provide resultUrl when webhook log is enabled 119 | if (isset($matchedConfig['webhook']['log'])) { 120 | $data['resultUrl'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') 121 | ."://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]" 122 | ."?log={$info['project']}&branch={$info['branch']}&token={$info['token']}"; 123 | } 124 | responseWithPack($data, 200, 'Deployer will start processing! Check log for result information.'); 125 | ignore_user_abort(true); 126 | header('Connection: close'); 127 | flush(); 128 | fastcgi_finish_request(); 129 | 130 | /** 131 | * Bootstrap. 132 | */ 133 | // Loader 134 | require __DIR__.'/../../src/ShellConsole.php'; 135 | require __DIR__.'/../../src/Deployer.php'; 136 | // Config initialized 137 | 138 | $defaultConfig = require __DIR__.'/../../src/default-config.inc.php'; 139 | $matchedConfig = array_replace_recursive($defaultConfig, $matchedConfig); 140 | // Initial Deployer 141 | $deployer = new Deployer($matchedConfig); 142 | // Run Deployer 143 | $res = $deployer->run(); 144 | file_put_contents('/tmp/debug2.log', json_encode($res)); 145 | 146 | if ($res && isset($matchedConfig['webhook']['log'])) { 147 | // Max rows per each log file 148 | $limit = 100; 149 | // Log file 150 | $logFile = is_string($matchedConfig['webhook']['log']) 151 | ? $matchedConfig['webhook']['log'] 152 | : $defaultLogFile; 153 | // Format 154 | $row = [ 155 | 'provider' => $config['webhook']['provider'], 156 | 'info' => $info, 157 | 'datetime' => date('Y-m-d H:i:s'), 158 | 'response' => $res, 159 | ]; 160 | // Log text 161 | $logList = []; 162 | 163 | if (file_exists($logFile)) { 164 | // Read log 165 | $oldList = json_decode(file_get_contents($logFile), true); 166 | $logList = is_array($oldList) ? $oldList : []; 167 | // Limit handling 168 | if (count($logList) >= $limit) { 169 | array_pop($logList); 170 | } 171 | } 172 | array_unshift($logList, $row); 173 | // Write back to log 174 | file_put_contents($logFile, json_encode($logList)); 175 | } 176 | 177 | /** 178 | * writeLog. 179 | * 180 | * @param string $text 181 | * 182 | * @return void 183 | */ 184 | function writeLog($text = 'no message', $writeLogFile = '/tmp/deployer-php-cli.log') 185 | { 186 | $text = is_array($text) ? print_r($text, true) : $text; 187 | 188 | file_put_contents($writeLogFile, $text); 189 | } 190 | 191 | /** 192 | * Response. 193 | * 194 | * @param int $status 195 | * @param array $body 196 | * 197 | * @return void 198 | */ 199 | function response($status = 200, $body = []) 200 | { 201 | http_response_code($status); 202 | header('Content-Type: application/json; charset=utf-8'); 203 | echo json_encode($body, JSON_UNESCAPED_SLASHES); 204 | } 205 | 206 | /** 207 | * Responese with pack. 208 | * 209 | * @param [type] $data 210 | * @param int $status 211 | * @param [type] $message 212 | * 213 | * @return void 214 | */ 215 | function responseWithPack($data = null, $status = 200, $message = null) 216 | { 217 | $body = [ 218 | 'code' => $status, 219 | ]; 220 | // Message field 221 | if ($message) { 222 | $body['message'] = $message; 223 | } 224 | // Data field 225 | if ($data) { 226 | $body['data'] = $data; 227 | } 228 | 229 | return response($status, $body); 230 | } 231 | -------------------------------------------------------------------------------- /tools/deployer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 3 | 11 | * @filesource PHP >= 5.4 (Support 5.0 if removing Short-Array-Syntax) 12 | * 13 | * @param string $argv[1] Target servers group key of remoteServers 14 | * @example 15 | * $ ./deployer // Rsync to servers in default group 16 | * $ ./deployer stage // Rsync to servers in stage group 17 | * $ ./deployer prod // Rsync to servers in prod group 18 | */ 19 | 20 | 21 | /* Configuration */ 22 | 23 | /** 24 | * @var array Distant server host list 25 | */ 26 | $config['remoteServers'] = [ 27 | 'default' => [ 28 | '110.1.1.1', 29 | '110.1.2.1', 30 | ], 31 | 'stage' => [ 32 | '110.1.1.1', 33 | ], 34 | 'prod' => [ 35 | '110.1.2.1', 36 | ], 37 | ]; 38 | 39 | /** 40 | * @var string Remote server user 41 | */ 42 | $config['remoteUser'] = 'www-data'; 43 | 44 | /** 45 | * @var string Local directory for deploy 46 | */ 47 | $config['sourceFile'] = '/home/www/www.project.com/webroot'; 48 | 49 | /** 50 | * @var string Remote path for synchronism 51 | */ 52 | $config['remotePath'] = '/home/www/www.project.com/'; 53 | 54 | /** 55 | * @var string Addition params of rsync command 56 | */ 57 | $config['rsyncParams'] = '-av --delete'; 58 | 59 | /** 60 | * @var array Excluded files based on sourceFile path 61 | */ 62 | $config['excludeFiles'] = [ 63 | 'web/upload', 64 | 'runtime/log', 65 | ]; 66 | 67 | /** 68 | * @var int Seconds waiting of each rsync connections 69 | */ 70 | $config['sleepSeconds'] = 0; 71 | 72 | 73 | /** 74 | * @var bool Enabled git or not 75 | */ 76 | $config['gitEnabled'] = false; 77 | 78 | /** 79 | * @var string Execute git checkout -- . before git pull 80 | */ 81 | $config['gitCheckoutEnabled'] = false; 82 | 83 | /** 84 | * @var string Branch name for git pull, pull default branch if empty 85 | */ 86 | $config['gitBranch'] = ''; 87 | 88 | /** 89 | * @var bool Enabled Composer or not 90 | */ 91 | $config['composerEnabled'] = false; 92 | 93 | /** 94 | * @var string Composer command line for update or install 95 | */ 96 | $config['composerCommand'] = 'composer update'; 97 | 98 | /** 99 | * @var string Array of commands executing before deployment 100 | */ 101 | $config['commandsBeforeDeploy'] = [ 102 | // 'cd /var/www/html/your-project', 103 | // 'gulp minify-all', 104 | // 'Minify' => 'cd /var/www/html/your-project; gulp minify-all', 105 | ]; 106 | 107 | /* /Configuration */ 108 | 109 | 110 | ob_implicit_flush(); 111 | 112 | // Target server group list for rsync 113 | $serverEnv = (isset($argv[1])) ? $argv[1] : 'default'; 114 | 115 | try { 116 | 117 | // Check for server list 118 | if (!isset($config['remoteServers'][$serverEnv]) 119 | || !$config['remoteServers'][$serverEnv]) { 120 | 121 | throw new Exception("No server host in group: {$serverEnv}"); 122 | } 123 | 124 | $sourceFile = $config['sourceFile']; 125 | $remotePath = $config['remotePath']; 126 | 127 | // File existence check 128 | if (strlen(trim($sourceFile))==0) { 129 | 130 | throw new Exception('None of file input'); 131 | } 132 | 133 | // Check for type of file / directory 134 | if (!is_file($sourceFile) && !is_dir($sourceFile) ) { 135 | 136 | throw new Exception('Source file is not a file or directory'); 137 | } 138 | 139 | // Check for type of link 140 | if (is_link($sourceFile)) { 141 | 142 | throw new Exception('File input is symblic link'); 143 | } 144 | 145 | // Directory locate 146 | $result = shell_exec("cd {$config['sourceFile']};"); 147 | 148 | // Git process 149 | if ($config['gitEnabled']) { 150 | 151 | echo "Processing Git...\n"; 152 | $cmd = ($config['gitCheckoutEnabled']) 153 | ? "git checkout - .;" 154 | : ""; 155 | $cmd .= ($config['gitBranch']) 156 | ? "git pull origin {$config['gitBranch']}" 157 | : "git pull"; 158 | 159 | // Shell execution 160 | $result = shell_exec($cmd); 161 | 162 | echo "/* --- Git Process Result --- */\n"; 163 | echo $result; 164 | echo "/* -------------------------- */\n"; 165 | echo "\r\n"; 166 | } 167 | 168 | // Composer process 169 | if ($config['composerEnabled']) { 170 | 171 | echo "/* --- Composer Process Start --- */\n"; 172 | $cmd = $config['composerCommand']; 173 | 174 | // Shell execution 175 | $result = shell_exec($cmd); 176 | echo $result; 177 | 178 | echo "/* --- Composer Process End --- */\n"; 179 | echo "\r\n"; 180 | } 181 | 182 | // Commands process 183 | if ($config['commandsBeforeDeploy']) { 184 | 185 | foreach ((array)$config['commandsBeforeDeploy'] as $key => $cmd) { 186 | 187 | echo "/* --- Command:{$key} Process Start --- */\n"; 188 | 189 | // Format command 190 | $cmd = "{$cmd};"; 191 | // Shell execution 192 | $result = shell_exec($cmd); 193 | echo $result; 194 | 195 | echo "/* --- Command:{$key} Process End --- */\n"; 196 | echo "\r\n"; 197 | } 198 | } 199 | 200 | // Rsync each servers 201 | foreach ($config['remoteServers'][$serverEnv] as $key => $server) { 202 | 203 | // Info display 204 | echo "/* --- Rsync Process Info --- */\n"; 205 | echo '[Process]: '.($key+1)."\n"; 206 | echo '[Group ]: '.$serverEnv."\n"; 207 | echo '[Server ]: '.$server."\n"; 208 | echo '[User ]: '.$config['remoteUser']."\n"; 209 | echo '[Source ]: '.$sourceFile."\n"; 210 | echo '[Remote ]: '.$remotePath."\n"; 211 | echo "/* -------------------------- */\n"; 212 | echo "Processing Rsync...\n"; 213 | 214 | 215 | /* Command builder */ 216 | 217 | $cmd = 'rsync ' . $config['rsyncParams']; 218 | 219 | // Add exclude 220 | $excludeFiles = $config['excludeFiles']; 221 | foreach ((array)$excludeFiles as $key => $file) { 222 | $cmd .= " --exclude \"{$file}\""; 223 | } 224 | 225 | // Rsync shell command 226 | $cmd = sprintf("%s %s %s@%s:%s", 227 | $cmd, 228 | $sourceFile, 229 | $config['remoteUser'], 230 | $server, 231 | $remotePath 232 | ); 233 | 234 | echo '[Command]: '.$cmd."\n"; 235 | 236 | // Shell execution 237 | $result = shell_exec($cmd); 238 | 239 | echo "/* --- Rsync Process Result --- */\n"; 240 | echo $result; 241 | echo "/* ---------------------------- */\n"; 242 | echo "\r\n"; 243 | 244 | sleep($config['sleepSeconds']); 245 | } 246 | 247 | echo "\r\n"; 248 | 249 | } catch (Exception $e) { 250 | 251 | die('ERROR:'.$e->getMessage()."\n"); 252 | } 253 | 254 | 255 | -------------------------------------------------------------------------------- /webhook/gitlab/index.php: -------------------------------------------------------------------------------- 1 | $inputToken, 20 | 'project' => $data['project']['path_with_namespace'], 21 | 'projectUrl' => $data['project']['url'], 22 | 'branch' => str_replace('refs/heads/', '', $data['ref']), 23 | ]; 24 | // writeLog($info);exit; 25 | 26 | // Log mode info rewrite 27 | if ($logMode) { 28 | $info = [ 29 | 'token' => isset($_GET['token']) ? $_GET['token'] : null, 30 | 'project' => $_GET['log'], 31 | 'branch' => isset($_GET['branch']) ? $_GET['branch'] : 'master', 32 | ]; 33 | } 34 | 35 | try { 36 | 37 | // Check 38 | $configList = require __DIR__. '/../../config.inc.php'; 39 | 40 | $matchedConfig = []; 41 | $errorInfo = null; 42 | 43 | foreach ($configList as $key => $config) { 44 | 45 | // Webhook setting check 46 | if (!isset($config['webhook']['enabled']) || !$config['webhook']['enabled']) { 47 | continue; 48 | } 49 | // Gitlab provider check 50 | elseif (!isset($config['webhook']['provider']) || $config['webhook']['provider']!='gitlab') { 51 | continue; 52 | } 53 | // Last mapping for project name 54 | elseif (!isset($config['webhook']['project']) || $config['webhook']['project']!=$info['project']) { 55 | continue; 56 | } 57 | // Webhook branch check 58 | elseif (isset($config['webhook']['branch']) && $config['webhook']['branch']!=$info['branch']) { 59 | $errorInfo[] = "Branch `{$info['branch']}` could not be matched from webhook config"; 60 | continue; 61 | } 62 | // Use Git branch setting while no branch setting in Webhook 63 | elseif (!isset($config['webhook']['branch']) && isset($config['git']['branch']) && $config['git']['branch']!=$info['branch']) { 64 | $errorInfo[] = "Branch `{$info['branch']}` could not be matched from Git config"; 65 | continue; 66 | } 67 | 68 | // match config 69 | $matchedConfig = $config; 70 | // For Deployer config 71 | $matchedConfig['projectKey'] = $key; 72 | 73 | break; 74 | } 75 | } catch (\Exception $e) { 76 | responeseWithPack(null, $e->getCode(), $e->getMessage()); 77 | exit; 78 | } 79 | 80 | // Matched config check 81 | if (empty($matchedConfig)) { 82 | responeseWithPack($errorInfo, 404, 'No matched config found'); 83 | exit; 84 | } 85 | // Authorization while setting token 86 | elseif (isset($matchedConfig['webhook']['token']) && $info['token'] != $matchedConfig['webhook']['token']) { 87 | responeseWithPack(['inputToken' => $info['token']], 403, 'Token is invalid'); 88 | exit; 89 | } 90 | 91 | // Log mode 92 | if ($logMode) { 93 | 94 | if (!isset($matchedConfig['webhook']['log'])) { 95 | die('Log setting is disabled'); 96 | } 97 | 98 | $logFile = is_string($matchedConfig['webhook']['log']) 99 | ? $matchedConfig['webhook']['log'] 100 | : $defaultLogFile; 101 | 102 | if (!file_exists($logFile)) { 103 | die('Log file not found'); 104 | } 105 | 106 | // Read log 107 | $oldList = json_decode(file_get_contents($logFile), true); 108 | $logList = is_array($oldList) ? $oldList : []; 109 | 110 | // Output 111 | if ($logList) { 112 | foreach ($logList as $key => $row) { 113 | echo "{$row['datetime']}
". $row['response'] ."
"; 114 | } 115 | } else { 116 | echo 'No record yet'; 117 | } 118 | 119 | exit; 120 | } 121 | 122 | /** 123 | * Fast response for webhook 124 | */ 125 | $data = []; 126 | // Provide resultUrl when webhook log is enabled 127 | if (isset($matchedConfig['webhook']['log'])) { 128 | $data['resultUrl'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") 129 | . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]" 130 | . "?log={$info['project']}&branch={$info['branch']}&token={$info['token']}"; 131 | } 132 | responeseWithPack($data, 200, 'Deployer will start processing! Check log for result information.'); 133 | ignore_user_abort(true); 134 | header('Connection: close'); 135 | flush(); 136 | fastcgi_finish_request(); 137 | 138 | /** 139 | * Bootstrap 140 | */ 141 | // Loader 142 | require __DIR__. '/../../src/ShellConsole.php'; 143 | require __DIR__. '/../../src/Deployer.php'; 144 | // Config initialized 145 | $defaultConfig = require __DIR__. '/../../src/default-config.inc.php'; 146 | $matchedConfig = array_replace_recursive($defaultConfig, $matchedConfig); 147 | // Initial Deployer 148 | $deployer = new Deployer($matchedConfig); 149 | // Run Deployer 150 | $res = $deployer->run(); 151 | 152 | if ($res && isset($matchedConfig['webhook']['log'])) { 153 | 154 | // Max rows per each log file 155 | $limit = 100; 156 | // Log file 157 | $logFile = is_string($matchedConfig['webhook']['log']) 158 | ? $matchedConfig['webhook']['log'] 159 | : $defaultLogFile; 160 | // Format 161 | $row = [ 162 | 'provider' => $config['webhook']['provider'], 163 | 'info' => $info, 164 | 'datetime' => date("Y-m-d H:i:s"), 165 | 'response' => $res, 166 | ]; 167 | // Log text 168 | $logList = []; 169 | 170 | if (file_exists($logFile)) { 171 | 172 | // Read log 173 | $oldList = json_decode(file_get_contents($logFile), true); 174 | $logList = is_array($oldList) ? $oldList : []; 175 | // Limit handling 176 | if (count($logList) >= $limit) { 177 | array_pop($logList); 178 | } 179 | } 180 | array_unshift($logList, $row); 181 | // Write back to log 182 | file_put_contents($logFile, json_encode($logList)); 183 | } 184 | 185 | /** 186 | * writeLog 187 | * 188 | * @param string $text 189 | * @return void 190 | */ 191 | function writeLog($text='no message', $writeLogFile='/tmp/deployer-php-cli.log') 192 | { 193 | $text = is_array($text) ? print_r($text, true) : $text; 194 | 195 | file_put_contents($writeLogFile, $text); 196 | } 197 | 198 | /** 199 | * Response 200 | * 201 | * @param integer $status 202 | * @param array $body 203 | * @return void 204 | */ 205 | function response($status=200, $body=[]) 206 | { 207 | http_response_code($status); 208 | header('Content-Type: application/json; charset=utf-8'); 209 | echo json_encode($body, JSON_UNESCAPED_SLASHES); 210 | } 211 | 212 | /** 213 | * Responese with pack 214 | * 215 | * @param [type] $data 216 | * @param integer $status 217 | * @param [type] $message 218 | * @return void 219 | */ 220 | function responeseWithPack($data=null, $status=200, $message=null) 221 | { 222 | $body = [ 223 | 'code' => $status, 224 | ]; 225 | // Message field 226 | if ($message) { 227 | $body['message'] = $message; 228 | } 229 | // Data field 230 | if ($data) { 231 | $body['data'] = $data; 232 | } 233 | 234 | return response($status, $body); 235 | } -------------------------------------------------------------------------------- /src/Deployer.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | 11 | /** 12 | * Deployer Core 13 | */ 14 | class Deployer 15 | { 16 | use ShellConsole; 17 | 18 | private $_config; 19 | 20 | /** 21 | * Result response 22 | * 23 | * @var string Text 24 | */ 25 | private $_response; 26 | 27 | function __construct($config) 28 | { 29 | $this->_setConfig($config); 30 | } 31 | 32 | /** 33 | * Run 34 | * 35 | * @return string Result response 36 | */ 37 | public function run() 38 | { 39 | $config = &$this->_config; 40 | 41 | // Check config 42 | $this->_checkConfig(); 43 | 44 | ob_implicit_flush(); 45 | 46 | // Local user check 47 | /** 48 | * @todo Switch user 49 | */ 50 | if ($config['user']['local'] && $config['user']['local']!=$this->_getUser()) { 51 | $this->_print("Access denied, please switch to local user: `{$config['user']['local']}` from config"); 52 | exit; 53 | } 54 | 55 | // cd into source directory 56 | $this->_cmd("cd {$this->_config['source']};"); 57 | 58 | // Project selected info 59 | $this->_result("Selected Project: {$config['projectKey']}"); 60 | 61 | // Total cost time start 62 | $startSecond = microtime(true); 63 | 64 | $this->runCommands('init'); 65 | $this->runGit(); 66 | $this->runComposer(); 67 | $this->runTest(); 68 | $this->runTests(); 69 | $this->runCommands('before'); 70 | $this->runDeploy(); 71 | $this->runCommands('after'); 72 | 73 | // Total cost time end 74 | $costSecond = abs(microtime(true) - $startSecond); 75 | $costSecond = number_format($costSecond, 2, ".", ""); 76 | $this->_result("Total Cost Time: {$costSecond}s"); 77 | 78 | return $this->_response; 79 | } 80 | 81 | /** 82 | * Git Process 83 | */ 84 | public function runGit() 85 | { 86 | if (!isset($this->_config['git'])) { 87 | return; 88 | } 89 | 90 | // Default config 91 | $defaultConfig = [ 92 | 'enabled' => false, 93 | 'path' => './', 94 | 'checkout' => true, 95 | 'branch' => 'master', 96 | 'submodule' => false, 97 | ]; 98 | 99 | // Config init 100 | $config = array_merge($defaultConfig, $this->_config['git']); 101 | 102 | // Check enabled 103 | if (!$config || empty($config['enabled']) ) { 104 | return; 105 | } 106 | 107 | // Git process 108 | $this->_verbose(""); 109 | $this->_verbose("### Git Process Start"); 110 | 111 | // Path 112 | $path = (isset($config['path'])) ? $config['path'] : './'; 113 | $path = $this->_getAbsolutePath($path); 114 | 115 | // Git Checkout 116 | if ($config['checkout']) { 117 | $result = $this->_cmd("git checkout -- .", $output, $path); 118 | // Common error check 119 | $this->checkError($result, $output); 120 | } 121 | // Git pull 122 | $cmd = ($config['branch']) 123 | ? "git pull origin {$config['branch']}" 124 | : "git pull"; 125 | $result = $this->_cmd($cmd, $output, $path); 126 | // Common error check 127 | $this->checkError($result, $output); 128 | $this->_verbose("### Git Process Pull"); 129 | $this->_verbose($output); 130 | 131 | // Git Checkout 132 | if (isset($config['submodule']) && $config['submodule']) { 133 | $result = $this->_cmd("git submodule init", $output, $path); 134 | $result = $this->_cmd("git submodule update", $output, $path); 135 | // Common error check 136 | $this->checkError($result, $output); 137 | } 138 | 139 | // Git reset commit 140 | if (isset($config['reset']) && $config['reset']) { 141 | $result = $this->_cmd("git reset --hard {$config['reset']}", $output, $path); 142 | $this->_verbose("### Git Process Reset Commit"); 143 | $this->_verbose($result); 144 | // Common error check 145 | $this->checkError($result, $output); 146 | } 147 | 148 | $this->_verbose("### /Git Process End\n"); 149 | 150 | $this->_done("Git"); 151 | } 152 | 153 | /** 154 | * Composer Process 155 | */ 156 | public function runComposer() 157 | { 158 | if (!isset($this->_config['composer'])) { 159 | return; 160 | } 161 | 162 | // Composer Config 163 | $config = &$this->_config['composer']; 164 | 165 | // Check enabled 166 | if (!$config || empty($config['enabled']) ) { 167 | return; 168 | } 169 | 170 | // Composer process 171 | $this->_verbose(""); 172 | $this->_verbose("### Composer Process Start"); 173 | 174 | // Path 175 | $path = (isset($config['path'])) ? $config['path'] : './'; 176 | // Alternative multiple composer option 177 | $paths = is_array($path) ? $path : [$path]; 178 | $isSinglePath = (count($paths)<=1) ? true : false; 179 | 180 | // Each composer path with same setting 181 | foreach ($paths as $key => $path) { 182 | 183 | $path = $this->_getAbsolutePath($path); 184 | 185 | $cmd = $config['command']; 186 | // Shell execution 187 | $result = $this->_cmd($cmd, $output, $path); 188 | 189 | $this->_verbose("### Composer Process Result"); 190 | $this->_verbose($output); 191 | 192 | /** 193 | * Check error 194 | */ 195 | if (!$result) { 196 | // Error 197 | $this->_verbose($output); 198 | // Single or multiple 199 | if ($isSinglePath) { 200 | // Single path does not show the key 201 | $this->_error("Composer"); 202 | } else { 203 | // Multiple paths shows current info 204 | $this->_error("Composer #{$key} with path: {$path}"); 205 | } 206 | } 207 | 208 | } 209 | 210 | $this->_verbose("### /Composer Process End\n"); 211 | 212 | $this->_done("Composer"); 213 | } 214 | 215 | /** 216 | * Test Process 217 | */ 218 | public function runTest($config=null) 219 | { 220 | if (!$config) { 221 | if (!isset($this->_config['test'])) { 222 | return; 223 | } 224 | 225 | // Test Config 226 | $config = &$this->_config['test']; 227 | } 228 | 229 | // Check enabled 230 | if (!$config || empty($config['enabled']) ) { 231 | return; 232 | } 233 | 234 | // Commend required 235 | if (!isset($config['command'])) { 236 | $this->_error("Test (Config `command` not found)"); 237 | } 238 | 239 | $name = (isset($config['name'])) ? $config['name'] : $config['command']; 240 | 241 | // Start process 242 | $this->_verbose(""); 243 | $this->_verbose("### Test `{$name}` Process Start"); 244 | 245 | // command 246 | $cmd = $this->_getAbsolutePath($config['command']); 247 | 248 | $configuration = (isset($config['configuration'])) ? $this->_getAbsolutePath($config['configuration']) : null; 249 | 250 | switch ($type = isset($config['type']) ? $config['type'] : null) { 251 | 252 | case 'phpunit': 253 | default: 254 | 255 | $cmd = ($configuration) ? "{$cmd} -c {$configuration}" : $cmd; 256 | break; 257 | } 258 | 259 | // Shell execution 260 | $result = $this->_cmd($cmd, $output); 261 | 262 | $this->_verbose("### Test `{$name}` Process Result"); 263 | $this->_verbose($output); 264 | 265 | // Failures check 266 | $this->checkError($result, $output); 267 | 268 | $this->_verbose("### /Test Process End\n"); 269 | 270 | $this->_done("Test `{$name}`"); 271 | } 272 | 273 | /** 274 | * Test Process 275 | */ 276 | public function runTests() 277 | { 278 | if (!isset($this->_config['tests'])) { 279 | return; 280 | } 281 | 282 | // Tests Config 283 | $configs = &$this->_config['tests']; 284 | 285 | if (!is_array($configs)) { 286 | $this->_error("Tests (Config must be array)"); 287 | } 288 | 289 | foreach ($configs as $key => $config) { 290 | $this->runTest($config); 291 | } 292 | } 293 | 294 | /** 295 | * Customized Commands Process 296 | * 297 | * @param string Trigger point 298 | */ 299 | public function runCommands($trigger) 300 | { 301 | if (!isset($this->_config['commands'])) { 302 | return; 303 | } 304 | 305 | // Commands Config 306 | $config = &$this->_config['commands']; 307 | 308 | // Check enabled 309 | if (!isset($config[$trigger]) || !is_array($config[$trigger])) { 310 | return; 311 | } 312 | 313 | // process 314 | foreach ($config[$trigger] as $key => $cmd) { 315 | 316 | if (!$cmd) { 317 | continue; 318 | } 319 | 320 | // Format compatibility 321 | $cmd = is_array($cmd) ? $cmd : ['command' => $cmd]; 322 | 323 | $this->_verbose(""); 324 | $this->_verbose("### Command:{$key} Process Start"); 325 | 326 | // Format command 327 | $command = "{$cmd['command']};"; 328 | $result = $this->_cmd($command, $output, true); 329 | 330 | // Check 331 | if (!$result) { 332 | $this->_verbose($output); 333 | $this->_error("Command:{$key}"); 334 | } 335 | 336 | $this->_verbose("### Command:{$key} Process Result"); 337 | $this->_verbose($output); 338 | $this->_verbose("### Command:{$key} Process Start"); 339 | 340 | $this->_done("Commands {$trigger}: {$key}"); 341 | } 342 | } 343 | 344 | /** 345 | * Deploy Process 346 | */ 347 | public function runDeploy() 348 | { 349 | // Config 350 | $config = isset( $this->_config['rsync']) ? $this->_config['rsync'] : []; 351 | 352 | // Default config 353 | $defaultConfig = [ 354 | 'enabled' => true, 355 | 'params' => '-av --delete', 356 | 'timeout' => 15, 357 | ]; 358 | 359 | // Config init 360 | $config = array_merge($defaultConfig, $this->_config['rsync']); 361 | 362 | // Check enabled 363 | if (!$config['enabled']) { 364 | return; 365 | } 366 | 367 | /** 368 | * Command builder 369 | */ 370 | $rsyncCmd = 'rsync ' . $config['params']; 371 | 372 | // Add exclude 373 | $excludeFiles = $this->_config['exclude']; 374 | foreach ((array)$excludeFiles as $key => $file) { 375 | $rsyncCmd .= " --exclude \"{$file}\""; 376 | } 377 | 378 | // IdentityFile 379 | $identityFile = isset($config['identityFile']) 380 | ? $config['identityFile'] 381 | : null; 382 | if ($identityFile && file_exists($identityFile)) { 383 | $rsyncCmd .= " -e \"ssh -i {$identityFile}\""; 384 | } 385 | elseif ($identityFile) { 386 | $this->_error("Deploy (IdentityFile not found: {$identityFile})"); 387 | } 388 | 389 | // Common parameters 390 | $rsyncCmd = sprintf("%s --timeout=%d %s", 391 | $rsyncCmd, 392 | $config['timeout'], 393 | $this->_config['source'] 394 | ); 395 | 396 | /** 397 | * Process 398 | */ 399 | foreach ($this->_config['servers'] as $key => $server) { 400 | 401 | // Info display 402 | $this->_verbose(""); 403 | $this->_verbose("### Rsync Process Info"); 404 | $this->_verbose('[Process]: '.($key+1)); 405 | $this->_verbose('[Server ]: '.$server); 406 | $this->_verbose('[User ]: '.$this->_config['user']['remote']); 407 | $this->_verbose('[Source ]: '.$this->_config['source']); 408 | $this->_verbose('[Remote ]: '.$this->_config['destination']); 409 | 410 | // Rsync destination building for each server 411 | $cmd = sprintf("%s --no-owner --no-group %s@%s:%s", 412 | $rsyncCmd, 413 | $this->_config['user']['remote'], 414 | $server, 415 | $this->_config['destination'] 416 | ); 417 | 418 | $this->_verbose('[Command]: '.$cmd); 419 | 420 | // Shell execution 421 | $result = $this->_cmd($cmd, $output); 422 | 423 | $this->_verbose("### Rsync Process Result"); 424 | $this->_verbose("--------------------------"); 425 | $this->_verbose($output); 426 | $this->_verbose("----------------------------"); 427 | $this->_verbose(""); 428 | 429 | /** 430 | * Check error 431 | */ 432 | // Success only: sending incremental file list 433 | if (!$result) { 434 | // Error 435 | $this->_error("Deploy to {$server}"); 436 | 437 | } else { 438 | 439 | // Sleep option per each deployed server 440 | if (isset($config['sleepSeconds'])) { 441 | 442 | sleep((int)$config['sleepSeconds']); 443 | } 444 | 445 | $this->_done("Deploy to {$server}"); 446 | } 447 | } 448 | 449 | $this->_done("Deploy"); 450 | } 451 | 452 | /** 453 | * Get project config 454 | * 455 | * @return array Config 456 | */ 457 | public function getConfig() 458 | { 459 | return $this->_config; 460 | } 461 | 462 | /** 463 | * Config setting 464 | * 465 | * @param array $config 466 | */ 467 | private function _setConfig($config) 468 | { 469 | if (!isset($config['servers']) || !$config['servers'] || !is_array($config['servers'])) { 470 | throw new Exception('Config not set: servers', 400); 471 | } 472 | 473 | if (!isset($config['source']) || !$config['source']) { 474 | throw new Exception('Config not set: source', 400); 475 | } 476 | 477 | $config['user'] = (isset($config['user'])) 478 | ? $config['user'] 479 | : []; 480 | 481 | $config['user']['local'] = is_string($config['user']) ? $config['user'] : $config['user']['local']; 482 | $config['user']['local'] = (isset($config['user']['local']) && $config['user']['local']) 483 | ? $config['user']['local'] 484 | : $this->_getUser(); 485 | 486 | $config['user']['remote'] = (isset($config['user']['remote']) && $config['user']['remote']) 487 | ? $config['user']['remote'] 488 | : $config['user']['local']; 489 | 490 | $config['destination'] = (isset($config['destination'])) 491 | ? $config['destination'] 492 | : $config['source']; 493 | 494 | return $this->_config = $config; 495 | } 496 | 497 | private function _checkConfig() 498 | { 499 | $config = &$this->_config; 500 | 501 | // Check for type of file / directory 502 | if (!is_dir($config['source']) ) { 503 | 504 | throw new Exception('Source file is not a directory (project)'); 505 | } 506 | 507 | // Check for type of link 508 | if (is_link($config['source'])) { 509 | 510 | throw new Exception('File input is symblic link'); 511 | } 512 | } 513 | 514 | /** 515 | * Response 516 | * 517 | * @param string $string 518 | */ 519 | private function _done($string) 520 | { 521 | $this->_result("Successful Excuted Task: {$string}"); 522 | } 523 | 524 | /** 525 | * Response for error 526 | * 527 | * @param string $string 528 | */ 529 | private function _error($string) 530 | { 531 | $this->_result("Failing Excuted Task: {$string}"); 532 | if (!isset($this->_config['verbose']) || !$this->_config['verbose']) { 533 | $this->_result("(Use -v --verbose parameter to display error message)"); 534 | } 535 | exit; 536 | } 537 | 538 | /** 539 | * Combined path with config source path if is relatived path 540 | * 541 | * @param $path 542 | * @return string Path 543 | */ 544 | private function _getAbsolutePath($path=null) 545 | { 546 | // Is absolute path 547 | if (strpos($path, '/')===0 && file_exists($path)) { 548 | 549 | return $path; 550 | } 551 | 552 | return ($path) ? $this->_config['source'] ."/{$path}" : $this->_config['source']; 553 | } 554 | 555 | /** 556 | * Command (Shell as default) 557 | * 558 | * @param string $cmd 559 | * @param string $resultText 560 | * @param bool|string cd into source directory first (CentOS issue), string for customization 561 | * @return mixed Response 562 | */ 563 | private function _cmd($cmd, &$resultText='', $cdSource=false) 564 | { 565 | // Clear rtrim 566 | $cmd = rtrim($cmd, ';'); 567 | 568 | if ($cdSource) { 569 | // Get path with the determination 570 | $path = ($cdSource===true) ? $this->_config['source'] : $cdSource; 571 | $cmd = "cd {$path};{$cmd}"; 572 | } 573 | 574 | return $this->_exec($cmd, $resultText); 575 | } 576 | 577 | /** 578 | * Result response 579 | * 580 | * @param string $string 581 | */ 582 | private function _result($string='') 583 | { 584 | $this->_response .= $string . "\n"; 585 | $this->_print($string); 586 | } 587 | 588 | /** 589 | * Verbose response 590 | * 591 | * @param string $string 592 | */ 593 | private function _verbose($string='') 594 | { 595 | if (isset($this->_config['verbose']) && $this->_config['verbose']) { 596 | $this->_result($string); 597 | } 598 | } 599 | 600 | /** 601 | * check error for Git 602 | * 603 | * @param boolean $result Command result 604 | * @param string $output Result text 605 | * @return void 606 | */ 607 | private function checkError($result, $output) 608 | { 609 | if (!$result) { 610 | 611 | $this->_verbose($output); 612 | $this->_error("Git"); 613 | } 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deployer PHP-CLI 2 | ================ 3 | 4 | CI/CD Deployment tool written in PHP supported for popular frameworks 5 | 6 | [![Latest Stable Version](https://poser.pugx.org/yidas/deployer-php-cli/v/stable?format=flat-square)](https://packagist.org/packages/yidas/deployer-php-cli) 7 | [![License](https://poser.pugx.org/yidas/deployer-php-cli/license?format=flat-square)](https://packagist.org/packages/yidas/deployer-php-cli) 8 | 9 | FEATURES 10 | -------- 11 | 12 | - *Deploy to **multiple** servers by **projects/groups*** 13 | 14 | - ***Yii2, Laravel, Codeigniter3** Frameworks support* 15 | 16 | - ***Pipeline support** for Git, Composer, test and customized tasks* 17 | 18 | - ***CI/CD** automation solution* 19 | 20 | Helping developers to deploy codes from local instance to remote instances. 21 | 22 | --- 23 | 24 | OUTLINE 25 | ------- 26 | 27 | * [Demonstration](#demonstration) 28 | * [Requirements](#requirements) 29 | * [Installation](#installation) 30 | - [Composer Installation](#composer-installation) 31 | - [Wget Installation](#wget-installation) 32 | - [Make Command](#make-command) 33 | - [Startup](#startup) 34 | - [Upgrade](#upgrade) 35 | * [Configuration](#configuration) 36 | - [Project Setting](#project-setting) 37 | - [Config Options](#config-options) 38 | - [Git](#git) 39 | - [Composer](#composer) 40 | - [Test](#test) 41 | - [Tests](#tests) 42 | - [Rsync](#rsync) 43 | - [Commands](#commands) 44 | - [Example](#example) 45 | * [Usage](#usage) 46 | - [Interactive Project Select](#interactive-project-select) 47 | - [Non-Interactive Project Select](#non-interactive-project-select) 48 | - [Skip Flows](#skip-flows) 49 | - [Revert & Reset back](#revert--reset-back) 50 | * [Implementation](#implementation) 51 | - [Permissions Handling](#permissions-handling) 52 | * [CI/CD](#cicd) 53 | - [Webhook](#webhook) 54 | - [PHP Web Setting](#php-web-setting) 55 | - [Gitlab](#gitlab) 56 | * [Additions](#additions) 57 | - [Rsync without Password](#rsync-without-password) 58 | - [Save Binary Encode File](#save-binary-encode-file) 59 | - [Yii2 Deployment](#yii2-deployment) 60 | - [Minify/Uglify by Gulp](#minifyuglify-by-gulp) 61 | 62 | --- 63 | 64 | DEMONSTRATION 65 | ------------- 66 | 67 | ![Basic Flow](https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/yidas/deployer-php-cli/master/img/sequence-diagram.plantuml) 68 | 69 | ### Command Line 70 | 71 | Deploy local project to remote servers by just executing the deployer in command after installation: 72 | 73 | ``` 74 | $ deployer 75 | ``` 76 | 77 | > Alternatively, you could call the original bootstrap: `$ ./deployer`, `$ php ./deployer` 78 | 79 | The interactive result could like be: 80 | ``` 81 | $ deployer 82 | 83 | Your available projects in configuration: 84 | [0] your.project.com 85 | [1] second.project.com 86 | [2] other.site.com 87 | 88 | Please select a project [number or project, Ctrl+C to quit]:0 89 | 90 | Selected Project: your.project.com 91 | Successful Excuted Task: Git 92 | Successful Excuted Task: Composer 93 | Successful Excuted Task: Composer 94 | Successful Excuted Task: Test UnitTest 95 | Successful Excuted Task: Commands before: Minify assets 96 | Successful Excuted Task: Deploy to 127.0.0.11 97 | Successful Excuted Task: Deploy to 127.0.0.12 98 | Successful Excuted Task: Deploy 99 | Successful Excuted Task: Commands after: Email notification 100 | ``` 101 | 102 | Or you could run by non-interactive mode with the same purpose: 103 | 104 | ``` 105 | $ deployer --project="your.project.com" 106 | ``` 107 | 108 | --- 109 | 110 | REQUIREMENTS 111 | ------------ 112 | 113 | This library requires the following: 114 | 115 | - PHP(CLI) 5.4.0+ 116 | - RSYNC 117 | 118 | --- 119 | 120 | INSTALLATION 121 | ------------ 122 | 123 | ### Composer Installation 124 | 125 | Using Composer by `sudoer` or `root` to install is the easiest way with auto-installer: 126 | 127 | ``` 128 | composer create-project --prefer-dist yidas/deployer-php-cli 129 | ``` 130 | 131 | ### Wget Installation 132 | 133 | You could see [Release](https://github.com/yidas/deployer-php-cli/releases) for picking up the package with version, for example: 134 | 135 | ``` 136 | $ wget https://github.com/yidas/deployer-php-cli/archive/master.tar.gz -O deployer-php-cli.tar.gz 137 | ``` 138 | 139 | After download, uncompress the package: 140 | 141 | ``` 142 | $ tar -zxvf deployer-php-cli.tar.gz 143 | ``` 144 | 145 | > In addition, you can rename the unzipped folder by `mkdir deployer-php-cli && tar -zxvf deployer-php-cli.tar.gz --strip-components 1 -C deployer-php-cli` 146 | 147 | #### Make Command 148 | 149 | To make a command for deployer, if the package folder is `deployer-php-cli` then create a symbol by following command: 150 | 151 | ``` 152 | $ sudo chmod +x $(pwd -L)/deployer-php-cli/deployer 153 | $ sudo ln -s $(pwd -L)/deployer-php-cli/deployer /usr/bin/deployer 154 | ``` 155 | 156 | ### Startup 157 | 158 | After installation, you could start to set up the `config.inc.php` for deployer, and enjoy to use: 159 | 160 | ``` 161 | $ deployer 162 | ``` 163 | 164 | ### Upgrade 165 | 166 | To upgrade, you could re-install the deployer and copy the old `config.inc.php` to the new one, for example: 167 | 168 | ``` 169 | $ cp ./deployer-php-cli/config.inc.php ./ 170 | $ rm -r deployer-php-cli 171 | $ composer create-project --prefer-dist yidas/deployer-php-cli 172 | $ mv ./config.inc.php ./deployer-php-cli 173 | ``` 174 | 175 | --- 176 | 177 | CONFIGURATION 178 | ------------- 179 | 180 | ### Project Setting: 181 | 182 | You need to set up the projects configuration such as servers, source and destination in `config.inc.php` file: 183 | 184 | ```php 185 | [ 190 | 'servers' => [ 191 | '127.0.0.1', 192 | ], 193 | 'source' => '/home/user/project', 194 | 'destination' => '/var/www/html/prod/', 195 | ], 196 | ]; 197 | ``` 198 | 199 | > You could refer [config.inc.php](https://github.com/yidas/deployer-php-cli/blob/master/config.inc.php) file as an example.. 200 | 201 | ### Config Options: 202 | 203 | Configuration provides many features' setting, you could customize and pick up the setting you need. 204 | 205 | |Key|Type|Description| 206 | |:-|:-|:-| 207 | |**servers**|array|Distant server host list| 208 | |**user**|array\|string|Local/Remote server user, auto detect current user if empty| 209 | |**source**|string|Local directory for deploy, use `/` as end means `*` | 210 | |**destination**|string|Remote path for synchronism| 211 | |**exclude**|array|Excluded files based on sourceFile path| 212 | |verbose|bool|Enable verbose with more infomation or not| 213 | 214 | #### Git 215 | 216 | To use Git into deploy task, you need to init or clone Git to the source directory at the first time: 217 | 218 | ``` 219 | $ git clone git@gitlab.com:username/project-to-deploy.git sourceDir 220 | ``` 221 | 222 | |Key|Type|Description| 223 | |:-|:-|:-| 224 | |enabled|bool|Enable git or not| 225 | |checkout|bool|Execute git checkout -- . before git pull | 226 | |branch|string|Branch name for git pull, pull default branch if empty | 227 | |submodule|bool|Git submodule enabled | 228 | 229 | #### Composer 230 | 231 | To use Composer into deploy task, make sure that there are composer files in the source directory. 232 | 233 | |Key|Type|Description| 234 | |:-|:-|:-| 235 | |enabled|bool|Enable Composer or not| 236 | |path|string|Composer executing relative path which supports multiple array paths| 237 | |command|string|Update command likes `composer update`| 238 | 239 | #### Test 240 | 241 | To use Test into deploy task, make sure that there are test configuration in the source directory. 242 | 243 | |Key|Type|Description| 244 | |:-|:-|:-| 245 | |enabled|bool|Enable Test or not| 246 | |name|string|The test name for display| 247 | |type|string|Test type, support `phpunit`.| 248 | |command|string|The test bootstrap command supported relative filepath such as `./vendor/bin/phpunit`| 249 | |configuration|string|The test configuration file supported relative filepath such as `./phpunit.xml`| 250 | 251 | #### Tests 252 | 253 | For multiple test tasks, using array to declare each [test options](#test): 254 | 255 | ```php 256 | return [ 257 | 'default' => [ 258 | 'tests' => [ 259 | [ 260 | 'name' => 'Test Task 1', 261 | // ... 262 | ], 263 | [ 264 | 'name' => 'Test Task 2', 265 | // ... 266 | ], 267 | ], 268 | // ... 269 | ``` 270 | 271 | #### Rsync 272 | 273 | |Key|Type|Description| 274 | |:-|:-|:-| 275 | |enabled|bool|Enable rsync or not| 276 | |params|string|Addition params of rsync command| 277 | |timeout|int|Timeout seconds of each rsync connections| 278 | |sleepSeconds|int|Seconds waiting of each rsync connections| 279 | |identityFile|string|Identity file path for appling rsync| 280 | 281 | #### Commands 282 | 283 | Commands provides you to customize deploy tasks with many trigger hooks. 284 | 285 | |Key|Type|Description| 286 | |:-|:-|:-| 287 | |init|array|Addition commands triggered at initialization| 288 | |before|array|Addition commands triggered before deploying| 289 | |after|array|Addition commands triggered after deploying| 290 | 291 | ### Example 292 | 293 | * Copy `project` directory form `/var/www/html/` to destination under `/var/www/html/test/`: 294 | 295 | ```php 296 | 'source' => '/var/www/html/project', 297 | 'destination' => '/var/www/html/test/', 298 | ``` 299 | 300 | * Copy all files (`*`) form `/var/www/html/project/` to destination under `/var/www/html/test/`: 301 | 302 | ```php 303 | 'source' => '/var/www/html/project/', 304 | 'destination' => '/var/www/html/test/', 305 | ``` 306 | 307 | --- 308 | 309 | USAGE 310 | ----- 311 | 312 | ``` 313 | Usage: 314 | deployer [options] [arguments] 315 | ./deployer [options] [arguments] 316 | 317 | Options: 318 | -h, --help Display this help message 319 | --version Show the current version of the application 320 | -p, --project Project key by configuration for deployment 321 | --config Show the seleted project configuration 322 | --configuration 323 | --skip-git Force to skip Git process 324 | --skip-composer Force to skip Composer process 325 | --git-reset Git reset to given commit with --hard option 326 | -v, --verbose Increase the verbosity of messages 327 | ``` 328 | 329 | ### Interactive Project Select 330 | 331 | ``` 332 | $ deployer 333 | 334 | Your available projects in configuration: 335 | [0] default 336 | [1] your.project.com 337 | 338 | Please select a project [number or project, Ctrl+C to quit]:your.project.com 339 | 340 | Selected Project: your.project.com 341 | Successful Excuted Task: Git 342 | Successful Excuted Task: Composer 343 | Successful Excuted Task: Deploy to 127.0.0.11 344 | Successful Excuted Task: Deploy 345 | ``` 346 | 347 | ### Non-Interactive Project Select 348 | 349 | ``` 350 | $ deployer --project="your.project.com" 351 | ``` 352 | 353 | ### Skip Flows 354 | 355 | You could force to skip flows such as Git and Composer even when you enable then in config. 356 | 357 | ``` 358 | $ deployer --project="default" --skip-git --skip-composer 359 | ``` 360 | 361 | ### Revert & Reset back 362 | 363 | You could reset git to specified commit by using `--git-reset` option when you get trouble after newest release. 364 | 365 | ``` 366 | $ deployer --project="default" --git-reset="79616d" 367 | ``` 368 | 369 | > This option is same as executing `git reset --hard 79616d` in source project. 370 | 371 | --- 372 | 373 | IMPLEMENTATION 374 | -------------- 375 | 376 | Assuming `project1` is the developing project which you want to deploy. 377 | 378 | Developers must has their own site to develop, for example: 379 | 380 | ``` 381 | # Dev host 382 | /var/www/html/dev/nick/project1 383 | /var/www/html/dev/eric/project1 384 | ``` 385 | 386 | In general, you would has stage `project1` which the files are same as production: 387 | 388 | ``` 389 | # Dev/Stage host 390 | /var/www/html/project1 391 | ``` 392 | 393 | The purpose is that production files need to be synchronous from stage: 394 | 395 | ``` 396 | # Production host 397 | /var/www/html/project1 398 | ``` 399 | 400 | This tool regard stage project as `source`, which means production refers to `destination`, so the config file could like: 401 | 402 | ```php 403 | return [ 404 | 'project1' => [ 405 | ... 406 | 'source' => '/var/www/html/project1', 407 | 'destination' => '/var/www/html/', 408 | ... 409 | ``` 410 | 411 | After running this tool to deploy `project1`, the stage project's files would execute processes likes `git pull` then synchronise to production. 412 | 413 | 414 | ### Permissions Handling 415 | 416 | ##### 1. Local and Remote Users 417 | 418 | You could create a user on local for runing Deployer with `umask 002`. It will run process by the local user you set even you run Deployer by root: 419 | 420 | ```php 421 | return [ 422 | 'project1' => [ 423 | 'user' => [ 424 | 'local' => 'deployer', 425 | 'remote' => 'deployer', 426 | ], 427 | ... 428 | ``` 429 | 430 | ##### 2. Application File Permissions 431 | 432 | Deployer uses `rsync` to deploy local source project to remote ***without*** `--no-perms`, which means that the source files' permission would keep on remote, but the files' owner would re-generate by remote user including `root` with `--no-owner --no-group`. 433 | 434 | On the remote user, you could set the user's default groud ID to `www-data` in `/etc/passwd`, which the ***local user*** generates `664/775` mod files to deploy for ***remote*** `www-data` access. 435 | 436 | > For local user, `umask 002` could be set in `~/.bashrc` or global. Note that the permission need to apply for source files such as init from Git clone. 437 | 438 | --- 439 | 440 | CI/CD 441 | ----- 442 | 443 | ### Webhook 444 | 445 | Deployer provides webhook feature for triggering project deployment by any webhook service such as Gitlab. 446 | 447 | To use webhook, you need add webhook setting into the projects you needed in `config.inc.php`: 448 | 449 | ```php 450 | return [ 451 | 'project' => [ 452 | // ... 453 | 'webhook' => [ 454 | 'enabled' => true, 455 | 'provider' => 'gitlab', 456 | 'project' => 'yidas/deployer-php-cli', 457 | 'token' => 'da39a3ee5e6b4b0d3255bfef95601890afd80709', 458 | 'branch' => 'release', 459 | 'log' => '/tmp/deployer-webhook-project.log' 460 | ], 461 | ], 462 | ]; 463 | ``` 464 | 465 | |Key|Type|Description| 466 | |:-|:-|:-| 467 | |enabled|bool|Enable Webhook or not| 468 | |provider|string|Webhook provider such as `gitlab`| 469 | |project|string|Provider's project name likes `username/project`| 470 | |token|string|Webhook secret token| 471 | |branch|string|Listening branch for push event| 472 | |log|bool\|string|Enabled log and specify the log file| 473 | 474 | #### PHP Web Setting 475 | 476 | Deployer need a user to excute deployment, and the user is usually not the PHP web user. 477 | 478 | For PHP-FPM, you could add a new PHP pool socket with the current user setting for the webhook site, for example `/etc/php/fpm/pool.d/deployer.conf`: 479 | 480 | ```php 481 | [deployer] 482 | 483 | user = deployer 484 | group = www-data 485 | 486 | listen = /run/php/php7.0-fpm_deployer.sock 487 | ``` 488 | 489 | Then give the new socket to the webhook server setting, for Nginx eaxmple `/etc/nginx/site-enabled/webhook`: 490 | 491 | ```nginx 492 | server_name webhook.your.com; 493 | root /srv/deployer/deployer-php-cli/webhook; 494 | 495 | location ~ \.php$ { 496 | include snippets/fastcgi-php.conf; 497 | fastcgi_param SCRIPT_FILENAME $request_filename; 498 | fastcgi_pass unix:/run/php/php7.0-fpm_deployer.sock; 499 | } 500 | ``` 501 | 502 | After a successful webhook, Deployer would prepare to process while responding the status and the result url for checking the deployment result. 503 | 504 | > Note: The `PATH` environment variable between Shell and PHP should be set to the same to prevent any unexpected problems. 505 | 506 | #### Gitlab 507 | 508 | - Prividor key: `gitlab` 509 | 510 | According to above Nginx website setting, the webhook URL could be `https://webhook.your.com/gitlab`. After setting `config.inc.php` and setting up scecret token, you could give a push event to go! 511 | 512 | 513 | 514 | > Note: Default setting is listen `release` branch's push event to trigger. 515 | 516 | To browse the web page for result log report, enter the same webhook URL with `log` and `token` parameters to access. 517 | For example: `https://webhook.your.com/gitlab?log={project-name}&token={project-token}` 518 | 519 | --- 520 | 521 | ADDITIONS 522 | --------- 523 | 524 | ### Rsync without Password: 525 | 526 | You can put your local user's SSH public key to destination server user for authorization. 527 | ``` 528 | .ssh/id_rsa.pub >> .ssh/authorized_keys 529 | ``` 530 | 531 | ### Save Binary Encode File: 532 | 533 | 534 | While excuting script, if you get the error like `Exception: Zend Extension ./deployer does not exist`, you may save the script file with binary encode, which could done by using `vim`: 535 | 536 | ``` 537 | :set ff=unix 538 | ``` 539 | 540 | ### Yii2 Deployment 541 | 542 | For `yii2-app-advanced`, you need to enable Composer and set yii2 init command in `config.inc.php`: 543 | 544 | ```php 545 | 'composer' => [ 546 | 'enabled' => true, 547 | ], 548 | 'commands' => [ 549 | 'before' => [ 550 | 'yii2 init prod' => './init --env=Production --overwrite=All', 551 | ], 552 | ], 553 | ``` 554 | 555 | ### Minify/Uglify by Gulp 556 | 557 | #### 1. Install NPM, for Debian/Ubuntu: 558 | 559 | ``` 560 | apt-get install npm 561 | ``` 562 | 563 | #### 2. Install Gulp by NPM 564 | 565 | ``` 566 | npm install -g gulp 567 | ``` 568 | 569 | #### 3. Create Gulp Project 570 | 571 | ``` 572 | cd /srv/tools/minify-project 573 | npm init 574 | npm install gulp --save-dev 575 | touch gulpfile.js 576 | ``` 577 | 578 | #### 4. Set Gulp with packages 579 | 580 | Package: [gulp-uglify](https://www.npmjs.com/package/gulp-uglify) 581 | 582 | ``` 583 | $ npm install gulp-uglify --save-dev 584 | $ npm install pump --save-dev 585 | ``` 586 | 587 | `gulpfile.js`: 588 | 589 | ```javascript 590 | var gulp = require('gulp'); 591 | var uglify = require('gulp-uglify'); 592 | var pump = require('pump'); 593 | var assetPath = '/srv/your.project.com/assets/js'; 594 | 595 | gulp.task('compress', function (callback) { 596 | pump([ 597 | gulp.src(assetPath+'/**/*.js'), 598 | uglify(), 599 | gulp.dest(assetPath) 600 | ], 601 | callback 602 | ); 603 | }); 604 | ``` 605 | 606 | #### 5. Set Gulp Process into Deployer 607 | 608 | ``` 609 | 'source' => '/srv/project', 610 | 'commands' => [ 611 | 'before' => [ 612 | 'Minify inner JS' => [ 613 | 'command' => 'cd /srv/tools/minify-project; gulp compress', 614 | ], 615 | ], 616 | ], 617 | ``` 618 | 619 | 620 | --------------------------------------------------------------------------------