* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; require __DIR__ . '/common/config.php'; use Deployer\Type\Csv; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; /** * Configuration */ set('keep_releases', 5); set('repository', ''); // Repository to deploy. set('branch', ''); // Branch to deploy. set('shared_dirs', []); set('shared_files', []); set('copy_dirs', []); set('writable_dirs', []); set('writable_mode', 'acl'); // chmod, chown, chgrp or acl. set('writable_use_sudo', false); // Using sudo in writable commands? set('writable_chmod_mode', '0755'); // For chmod mode set('http_user', false); set('http_group', false); set('clear_paths', []); // Relative path from deploy_path set('clear_use_sudo', false); // Using sudo in clean commands? set('use_relative_symlink', true); set('composer_action', 'install'); set('composer_options', '{{composer_action}} --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader'); set('env_vars', ''); // Variable assignment before cmds (for example, SYMFONY_ENV={{set}}) set('git_cache', function () { //whether to use git cache - faster cloning by borrowing objects from existing clones. $gitVersion = run('{{bin/git}} version'); $regs = []; if (preg_match('/((\d+\.?)+)/', $gitVersion, $regs)) { $version = $regs[1]; } else { $version = "1.0.0"; } return version_compare($version, '2.3', '>='); }); set('release_name', function () { $list = get('releases_list'); // Filter out anything that does not look like a release. $list = array_filter($list, function ($release) { return preg_match('/^[\d\.]+$/', $release); }); $nextReleaseNumber = 1; if (count($list) > 0) { $nextReleaseNumber = (int)max($list) + 1; } return (string)$nextReleaseNumber; }); // name of folder in releases /** * Return list of releases on server. */ set('releases_list', function () { cd('{{deploy_path}}'); // If there is no releases return empty list. if (!run('[ -d releases ] && [ "$(ls -A releases)" ] && echo "true" || echo "false"')->toBool()) { return []; } // Will list only dirs in releases. $list = run('cd releases && ls -t -d */')->toArray(); // Prepare list. $list = array_map(function ($release) { return basename(rtrim($release, '/')); }, $list); $releases = []; // Releases list. if (run('if [ -f .dep/releases ]; then echo "true"; fi')->toBool()) { $keepReleases = get('keep_releases'); if ($keepReleases === -1) { $csv = run('cat .dep/releases'); } else { $csv = run("tail -n " . ($keepReleases + 5) . " .dep/releases"); } $metainfo = Csv::parse($csv); for ($i = count($metainfo) - 1; $i >= 0; --$i) { if (is_array($metainfo[$i]) && count($metainfo[$i]) >= 2) { list($date, $release) = $metainfo[$i]; $index = array_search($release, $list, true); if ($index !== false) { $releases[] = $release; unset($list[$index]); } } } } return $releases; }); /** * Return release path. */ set('release_path', function () { $releaseExists = run("if [ -h {{deploy_path}}/release ]; then echo 'true'; fi")->toBool(); if (!$releaseExists) { throw new \RuntimeException( "Release path does not found.\n" . "Run deploy:release to create new release." ); } $link = run("readlink {{deploy_path}}/release")->toString(); return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link; }); /** * Return current release path. */ set('current_path', function () { $link = run("readlink {{deploy_path}}/current")->toString(); return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link; }); /** * Custom bins. */ set('bin/php', function () { return run('which php')->toString(); }); set('bin/git', function () { return run('which git')->toString(); }); set('bin/composer', function () { if (commandExist('composer')) { $composer = run('which composer')->toString(); } if (empty($composer)) { run("cd {{release_path}} && curl -sS https://getcomposer.org/installer | {{bin/php}}"); $composer = '{{bin/php}} {{release_path}}/composer.phar'; } return $composer; }); set('bin/symlink', function () { if (get('use_relative_symlink')) { // Check if target system supports relative symlink. if (run('if [[ "$(man ln)" =~ "--relative" ]]; then echo "true"; fi')->toBool()) { return 'ln -nfs --relative'; } } return 'ln -nfs'; }); /** * Default arguments and options. */ argument('stage', InputArgument::OPTIONAL, 'Run tasks only on this server or group of servers'); option('tag', null, InputOption::VALUE_OPTIONAL, 'Tag to deploy'); option('revision', null, InputOption::VALUE_OPTIONAL, 'Revision to deploy'); option('branch', null, InputOption::VALUE_OPTIONAL, 'Branch to deploy'); desc('Rollback to previous release'); task('rollback', function () { $releases = get('releases_list'); if (isset($releases[1])) { $releaseDir = "{{deploy_path}}/releases/{$releases[1]}"; // Symlink to old release. run("cd {{deploy_path}} && {{bin/symlink}} $releaseDir current"); // Remove release run("rm -rf {{deploy_path}}/releases/{$releases[0]}"); if (isVerbose()) { writeln("Rollback to `{$releases[1]}` release was successful."); } } else { writeln("No more releases you can revert to."); } }); desc('Lock deploy'); task('deploy:lock', function () { $locked = run("if [ -f {{deploy_path}}/deploy.lock ]; then echo 'true'; fi")->toBool(); if ($locked) { throw new \RuntimeException( "Deploy locked.\n" . "Run deploy:unlock command to unlock." ); } else { run("touch {{deploy_path}}/deploy.lock"); } }); desc('Unlock deploy'); task('deploy:unlock', function () { run("rm {{deploy_path}}/deploy.lock"); }); desc('Preparing server for deploy'); task('deploy:prepare', function () { // Check if shell is POSIX-compliant try { cd(''); // To run command as raw. $result = run('echo $0')->toString(); if ($result == 'stdin: is not a tty') { throw new \RuntimeException( "Looks like ssh inside another ssh.\n" . "Help: http://goo.gl/gsdLt9" ); } } catch (\RuntimeException $e) { $formatter = Deployer::get()->getHelper('formatter'); $errorMessage = [ "Shell on your server is not POSIX-compliant. Please change to sh, bash or similar.", "Usually, you can change your shell to bash by running: chsh -s /bin/bash", ]; write($formatter->formatBlock($errorMessage, 'error', true)); throw $e; } run('if [ ! -d {{deploy_path}} ]; then mkdir -p {{deploy_path}}; fi'); // Check for existing /current directory (not symlink) $result = run('if [ ! -L {{deploy_path}}/current ] && [ -d {{deploy_path}}/current ]; then echo true; fi')->toBool(); if ($result) { throw new \RuntimeException('There already is a directory (not symlink) named "current" in ' . get('deploy_path') . '. Remove this directory so it can be replaced with a symlink for atomic deployments.'); } // Create metadata .dep dir. run("cd {{deploy_path}} && if [ ! -d .dep ]; then mkdir .dep; fi"); // Create releases dir. run("cd {{deploy_path}} && if [ ! -d releases ]; then mkdir releases; fi"); // Create shared dir. run("cd {{deploy_path}} && if [ ! -d shared ]; then mkdir shared; fi"); }); desc('Prepare release'); task('deploy:release', function () { cd('{{deploy_path}}'); // Clean up if there is unfinished release. $previousReleaseExist = run("if [ -h release ]; then echo 'true'; fi")->toBool(); if ($previousReleaseExist) { run('rm -rf "$(readlink release)"'); // Delete release. run('rm release'); // Delete symlink. } $releaseName = get('release_name'); // Fix collisions. $i = 0; while (run("if [ -d {{deploy_path}}/releases/$releaseName ]; then echo 'true'; fi")->toBool()) { $releaseName .= '.' . ++$i; set('release_name', $releaseName); } $releasePath = parse("{{deploy_path}}/releases/{{release_name}}"); // Metainfo. $date = run('date +"%Y%m%d%H%M%S"'); // Save metainfo about release. run("echo '$date,{{release_name}}' >> .dep/releases"); // Make new release. run("mkdir $releasePath"); run("{{bin/symlink}} $releasePath {{deploy_path}}/release"); }); desc('Update code'); task('deploy:update_code', function () { $repository = trim(get('repository')); $branch = get('branch'); $git = get('bin/git'); $gitCache = get('git_cache'); $depth = $gitCache ? '' : '--depth 1'; // If option `branch` is set. if (input()->hasOption('branch')) { $inputBranch = input()->getOption('branch'); if (!empty($inputBranch)) { $branch = $inputBranch; } } // Branch may come from option or from configuration. $at = ''; if (!empty($branch)) { $at = "-b $branch"; } // If option `tag` is set if (input()->hasOption('tag')) { $tag = input()->getOption('tag'); if (!empty($tag)) { $at = "-b $tag"; } } // If option `tag` is not set and option `revision` is set if (empty($tag) && input()->hasOption('revision')) { $revision = input()->getOption('revision'); if (!empty($revision)) { $depth = ''; } } $releases = get('releases_list'); if ($gitCache && isset($releases[1])) { try { run("$git clone $at --recursive -q --reference {{deploy_path}}/releases/{$releases[1]} --dissociate $repository {{release_path}} 2>&1"); } catch (\RuntimeException $exc) { // If {{deploy_path}}/releases/{$releases[1]} has a failed git clone, is empty, shallow etc, git would throw error and give up. So we're forcing it to act without reference in this situation run("$git clone $at --recursive -q $repository {{release_path}} 2>&1"); } } else { // if we're using git cache this would be identical to above code in catch - full clone. If not, it would create shallow clone. run("$git clone $at $depth --recursive -q $repository {{release_path}} 2>&1"); } if (!empty($revision)) { run("cd {{release_path}} && $git checkout $revision"); } }); desc('Copy directories'); task('deploy:copy_dirs', function () { $dirs = get('copy_dirs'); foreach ($dirs as $dir) { // Delete directory if exists. run("if [ -d $(echo {{release_path}}/$dir) ]; then rm -rf {{release_path}}/$dir; fi"); // Copy directory. run("if [ -d $(echo {{deploy_path}}/current/$dir) ]; then cp -rpf {{deploy_path}}/current/$dir {{release_path}}/$dir; fi"); } }); desc('Creating symlinks for shared files and dirs'); task('deploy:shared', function () { $sharedPath = "{{deploy_path}}/shared"; foreach (get('shared_dirs') as $dir) { // Create shared dir if it does not exist. run("mkdir -p $sharedPath/$dir"); // Copy shared dir files if they does not exist. run("if [ -d $(echo {{release_path}}/$dir) ]; then cp -rn {{release_path}}/$dir $sharedPath; fi"); // Remove from source. run("if [ -d $(echo {{release_path}}/$dir) ]; then rm -rf {{release_path}}/$dir; fi"); // Create path to shared dir in release dir if it does not exist. // (symlink will not create the path and will fail otherwise) run("mkdir -p `dirname {{release_path}}/$dir`"); // Symlink shared dir to release dir run("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir"); } foreach (get('shared_files') as $file) { $dirname = dirname($file); // Remove from source. run("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi"); // Ensure dir is available in release run("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi"); // Create dir of shared file run("mkdir -p $sharedPath/" . $dirname); // Touch shared run("touch $sharedPath/$file"); // Symlink shared dir to release dir run("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file"); } }); desc('Make writable dirs'); task('deploy:writable', function () { $dirs = join(' ', get('writable_dirs')); $mode = get('writable_mode'); $sudo = get('writable_use_sudo') ? 'sudo' : ''; $httpUser = get('http_user', false); if (empty($dirs)) { return; } if ($httpUser === false && $mode !== 'chmod') { // Detect http user in process list. $httpUser = run("ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\\ -f1")->toString(); if (empty($httpUser)) { throw new \RuntimeException( "Can't detect http user name.\n" . "Please setup `http_user` config parameter." ); } } try { cd('{{release_path}}'); if ($mode === 'chown') { // Change owner. // -R operate on files and directories recursively // -L traverse every symbolic link to a directory encountered run("$sudo chown -RL $httpUser $dirs"); } elseif ($mode === 'chgrp') { // Change group ownership. // -R operate on files and directories recursively // -L if a command line argument is a symbolic link to a directory, traverse it $httpGroup = get('http_group', false); if ($httpUser === false) { throw new \RuntimeException("Please setup `http_group` config parameter."); } run("$sudo chgrp -RH $httpGroup $dirs"); } elseif ($mode === 'chmod') { run("$sudo chmod -R {{writable_chmod_mode}} $dirs"); } elseif ($mode === 'acl') { if (strpos(run("chmod 2>&1; true"), '+a') !== false) { // Try OS-X specific setting of access-rights run("$sudo chmod +a \"$httpUser allow delete,write,append,file_inherit,directory_inherit\" $dirs"); run("$sudo chmod +a \"`whoami` allow delete,write,append,file_inherit,directory_inherit\" $dirs"); } elseif (commandExist('setfacl')) { if (!empty($sudo)) { run("$sudo setfacl -R -m u:\"$httpUser\":rwX -m u:`whoami`:rwX $dirs"); run("$sudo setfacl -dR -m u:\"$httpUser\":rwX -m u:`whoami`:rwX $dirs"); } else { // When running without sudo, exception may be thrown // if executing setfacl on files created by http user (in directory that has been setfacl before). // These directories/files should be skipped. // Now, we will check each directory for ACL and only setfacl for which has not been set before. $writeableDirs = get('writable_dirs'); foreach ($writeableDirs as $dir) { // Check if ACL has been set or not $hasfacl = run("getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l")->toString(); // Set ACL for directory if it has not been set before if (!$hasfacl) { run("setfacl -R -m u:\"$httpUser\":rwX -m u:`whoami`:rwX $dir"); run("setfacl -dR -m u:\"$httpUser\":rwX -m u:`whoami`:rwX $dir"); } } } } else { throw new \RuntimeException("Cant't set writable dirs with ACL."); } } else { throw new \RuntimeException("Unknown writable_mode `$mode`."); } } catch (\RuntimeException $e) { $formatter = Deployer::get()->getHelper('formatter'); $errorMessage = [ "Unable to setup correct permissions for writable dirs. ", "You need to configure sudo's sudoers files to not prompt for password,", "or setup correct permissions manually. ", ]; write($formatter->formatBlock($errorMessage, 'error', true)); throw $e; } }); desc('Installing vendors'); task('deploy:vendors', function () { run('cd {{release_path}} && {{env_vars}} {{bin/composer}} {{composer_options}}'); }); desc('Creating symlink to release'); task('deploy:symlink', function () { if (run('if [[ "$(man mv)" =~ "--no-target-directory" ]]; then echo "true"; fi')->toBool()) { run("mv -T {{deploy_path}}/release {{deploy_path}}/current"); } else { // Atomic symlink does not supported. // Will use simpleā‰¤ two steps switch. run("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} current"); // Atomic override symlink. run("cd {{deploy_path}} && rm release"); // Remove release link. } }); desc('Show current release'); task('current', function () { writeln('Current release: ' . basename(get('current_path'))); }); desc('Cleaning up old releases'); task('cleanup', function () { $releases = get('releases_list'); $keep = get('keep_releases'); if ($keep === -1) { // Keep unlimited releases. return; } while ($keep - 1 > 0) { array_shift($releases); --$keep; } foreach ($releases as $release) { run("rm -rf {{deploy_path}}/releases/$release"); } run("cd {{deploy_path}} && if [ -e release ]; then rm release; fi"); run("cd {{deploy_path}} && if [ -h release ]; then rm release; fi"); }); desc('Cleaning up files and/or directories'); task('deploy:clean', function () { $paths = get('clear_paths'); $sudo = get('clear_use_sudo') ? 'sudo' : ''; foreach ($paths as $path) { run("$sudo rm -rf {{release_path}}/$path"); } }); /** * Success message */ task('success', function () { Deployer::setDefault('terminate_message', 'Successfully deployed!'); })->once()->setPrivate(); /** * Deploy failure */ task('deploy:failed', function () { })->setPrivate(); onFailure('deploy', 'deploy:failed');