├── Installer.php ├── LICENSE ├── README.md ├── composer.json ├── config.inc.php ├── deployer ├── img ├── cicd-gitlab-webhook.png └── sequence-diagram.plantuml ├── src ├── App.php ├── Deployer.php ├── GetOpt.php ├── ShellConsole.php ├── default-config.inc.php └── views │ └── help.php ├── test ├── config.inc.php └── deployer ├── tools ├── README.md ├── deployer └── mirror └── webhook ├── bitbucket └── index.php ├── gitlab └── index.php └── index.html /Installer.php: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config.inc.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /img/cicd-gitlab-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yidas/deployer-php-cli/967258859b3ff4f166c46d5ff1c6158531aa7bf4/img/cicd-gitlab-webhook.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/deployer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 3 | run($configList, $argv); 27 | 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------