├── .gitignore ├── AutoGitPull.sublime-project ├── README.md ├── composer.json ├── composer.lock ├── deploy.example.php ├── scripts └── git-pull.sh └── src └── Deployer.php /.gitignore: -------------------------------------------------------------------------------- 1 | deploy.php 2 | -------------------------------------------------------------------------------- /AutoGitPull.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": "." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auto git pull 2 | 3 | Automatically pull when changes are pushed to a Git repository. 4 | (Actually it does a `git fetch` followed by `git reset` but that wasn't as catchy.) 5 | 6 | ## About 7 | 8 | There are two important parts: 9 | * A PHP script which Bitbucket or GitHub will automatically send a request to when you push. (`http://mysite/deploy.php` in the examples below) 10 | * A shell script which does the actual pulling. ([`scripts/git-pull.sh`](scripts/git-pull.sh)) 11 | 12 | The reason for the separation is so you don't need to grant the web user write permission to your files. You just need to allow it to run the one script as a user that does have write permission. 13 | 14 | ## Setup 15 | 16 | * Install the latest version with `composer require tmd/auto-git-pull` 17 | 18 | * Make the pull script executable (you need to do this if you update the package as well): 19 | ```bash 20 | chmod +x vendor/tmd/auto-git-pull/scripts/git-pull.sh 21 | ``` 22 | You can have this automatically happen by adding this to your `composer.json`: 23 | ``` 24 | "scripts": { 25 | "post-install-cmd": [ 26 | "chmod +x vendor/tmd/auto-git-pull/scripts/git-pull.sh" 27 | ] 28 | } 29 | ``` 30 | 31 | * Create a publicly accessible URL on your site which will be called by GitHub/Bitbucket and run the deployment (e.g. `http://mysite.com/deploy.php`) and set the parameters as appropriate. 32 | 33 | Example showing all the options that can be given and their default values: 34 | The only required option is `directory` 35 | ```php 36 | use Tmd\AutoGitPull\Deployer; 37 | 38 | require 'vendor/autoload.php'; 39 | 40 | $deployer = new Deployer([ 41 | // IP addresses that are allowed to trigger the pull 42 | // (CLI is always allowed) 43 | 'allowedIpRanges' => [ 44 | '131.103.20.160/27', // Bitbucket 45 | '165.254.145.0/26', // Bitbucket 46 | '104.192.143.0/24', // Bitbucket 47 | '104.192.143.192/28', // Bitbucket (Dec 2015) 48 | '104.192.143.208/28', // Bitbucket (Dec 2015) 49 | '192.30.252.0/22', // GitHub 50 | ], 51 | 52 | // These are added to the allowedIpRanges array 53 | // to avoid having to define the Bitbucket/GitHub IPs in your own code 54 | 'additionalAllowedIpRanges' => [ 55 | '192.168.0.2/24' 56 | ], 57 | 58 | // Git branch to reset to 59 | 'branch' => 'master', 60 | 61 | // User to run the script as 62 | 'deployUser' => 'anthony', 63 | 64 | // Directory of the repository 65 | 'directory' => '/var/www/mysite/', 66 | 67 | // Path to the pull script 68 | // (You can provide your own script instead) 69 | 'pullScriptPath' => __DIR__ . '/scripts/git-pull.sh', 70 | 71 | // Git remote to fetch from 72 | 'remote' => 'origin' 73 | ]); 74 | 75 | $deployer->postDeployCallback = function () { 76 | echo 'Yay!'; 77 | }; 78 | 79 | $deployer->deploy(); 80 | ``` 81 | 82 | Example in Laravel showing minimal options: 83 | ```php 84 | Route::post('deploy', function() 85 | { 86 | $deployer = new \Tmd\AutoGitPull\Deployer(array( 87 | 'directory' => '/var/www/mysite/' 88 | )); 89 | $deployer->deploy(); 90 | }); 91 | ``` 92 | 93 | Example with logging: 94 | ```php 95 | use Monolog\Logger; 96 | use Monolog\Handler\FingersCrossedHandler; 97 | use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; 98 | use Monolog\Handler\RotatingFileHandler; 99 | use Monolog\Handler\NativeMailerHandler; 100 | use Monolog\Handler\StreamHandler; 101 | use Tmd\AutoGitPull\Deployer; 102 | 103 | require 'vendor/autoload.php'; 104 | 105 | $deployer = new Deployer([ 106 | 'directory' => '/var/www/mysite/' 107 | ]); 108 | 109 | $logger = new Logger('deployment'); 110 | 111 | // Output log messages to screen 112 | $logger->pushHandler( 113 | new StreamHandler("php://output") 114 | ); 115 | 116 | // Write all log messages to a log file 117 | $logger->pushHandler( 118 | new RotatingFileHandler('/var/log/mysite-deploy.log') 119 | ); 120 | 121 | // Send an email if there's an error 122 | $logger->pushHandler( 123 | new FingersCrossedHandler( 124 | new NativeMailerHandler('anthony@example.com', 'Deployment Failed', 'anthony@localhost', Logger::DEBUG), 125 | new ErrorLevelActivationStrategy(Logger::ERROR) 126 | ) 127 | ); 128 | 129 | $deployer->setLogger($logger); 130 | 131 | $deployer->deploy(); 132 | ``` 133 | 134 | * Add the hook on Bitbucket/GitHub to run the script: 135 | 136 | ![Add bitbucket deploy hook](http://img.ctrlv.in/img/53038a61539f9.png) 137 | 138 | 139 | ### If the web server user does not have write permissions on the directory 140 | 141 | If your webserver runs as a different user than the owner of the files (as is best practise) you need to allow the webserver to do the pull. 142 | 143 | * Allow the web server user to run the pull script as a user with write permissions: 144 | 145 | ```bash 146 | sudo visudo 147 | 148 | # Add the line: 149 | # (Edit users and path as appropriate) 150 | # www-data = User the PHP script runs as 151 | # anthony = User the shell script needs to run as to write to the directory 152 | # /var/www/mysite/vendor/tmd/auto-git-pull/scripts/git-pull.sh = Path to shell script 153 | 154 | www-data ALL=(anthony) NOPASSWD: /var/www/mysite/vendor/tmd/auto-git-pull/scripts/git-pull.sh 155 | ``` 156 | 157 | * Set the user to run the pull as in the parameters: 158 | ```php 159 | $deployer = new \Tmd\AutoGitPull\Deployer(array( 160 | 'deployUser' => 'anthony', 161 | // ... 162 | )); 163 | ``` 164 | 165 | 166 | ### If your repository is private 167 | 168 | You need to setup a deployment key so the pull can happen without a password being entered. 169 | 170 | * Generate an sshkey using `ssh-keygen` *for the user that will run the pull script (e.g. `www-data`). 171 | 172 | * Follow these instructions to add a deployment key to the git repository: 173 | 174 | Bitbucket: https://confluence.atlassian.com/bitbucket/use-deployment-keys-294486051.html 175 | GitHub: https://developer.github.com/guides/managing-deploy-keys/#deploy-keys 176 | 177 | * Change your git remote url from HTTPS to SSH if necessary: 178 | ``` 179 | cd /var/www/mysite 180 | git remote -v 181 | ``` 182 | 183 | If your output looks like this, you're using HTTPS: 184 | ``` 185 | origin https://bitbucket.org/me/mysite.git (fetch) 186 | origin https://bitbucket.org/me/mysite.git (push) 187 | ``` 188 | 189 | Change it to use ssh, like this: 190 | ``` 191 | git remote set-url origin git@bitbucket.org:me/mysite.git 192 | ``` 193 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmd/auto-git-pull", 3 | "description": "Automatically pulls a git repository when commits are pushed.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Anthony Kuske", 8 | "homepage": "http://www.anthonykuske.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Tmd\\AutoGitPull\\": "src/" 14 | } 15 | }, 16 | "scripts": { 17 | "post-install-cmd": [ 18 | "chmod +x ./scripts/git-pull.sh" 19 | ], 20 | "post-update-cmd": [ 21 | "chmod +x ./scripts/git-pull.sh" 22 | ] 23 | }, 24 | "require": { 25 | "monolog/monolog": "^1.17" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "4ec9dfe28d043597205e877f47ab1fb8", 8 | "content-hash": "98681dbfb43c0b85a09cdcc7c0a81feb", 9 | "packages": [ 10 | { 11 | "name": "monolog/monolog", 12 | "version": "1.17.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Seldaek/monolog.git", 16 | "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bee7f0dc9c3e0b69a6039697533dca1e845c8c24", 21 | "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.3.0", 26 | "psr/log": "~1.0" 27 | }, 28 | "provide": { 29 | "psr/log-implementation": "1.0.0" 30 | }, 31 | "require-dev": { 32 | "aws/aws-sdk-php": "^2.4.9", 33 | "doctrine/couchdb": "~1.0@dev", 34 | "graylog2/gelf-php": "~1.0", 35 | "jakub-onderka/php-parallel-lint": "0.9", 36 | "php-console/php-console": "^3.1.3", 37 | "phpunit/phpunit": "~4.5", 38 | "phpunit/phpunit-mock-objects": "2.3.0", 39 | "raven/raven": "^0.13", 40 | "ruflin/elastica": ">=0.90 <3.0", 41 | "swiftmailer/swiftmailer": "~5.3", 42 | "videlalvaro/php-amqplib": "~2.4" 43 | }, 44 | "suggest": { 45 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 46 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 47 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 48 | "ext-mongo": "Allow sending log messages to a MongoDB server", 49 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 50 | "php-console/php-console": "Allow sending log messages to Google Chrome", 51 | "raven/raven": "Allow sending log messages to a Sentry server", 52 | "rollbar/rollbar": "Allow sending log messages to Rollbar", 53 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server", 54 | "videlalvaro/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib" 55 | }, 56 | "type": "library", 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "1.16.x-dev" 60 | } 61 | }, 62 | "autoload": { 63 | "psr-4": { 64 | "Monolog\\": "src/Monolog" 65 | } 66 | }, 67 | "notification-url": "https://packagist.org/downloads/", 68 | "license": [ 69 | "MIT" 70 | ], 71 | "authors": [ 72 | { 73 | "name": "Jordi Boggiano", 74 | "email": "j.boggiano@seld.be", 75 | "homepage": "http://seld.be" 76 | } 77 | ], 78 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 79 | "homepage": "http://github.com/Seldaek/monolog", 80 | "keywords": [ 81 | "log", 82 | "logging", 83 | "psr-3" 84 | ], 85 | "time": "2015-10-14 12:51:02" 86 | }, 87 | { 88 | "name": "psr/log", 89 | "version": "1.0.0", 90 | "source": { 91 | "type": "git", 92 | "url": "https://github.com/php-fig/log.git", 93 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" 94 | }, 95 | "dist": { 96 | "type": "zip", 97 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", 98 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", 99 | "shasum": "" 100 | }, 101 | "type": "library", 102 | "autoload": { 103 | "psr-0": { 104 | "Psr\\Log\\": "" 105 | } 106 | }, 107 | "notification-url": "https://packagist.org/downloads/", 108 | "license": [ 109 | "MIT" 110 | ], 111 | "authors": [ 112 | { 113 | "name": "PHP-FIG", 114 | "homepage": "http://www.php-fig.org/" 115 | } 116 | ], 117 | "description": "Common interface for logging libraries", 118 | "keywords": [ 119 | "log", 120 | "psr", 121 | "psr-3" 122 | ], 123 | "time": "2012-12-21 11:40:51" 124 | } 125 | ], 126 | "packages-dev": [], 127 | "aliases": [], 128 | "minimum-stability": "stable", 129 | "stability-flags": [], 130 | "prefer-stable": false, 131 | "prefer-lowest": false, 132 | "platform": [], 133 | "platform-dev": [] 134 | } 135 | -------------------------------------------------------------------------------- /deploy.example.php: -------------------------------------------------------------------------------- 1 | '/var/www/mysite/' 15 | ]); 16 | 17 | $logger = new Logger('deployment'); 18 | 19 | // Output log messages to screen 20 | $logger->pushHandler( 21 | new StreamHandler("php://output") 22 | ); 23 | 24 | // Write all log messages to a log file 25 | $logger->pushHandler( 26 | new RotatingFileHandler('/var/log/mysite-deploy.log') 27 | ); 28 | 29 | // Send an email if there's an error 30 | $logger->pushHandler( 31 | new FingersCrossedHandler( 32 | new NativeMailerHandler('anthony@example.com', 'Deployment Failed', 'anthony@localhost', Logger::DEBUG), 33 | new ErrorLevelActivationStrategy(Logger::ERROR) 34 | ) 35 | ); 36 | 37 | $deployer->setLogger($logger); 38 | 39 | $deployer->deploy(); -------------------------------------------------------------------------------- /scripts/git-pull.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | branch=master 4 | directory=. 5 | remote=origin 6 | 7 | while getopts b:d:r: opt; do 8 | case $opt in 9 | b) 10 | branch=$OPTARG 11 | ;; 12 | d) 13 | directory=$OPTARG 14 | ;; 15 | r) 16 | remote=$OPTARG 17 | ;; 18 | esac 19 | done 20 | 21 | shift $((OPTIND - 1)) 22 | 23 | echo "cd $directory" || { echo 'cd failed' ; exit 1; } 24 | cd $directory || exit 1 25 | 26 | #Download changes from origin 27 | echo "git fetch $remote 2>&1" 28 | git fetch $remote 2>&1 || { echo 'fetch failed' ; exit 1; } 29 | 30 | #Discard local changes and use latest from remote 31 | echo "git reset --hard $remote/$branch 2>&1" 32 | git reset --hard $remote/$branch || { echo 'reset failed' ; exit 1; } 33 | 34 | echo "git submodule init" 35 | git submodule init || { echo 'submodule init failed' ; exit 1; } 36 | 37 | echo "git submodule update" 38 | git submodule update || { echo 'submodule update failed' ; exit 1; } 39 | 40 | if [ -f "composer.json" ]; then 41 | echo "composer install" 42 | composer install || { echo 'composer install failed' ; exit 1; } 43 | fi 44 | -------------------------------------------------------------------------------- /src/Deployer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | /** 26 | * Automatic git deployment 27 | * 28 | * Use this code as a hook after pushing your code to a git repo 29 | * and it will automatically pull the new commits onto your server. 30 | * Built for use with BitBucket but it should be easily adaptable to work 31 | * with GitHub. 32 | * 33 | * TODO: Check which branch was pushed to (currently it pulls no 34 | * matter what branch was pushed to) 35 | * 36 | * Based on deployment script by Iain Gray igray@itgassociates.com 37 | * https://bitbucket.org/itjgray/bitbucket-php-deploy.git 38 | * 39 | * Based on deployment script by Brandon Summers 40 | * http://brandonsummers.name/blog/2012/02/10/using-bitbucket-for-automated-deployments/ 41 | * 42 | */ 43 | 44 | namespace Tmd\AutoGitPull; 45 | 46 | use Closure; 47 | use Exception; 48 | use Monolog\Logger; 49 | 50 | class Deployer 51 | { 52 | // User options... 53 | 54 | /** 55 | * Which IPs can trigger the deployment? 56 | * (PHP CLI is always allowed) 57 | * 58 | * Bitbucket IPs were found here on Oct 26th 2015: 59 | * https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html 60 | * and here on Dec 11th 2015: 61 | * https://blog.bitbucket.org/2015/12/03/making-bitbuckets-network-better-faster-and-ready-to-grow/ 62 | * 63 | * GitHub IPs where found here on Oct 26th 2015: 64 | * https://help.github.com/articles/what-ip-addresses-does-github-use-that-i-should-whitelist/ 65 | * 66 | * @var array 67 | */ 68 | public $allowedIpRanges = array( 69 | '131.103.20.160/27', // Bitbucket 70 | '165.254.145.0/26', // Bitbucket 71 | '104.192.143.0/24', // Bitbucket 72 | '104.192.143.192/28', // Bitbucket (Dec 2015) 73 | '104.192.143.208/28', // Bitbucket (Dec 2015) 74 | '192.30.252.0/22', // GitHub 75 | ); 76 | 77 | /** 78 | * Git branch to pull 79 | * 80 | * @var string 81 | */ 82 | public $branch = 'master'; 83 | 84 | /** 85 | * The username to run the deployment under 86 | * 87 | * @var string 88 | */ 89 | public $deployUser; 90 | 91 | /** 92 | * Directory to pull in 93 | * 94 | * @var string 95 | */ 96 | public $directory; 97 | 98 | /** 99 | * A callback function to call after the deploy has finished. 100 | * 101 | * @var Closure 102 | */ 103 | public $postDeployCallback; 104 | 105 | /** 106 | * The name of the deploy script to run 107 | * 108 | * @var string 109 | */ 110 | public $pullScriptPath; 111 | 112 | /** 113 | * Git remote to pull form 114 | * 115 | * @var string 116 | */ 117 | public $remote = 'origin'; 118 | 119 | /** 120 | * Monolog instance for logging. 121 | * 122 | * @var Logger 123 | */ 124 | private $logger; 125 | 126 | /** 127 | * Create instance 128 | * 129 | * @param array $options Array of options to set or override 130 | * 131 | * @throws Exception 132 | */ 133 | public function __construct($options = array()) 134 | { 135 | $possibleOptions = array( 136 | 'allowedIpRanges', 137 | 'branch', 138 | 'directory', 139 | 'pullScriptPath', 140 | 'deployUser', 141 | ); 142 | 143 | foreach ($options as $option => $value) { 144 | if (in_array($option, $possibleOptions)) { 145 | $this->{$option} = $value; 146 | } 147 | } 148 | 149 | if (empty($this->directory)) { 150 | throw new Exception("A directory must be supplied"); 151 | } 152 | 153 | 154 | if (isset($options['additionalAllowedIpRanges'])) { 155 | $this->allowedIpRanges = array_merge($this->allowedIpRanges, $options['additionalAllowedIpRanges']); 156 | } 157 | 158 | // Use the provided script by default 159 | if (empty($this->pullScriptPath)) { 160 | $this->pullScriptPath = dirname(__DIR__) . '/scripts/git-pull.sh'; 161 | } 162 | } 163 | 164 | /** 165 | * Set a Monolog instance to use for logging 166 | * 167 | * @param Logger $logger 168 | */ 169 | public function setLogger(Logger $logger) 170 | { 171 | $this->logger = $logger; 172 | } 173 | 174 | /** 175 | * Sends a message to Monolog. 176 | * 177 | * @param string $message The message to write 178 | * @param int $level One of the levels defined by Monolog (e.g. INFO, DEBUG, ERROR, etc.) 179 | * @param array $context 180 | */ 181 | private function log($message, $level = Logger::DEBUG, $context = array()) 182 | { 183 | if ($this->logger instanceof Logger) { 184 | $this->logger->log($level, $message, $context); 185 | } 186 | } 187 | 188 | /** 189 | * Write all the HTTP values from $_SERVER to the log file. 190 | * 191 | * @return bool 192 | */ 193 | private function logHeaders() 194 | { 195 | if (empty($_SERVER)) { 196 | return false; 197 | } 198 | $headers = []; 199 | foreach ($_SERVER as $name => $value) { 200 | if (substr($name, 0, 5) == 'HTTP_') { 201 | $headers[$name] = $value; 202 | } 203 | } 204 | $this->log('HTTP Headers', Logger::DEBUG, $headers); 205 | return true; 206 | } 207 | 208 | /** 209 | * Write all the input from $_POST to the log file. 210 | * 211 | * @return bool 212 | */ 213 | private function logPostedData() 214 | { 215 | if (isset($_POST['payload'])) { 216 | $_POST['payload'] = json_decode($_POST['payload']); 217 | } 218 | 219 | $this->log('POST Data', Logger::DEBUG, $_POST); 220 | return true; 221 | } 222 | 223 | /** 224 | * Return the IP the request is from. 225 | * (Might be from a proxy or via CloudFlare 226 | * 227 | * @return string|null 228 | */ 229 | protected function getIp() 230 | { 231 | $ipAddress = null; 232 | 233 | if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { 234 | $ipAddress = $_SERVER['HTTP_CF_CONNECTING_IP']; 235 | } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 236 | $ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR']; 237 | } elseif (!empty($_SERVER['REMOTE_ADDR'])) { 238 | $ipAddress = $_SERVER['REMOTE_ADDR']; 239 | } 240 | 241 | if (!$ipAddress) { 242 | return null; 243 | } 244 | 245 | // If there are multiple proxies, X_FORWARDED_FOR is a comma and space separated list of IPs 246 | $ipAddress = explode(', ', $ipAddress); 247 | 248 | // Use the first IP 249 | return $ipAddress[0]; 250 | } 251 | 252 | /** 253 | * Executes the necessary commands to do the pull. 254 | * 255 | * @throws Exception 256 | */ 257 | public function deploy() 258 | { 259 | $this->log('Attempting deployment...'); 260 | 261 | if (php_sapi_name() === 'cli') { 262 | $this->log("Running from PHP CLI"); 263 | } else { 264 | $ipAddress = $this->getIp(); 265 | $this->log("IP is {$ipAddress}"); 266 | $this->logHeaders(); 267 | $this->logPostedData(); 268 | 269 | if (!$this->isIpPermitted($ipAddress)) { 270 | $this->log($ipAddress . ' is not an authorised Remote IP Address', Logger::WARNING); 271 | 272 | header('HTTP/1.1 403 Forbidden'); 273 | throw new Exception($ipAddress . ' is not an authorised Remote IP Address'); 274 | } 275 | } 276 | 277 | // Run the deploy script 278 | 279 | $script = escapeshellarg($this->pullScriptPath) 280 | . " -b {$this->branch}" 281 | . " -d " . escapeshellarg($this->directory) 282 | . " -r {$this->remote}"; 283 | 284 | $cmd = "{$script} 2>&1"; 285 | 286 | if (!empty($this->deployUser)) { 287 | $cmd = "sudo -u {$this->deployUser} {$cmd}"; 288 | } 289 | 290 | $this->log($cmd, Logger::DEBUG); 291 | 292 | $output = []; 293 | exec($cmd, $output, $return); 294 | 295 | $this->log("Output from script", Logger::DEBUG, $output); 296 | 297 | if ($return !== 0) { 298 | $this->log("Deploy script exited with code $return", Logger::ERROR); 299 | throw new Exception("Deploy script exited with code $return"); 300 | } 301 | 302 | $this->log('Deployment successful.', Logger::NOTICE); 303 | 304 | if (!empty($this->postDeployCallback)) { 305 | $callback = $this->postDeployCallback; 306 | $callback(); 307 | } 308 | } 309 | 310 | 311 | /** 312 | * Check if an IP address is within the given range. 313 | * Source: https://gist.github.com/jonavon/2028872 314 | * 315 | * @param string $ip IPv4 address 316 | * @param string $range IPv4 range in CIDR notation 317 | * 318 | * @return bool 319 | */ 320 | private function isIpInRange($ip, $range) 321 | { 322 | if (strpos($range, '/') == false) { 323 | $range .= '/32'; 324 | } 325 | // $range is in IP/CIDR format eg 127.0.0.1/24 326 | list($range, $netmask) = explode('/', $range, 2); 327 | $rangeDecimal = ip2long($range); 328 | $ipDecimal = ip2long($ip); 329 | $wildcardDecimal = pow(2, (32 - $netmask)) - 1; 330 | $netmaskDecimal = ~$wildcardDecimal; 331 | return (($ipDecimal & $netmaskDecimal) == ($rangeDecimal & $netmaskDecimal)); 332 | } 333 | 334 | /** 335 | * Check if the given IP address is allowed to trigger the pull. 336 | * 337 | * @param string $ip IPv4 address 338 | * 339 | * @return bool 340 | */ 341 | private function isIpPermitted($ip) 342 | { 343 | foreach ($this->allowedIpRanges as $range) { 344 | if ($this->isIpInRange($ip, $range)) { 345 | return true; 346 | } 347 | } 348 | return false; 349 | } 350 | } 351 | --------------------------------------------------------------------------------