From b997f70ecf231db219e09a4d40fe109fd8e19196 Mon Sep 17 00:00:00 2001 From: Sebastian Fischer Date: Sat, 12 Apr 2025 20:28:16 +0200 Subject: [PATCH] [TASK] Update deployer to 7.x --- Classes/Config/AbstractConfiguration.php | 46 +- Classes/Config/Deployment.php | 1147 ++++++++++++++-------- Classes/Config/Rsync.php | 198 +--- composer.json | 2 +- deploy.php | 1 + 5 files changed, 766 insertions(+), 628 deletions(-) diff --git a/Classes/Config/AbstractConfiguration.php b/Classes/Config/AbstractConfiguration.php index 4d5e537..4ef2776 100644 --- a/Classes/Config/AbstractConfiguration.php +++ b/Classes/Config/AbstractConfiguration.php @@ -3,6 +3,7 @@ namespace Evoweb\DeployerConfig\Config; use Deployer\Deployer; +use Deployer\Task\GroupTask; use Deployer\Task\Task; use phpDocumentor\Reflection\DocBlockFactory; use ReflectionMethod; @@ -17,30 +18,32 @@ abstract class AbstractConfiguration protected function initializeTasks(): void { - foreach ($this->tasks as $name => $config) { - $this->registerTask($name, $config); - } + array_walk($this->tasks, [$this, 'registerTask']); } - protected function registerTask(string $name, array $config): void + protected function registerTask(array $config, string $name): void { - $methodName = $config['methodName'] ?? ''; - - if ($methodName === '' || !method_exists(static::class, $methodName)) { + $body = $config['body'] ?? ''; + if (is_string($body) && !method_exists($this, $body)) { return; } - $task = new Task($name, $this->$methodName(...)); + if (is_callable([$this, $body])) { + $task = new Task($name, [$this, $body]); - $description = $this->getFunctionSummary($methodName); - if ($description) { - $task->desc($description); + $description = $this->getFunctionSummary($body); + if ($description) { + $task->desc($description); + } + } elseif (is_array($body)) { + $task = new GroupTask($name, $body); + } else { + throw new \InvalidArgumentException('Task body should be a function or an array.'); } - $stages = $config['stages'] ?? []; - if (count($stages)) { - $task->onStage(...$stages); - } + array_map(function ($selector) use ($task) { + $task->addSelector($selector); + }, $config['stages'] ?? []); $deployer = Deployer::get(); $deployer->tasks->set($name, $task); @@ -60,18 +63,9 @@ abstract class AbstractConfiguration return $summary; } - protected function getTasks(): array - { - return array_keys($this->tasks); - } - protected function get(string $name, $default = null) { - return get($name, $default) ?? ( - isset(Deployer::get()->config[$name]) - ? Deployer::get()->config->get($name) - : $default - ); + return get($name, $default); } protected function set(string $name, $value): void @@ -81,7 +75,7 @@ abstract class AbstractConfiguration protected function has(string $name): bool { - return has($name) || Deployer::get()->config->has($name); + return has($name); } protected function hasArray(string $name): bool diff --git a/Classes/Config/Deployment.php b/Classes/Config/Deployment.php index 3e8c2dc..d040ac3 100644 --- a/Classes/Config/Deployment.php +++ b/Classes/Config/Deployment.php @@ -2,76 +2,119 @@ namespace Evoweb\DeployerConfig\Config; +use Deployer\Exception\ConfigurationException; use Deployer\Exception\Exception; +use Deployer\Exception\GracefulShutdownException; use Deployer\Host\Host; use Deployer\Task\Context; -use Deployer\Type\Csv; +use Symfony\Component\Console\Output\OutputInterface; use function Deployer\after; use function Deployer\cd; -use function Deployer\commandExist; +use function Deployer\commandSupportsOption; +use function Deployer\currentHost; +use function Deployer\error; use function Deployer\get; -use function Deployer\inventory; +use function Deployer\import; +use function Deployer\info; +use function Deployer\output; use function Deployer\parse; use function Deployer\run; -use function Deployer\task; +use function Deployer\runLocally; +use function Deployer\set; +use function Deployer\Support\escape_shell_argument; +use function Deployer\Support\str_contains; use function Deployer\test; +use function Deployer\testLocally; +use function Deployer\timestamp; +use function Deployer\warning; use function Deployer\writeln; class Deployment extends AbstractConfiguration { protected array $tasks = [ - 'remote:prepare' => ['methodName' => 'remotePrepare'], - 'rsync:warmup' => [], - 'deploy:prepare' => [], - 'deploy:lock' => [], - 'deploy:release' => [], - 'deploy:update_code' => [], - //'rsync:update_code' => [], + 'local:info' => ['body' => 'localInfo'], + 'local:setup' => ['body' => 'localSetup'], + 'remote:prepare' => ['body' => 'remotePrepare'], + 'local:lock' => ['body' => 'localLock'], + 'local:unlock' => ['body' => 'localUnlock'], + 'local:release' => ['body' => 'localRelease'], + 'local:update_code' => ['body' => 'localUpdateCode'], + 'local:env' => ['body' => 'localEnv'], + 'local:shared' => ['body' => 'localShared'], + 'local:writable' => ['body' => 'localWritable'], + 'local:write_release' => ['body' => 'localWriteRelease'], + 'local:create_folder' => ['body' => 'localCreateFolder'], + 'local:vendors' => ['body' => 'localVendors'], + 'local:clear_paths' => ['body' => 'localClearPaths'], + 'local:symlink' => ['body' => 'localSymlink'], + 'local:cleanup' => ['body' => 'localCleanup'], + 'local:echo_release_number' => ['body' => 'echoReleaseNumber'], + 'remote:change_group' => ['body' => 'remoteChangeGroup'], + 'remote:writable' => ['body' => 'remoteWritable'], + 'remote:db_compare' => ['body' => 'remoteDbCompare'], + 'remote:fix_folder_structure' => ['body' => 'remoteFixFolderStructure'], + 'remote:clear_cache' => ['body' => 'remoteClearCache'], + 'remote:clear_opcache' => ['body' => 'remoteClearOpcache'], - 'local:vendors' => ['methodName' => 'localVendors'], - 'local:create_folder' => ['methodName' => 'localCreateFolder'], - 'local:write_release' => ['methodName' => 'localWriteRelease'], - 'local:shared' => ['methodName' => 'localShared'], - 'local:writable_files' => ['methodName' => 'localWritableFiles'], - 'local:executable_files' => ['methodName' => 'localExecutableFiles'], - 'deploy:clear_paths' => [], - 'local:symlink' => ['methodName' => 'localSymlink'], - 'local:cleanup' => ['methodName' => 'localCleanup'], + 'deploy:prepare' => [ + 'body' => [ + 'local:info', + 'local:setup', + 'remote:prepare', + 'rsync:warmup', + 'local:lock', + 'local:release', + 'local:update_code', + 'local:env', + 'local:shared', + 'local:writable', + 'local:write_release', + 'local:create_folder', + 'local:vendors', + ] + ], - 'rsync:remote' => [], - 'remote:change_group' => ['methodName' => 'remoteChangeGroup'], - 'remote:writable' => ['methodName' => 'remoteWritable'], - 'remote:db_compare' => ['methodName' => 'remoteDbCompare'], - 'remote:fix_folder_structure' => ['methodName' => 'remoteFixFolderStructure'], - 'rsync:switch_current' => [], - 'remote:clear_cache' => ['methodName' => 'remoteClearCache'], - 'remote:clear_opcache' => ['methodName' => 'remoteClearOpcache'], - 'rsync:remote_additional_targets' => [], - 'rsync:switch_current_additional_targets' => [], - 'remote:hetzner_shared_hosting' => ['methodName' => 'remoteHetznerSharedHosting'], - - 'local:echo_release_number' => ['methodName' => 'echoReleaseNumber'], - 'deploy:unlock' => [], + 'deploy:publish' => [ + 'body' => [ + 'local:clear_paths', + 'local:symlink', + 'local:cleanup', + 'rsync:remote', + 'remote:change_group', + 'remote:writable', + 'remote:db_compare', + 'remote:fix_folder_structure', + 'rsync:switch_current', + 'remote:clear_cache', + 'remote:clear_opcache', + 'local:echo_release_number', + 'local:unlock', + 'deploy:success' + ] + ], ]; public function __construct(array $tasks = []) { - if (count($tasks)) { + if ($tasks !== []) { $this->tasks = $tasks; } - $this->loadBasicDeployerFiles(); + $this->loadDeployerCommon(); $this->setDefaultConfiguration(); $this->loadHostConfiguration(); $this->initializeTasks(); - $this->initializeMainTask(); + $this->registerAfterTask(); } - protected function loadBasicDeployerFiles(): void + protected function loadDeployerCommon(): void { - $commonFile = realpath(__DIR__ . '/../../../../deployer/deployer/recipe/common.php'); - if (file_exists($commonFile)) { - require $commonFile; + foreach (['/../../', '/../../../', '/../../../../'] as $path) { + $file = realpath(__DIR__ . $path . 'vendor/deployer/deployer/recipe/common.php'); + if (file_exists($file)) { + require $file; + break; + } } new Rsync(); @@ -112,31 +155,136 @@ class Deployment extends AbstractConfiguration 'composer.json', 'composer.lock', ]); + + + $this->set('bin/git', function () { + return $this->which('git'); + }); + + $this->set('bin/composer', function () { + if (testLocally('[ -f {{deploy_path}}/.dep/composer.phar ]')) { + return '{{bin/php}} {{deploy_path}}/.dep/composer.phar'; + } + + if ($this->commandExist('composer')) { + return $this->which('composer'); + } + + warning("Composer binary wasn't found. Installing latest composer to \"{{deploy_path}}/.dep/composer.phar\"."); + runLocally("cd {{deploy_path}} && curl -sS https://getcomposer.org/installer | {{bin/php}}"); + runLocally('mv {{deploy_path}}/composer.phar {{deploy_path}}/.dep/composer.phar'); + return '{{bin/php}} {{deploy_path}}/.dep/composer.phar'; + }); + + $this->set('use_relative_symlink', function () { + return $this->commandSupportsOption('ln', '--relative'); + }); + + $this->set('use_atomic_symlink', function () { + return $this->commandSupportsOption('mv', '--no-target-directory'); + }); + + $this->set('release_name', function () { + $latest = runLocally('cat {{deploy_path}}/.dep/latest_release || echo 0'); + return strval(intval($latest) + 1); + }); + + $this->set('releases_log', function () { + if (!testLocally('[ -f {{deploy_path}}/.dep/releases_log ]')) { + return []; + } + + $releaseLogs = array_map(function ($line) { + return json_decode($line, true); + }, explode("\n", runLocally('tail -n 300 {{deploy_path}}/.dep/releases_log'))); + + return array_filter($releaseLogs); // Return all non-empty lines. + }); + + $this->set('releases_list', function () { + // If there is no releases return empty list. + if (!testLocally('[ -d {{deploy_path}}/releases ] && [ "$(ls -A {{deploy_path}}/releases)" ]')) { + return []; + } + + // Will list only dirs in releases. + $ll = explode("\n", runLocally('cd {{deploy_path}}/releases && ls -t -1 -d */')); + $ll = array_map(function ($release) { + return basename(rtrim(trim($release), '/')); + }, $ll); + + // Return releases from newest to oldest. + $releasesLog = array_reverse(get('releases_log')); + + $releases = []; + foreach ($releasesLog as $release) { + if (in_array($release['release_name'], $ll, true)) { + $releases[] = $release['release_name']; + } + } + return $releases; + }); + + $this->set('release_path', function () { + $releaseExists = testLocally('[ -h {{deploy_path}}/release ]'); + if ($releaseExists) { + $link = runLocally("readlink {{deploy_path}}/release"); + return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link; + } else { + throw new Exception(parse('The "release_path" ({{deploy_path}}/release) does not exist.')); + } + }); + + $this->set('release_or_current_path', function () { + $releaseExists = testLocally('[ -h {{deploy_path}}/release ]'); + return $releaseExists ? get('release_path') : get('current_path'); + }); + + $this->set('http_user', function () { + $candidates = explode("\n", runLocally("ps axo comm,user | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \$NF}' | uniq")); + $httpUser = array_shift($candidates); + + if (empty($httpUser)) { + throw new \RuntimeException( + "Can't detect http user name.\n" . + "Please setup `http_user` config parameter." + ); + } + + return $httpUser; + }); + + $this->set('http_group', function () { + $candidates = explode("\n", runLocally("ps axo comm,group | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \$NF}' | uniq")); + $httpGroup = array_shift($candidates); + + if (empty($httpGroup)) { + throw new \RuntimeException( + "Can't detect http user name.\n" . + "Please setup `http_group` config parameter." + ); + } + + return $httpGroup; + }); } protected function loadHostConfiguration(): void { // read configuration - inventory(__DIR__ . '/../../../../../hosts.yaml'); + foreach (['/../../../../', '/../../../../../'] as $path) { + $file = realpath(__DIR__ . $path . $_ENV['ENVIRONMENT_NAME'] . '.yaml'); + if (file_exists($file)) { + import($file); + break; + } + } } - protected function initializeMainTask(): void + protected function registerAfterTask(): void { - // Main deployment task - task('deploy', $this->getTasks())->desc('Deploy your project'); - - // Display success message on completion - after('deploy', 'success'); - // [Optional] if deploy fails automatically unlock. - after('deploy:failed', 'deploy:unlock'); - } - - protected function getApplicationContext(): string - { - return $this->has('application_context') ? - 'TYPO3_CONTEXT="' . $this->get('application_context') . '" ' : - ''; + after('deploy:failed', 'local:unlock'); } protected function getSudo(): string @@ -150,35 +298,14 @@ class Deployment extends AbstractConfiguration if ($config->get('local') && $config->get('remote_path')) { // backup $contextBackup = Context::pop(); + $config->set('local', false); // create temporary host - $hostname = $contextBackup->getHost()->getHostname(); - print_r($hostname); + $hostname = $contextBackup->getHost()->get('hostname'); $host = new Host($hostname); - $methods = [ - 'hostname', - 'user', - 'port', - 'configFile', - 'identityFile', - 'forwardAgent', - 'multiplexing', - 'sshOptions', - 'sshFlags', - 'shellCommand', - ]; - foreach ($methods as $method) { - if ($config->has($method)) { - $host->$method($config->get($method)); - } - } - foreach ($config->getCollection()->getIterator() as $name => $value) { - $host->set($name, $value); - } - // create new context with host - $context = new Context($host, $contextBackup->getInput(), $contextBackup->getOutput()); + $context = new Context($host); Context::push($context); // execute the task @@ -191,40 +318,417 @@ class Deployment extends AbstractConfiguration } } + protected function commandSupportsOption(string $command, string $option): bool + { + $man = runLocally("(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true"); + if (empty($man)) { + return false; + } + return str_contains($man, $option); + } + + protected function commandExist(string $command): bool + { + return testLocally("hash $command 2>/dev/null"); + } + + protected function which(string $name): string + { + $nameEscaped = escapeshellarg($name); + + // Try `command`, should cover all Bourne-like shells + // Try `which`, should cover most other cases + // Fallback to `type` command, if the rest fails + $path = runLocally("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped"); + if (empty($path)) { + throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available"); + } + + // Deal with issue when `type -p` outputs something like `type -ap` in some implementations + return trim(str_replace("$name is", "", $path)); + } + + protected function getApplicationContext(): string + { + return $this->has('application_context') ? + 'TYPO3_CONTEXT="' . $this->get('application_context') . '" ' : + ''; + } + + protected function getTYPO3Bin(string $path, string $testPath): string + { + if (test('[ -f ' . $testPath . '/releases/{{release_name}}/vendor/bin/typo3cms ]')) { + $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3cms'; + } else { + $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3'; + } + return $bin; + } + + + /** + * Displays info about deployment + */ + public function localInfo(): void + { + info("deploying {{what}} to {{where}}"); + } + + /** + * Prepares host for deploy + */ + public function localSetup(): void + { + runLocally( + <<hasArray('prepare_dirs')) { - $callback = function () { - $prepareDirs = $this->get('prepare_dirs'); - foreach ($prepareDirs as $dir) { - if (test('[ ! -e "{{remote_path}}/' . $dir . '" ]')) { - run($this->getSudo() . ' mkdir -p {{remote_path}}/' . $dir); - } - } - }; - $this->processRemoteTask($callback); + if (!$this->hasArray('prepare_dirs')) { + return; + } + + $prepareDirs = $this->get('prepare_dirs'); + foreach ($prepareDirs as $dir) { + if (test('[ ! -e "{{remote_path}}/' . $dir . '" ]')) { + run($this->getSudo() . ' mkdir -p {{remote_path}}/' . $dir); + } } } /** - * Installing vendors + * Locks deploy */ - public function localVendors(): void + public function localLock(): void { - if (!commandExist('unzip')) { - writeln( - 'To speed up composer installation setup "unzip" command' . - ' with PHP zip extension https://goo.gl/sxzFcD' + $user = escapeshellarg(get('user')); + $locked = runLocally("[ -f {{deploy_path}}/.dep/deploy.lock ] && echo +locked || echo $user > {{deploy_path}}/.dep/deploy.lock"); + if ($locked === '+locked') { + $lockedUser = runLocally("cat {{deploy_path}}/.dep/deploy.lock"); + throw new GracefulShutdownException( + "Deploy locked by $lockedUser.\n" . + "Execute \"deploy:unlock\" task to unlock." ); } + } - // if composer.json exists - if (test('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) { - run('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};'); + /** + * Unlocks deploy + */ + public function localUnlock(): void + { + // always success + runLocally("rm -f {{deploy_path}}/.dep/deploy.lock"); + } + + /** + * Prepares release + */ + public function localRelease(): void + { + // Clean up if there is unfinished release. + if (testLocally('[ -h {{deploy_path}}/release ]')) { + runLocally('rm {{deploy_path}}/release'); // Delete symlink. } + + // We need to get releases_list at same point as release_name, + // as standard release_name's implementation depends on it and, + // if user overrides it, we need to get releases_list manually. + $releasesList = get('releases_list'); + $releaseName = get('release_name'); + $releasePath = "{{deploy_path}}/releases/$releaseName"; + + // Check what there is no such release path. + if (testLocally("[ -d $releasePath ]")) { + $freeReleaseName = '...'; + // Check what $releaseName is integer. + if (ctype_digit($releaseName)) { + $freeReleaseName = intval($releaseName); + // Find free release name. + while (testLocally("[ -d {{deploy_path}}/releases/$freeReleaseName ]")) { + $freeReleaseName++; + } + } + throw new Exception("Release name \"$releaseName\" already exists.\nRelease name can be overridden via:\n dep deploy -o release_name=$freeReleaseName"); + } + + // Save release_name. + if (is_numeric($releaseName) && is_integer(intval($releaseName))) { + runLocally("echo $releaseName > {{deploy_path}}/.dep/latest_release"); + } + + // Metainfo. + $timestamp = timestamp(); + $metainfo = [ + 'created_at' => $timestamp, + 'release_name' => $releaseName, + 'user' => get('user'), + 'target' => get('target'), + ]; + + // Save metainfo about release. + $json = escape_shell_argument(json_encode($metainfo)); + runLocally("echo $json >> {{deploy_path}}/.dep/releases_log"); + + // Make new release. + runLocally("mkdir -p $releasePath"); + runLocally("{{bin/symlink}} $releasePath {{deploy_path}}/release"); + + // Add to releases list. + array_unshift($releasesList, $releaseName); + set('releases_list', $releasesList); + + // Set previous_release. + if (isset($releasesList[1])) { + set('previous_release', "{{deploy_path}}/releases/{$releasesList[1]}"); + } + } + + /** + * Updates code + */ + public function localUpdateCode(): void + { + $git = get('bin/git'); + $repository = get('repository'); + $target = get('target'); + + if (empty($repository)) { + throw new ConfigurationException("Missing 'repository' configuration."); + } + + $targetWithDir = $target; + if (!empty(get('sub_directory'))) { + $targetWithDir .= ':{{sub_directory}}'; + } + + $bare = parse('{{deploy_path}}/.dep/repo'); + $env = [ + 'GIT_TERMINAL_PROMPT' => '0', + 'GIT_SSH_COMMAND' => get('git_ssh_command'), + ]; + + start: + // Clone the repository to a bare repo. + runLocally("[ -d $bare ] || mkdir -p $bare"); + runLocally("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", ['env' => $env]); + + // If remote url changed, drop `.dep/repo` and reinstall. + if (runLocally("cd $bare; $git config --get remote.origin.url") !== $repository) { + runLocally("rm -rf {{deploy_path}}$bare"); + goto start; + } + + runLocally("$git remote update 2>&1", ['env' => $env]); + + // Copy to release_path. + if (get('update_code_strategy') === 'archive') { + runLocally("cd $bare; $git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1"); + } elseif (get('update_code_strategy') === 'clone') { + runLocally("cd {{release_path}}; $git clone -l $bare ."); + runLocally("cd {{release_path}}; $git remote set-url origin $repository", ['env' => $env]); + runLocally("cd {{release_path}}; $git checkout --force $target"); + } else { + throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); + } + + // Save git revision in REVISION file. + $rev = escapeshellarg(runLocally("cd $bare; $git rev-list $target -1")); + runLocally("echo $rev > {{release_path}}/REVISION"); + } + + /** + * Configure .env file + */ + public function localEnv(): void + { + if (testLocally('[ ! -e {{release_or_current_path}}/.env ] && [ -f {{release_or_current_path}}/{{dotenv_example}} ]')) { + runLocally('cp {{release_or_current_path}}/{{dotenv_example}} {{release_or_current_path}}/.env'); + } + } + + /** + * Creates symlinks for shared files and dirs + */ + public function localShared(): void + { + $sharedPath = "{{deploy_path}}/shared"; + + // Validate shared_dir, find duplicates + foreach (get('shared_dirs') as $a) { + foreach (get('shared_dirs') as $b) { + if ($a !== $b && strpos(rtrim($a, '/') . '/', rtrim($b, '/') . '/') === 0) { + throw new Exception("Can not share same dirs `$a` and `$b`."); + } + } + } + + $copyVerbosity = output()->getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : ''; + + foreach (get('shared_dirs') as $dir) { + // Make sure all path without tailing slash. + $dir = trim($dir, '/'); + + // Check if shared dir does not exist. + if (!testLocally("[ -d $sharedPath/$dir ]")) { + // Create shared dir if it does not exist. + runLocally("mkdir -p $sharedPath/$dir"); + // If release contains shared dir, copy that dir from release to shared. + if (testLocally("[ -d $(echo {{release_path}}/$dir) ]")) { + runLocally("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir)); + } + } + + // Remove from source. + runLocally("rm -rf {{release_path}}/$dir"); + + // Create path to shared dir in release dir if it does not exist. + // Symlink will not create the path and will fail otherwise. + runLocally("mkdir -p `dirname {{release_path}}/$dir`"); + + // Symlink shared dir to release dir + runLocally("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir"); + } + + foreach (get('shared_files') as $file) { + $dirname = dirname(parse($file)); + + // Create dir of shared file if not existing + if (!testLocally("[ -d $sharedPath/$dirname ]")) { + runLocally("mkdir -p $sharedPath/$dirname"); + } + + // Check if shared file does not exist in shared. + // and file exist in release + if (!testLocally("[ -f $sharedPath/$file ]") && testLocally("[ -f {{release_path}}/$file ]")) { + // Copy file in shared dir if not present + runLocally("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file"); + } + + // Remove from source. + runLocally("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi"); + + // Ensure dir is available in release + runLocally("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi"); + + // Touch shared + runLocally("[ -f $sharedPath/$file ] || touch $sharedPath/$file"); + + // Symlink shared dir to release dir + runLocally("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file"); + } + } + + /** + * Makes writable dirs + */ + public function localWritable(): void + { + $dirs = join(' ', get('writable_dirs')); + $mode = get('writable_mode'); + $recursive = get('writable_recursive') ? '-R' : ''; + $sudo = get('writable_use_sudo') ? 'sudo' : ''; + + if (empty($dirs)) { + return; + } + // Check that we don't have absolute path + if (strpos($dirs, ' /') !== false) { + throw new \RuntimeException('Absolute path not allowed in config parameter `writable_dirs`.'); + } + + // Create directories if they don't exist + runLocally("cd {{release_or_current_path}}; mkdir -p $dirs"); + + if ($mode === 'chown') { + $httpUser = get('http_user'); + // Change owner. + // -L traverse every symbolic link to a directory encountered + runLocally("cd {{release_or_current_path}}; $sudo chown -L $recursive $httpUser $dirs"); + } elseif ($mode === 'chgrp') { + // Change group ownership. + // -L traverse every symbolic link to a directory encountered + runLocally("cd {{release_or_current_path}}; $sudo chgrp -L $recursive {{http_group}} $dirs"); + runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive g+rwx $dirs"); + } elseif ($mode === 'chmod') { + runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive {{writable_chmod_mode}} $dirs"); + } elseif ($mode === 'acl') { + $remoteUser = get('remote_user', false); + if (empty($remoteUser)) { + $remoteUser = runLocally('whoami'); + } + $httpUser = get('http_user'); + if (strlen(runLocally("chmod --help | grep ugoa; true")) > 0) { + // Try OS-X specific setting of access-rights + + runLocally("cd {{release_or_current_path}}; $sudo chmod g+w $dirs"); + } elseif ($this->commandExist('setfacl')) { + $setFaclUsers = "-m u:\"$httpUser\":rwX"; + // Check if remote user exists, before adding it to setfacl + $remoteUserExists = testLocally("id -u $remoteUser &>/dev/null 2>&1 || exit 0"); + if ($remoteUserExists === true) { + $setFaclUsers .= " -m u:$remoteUser:rwX"; + } + if (empty($sudo)) { + // 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 = runLocally("cd {{release_or_current_path}}; getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l"); + // Set ACL for directory if it has not been set before + if (!$hasfacl) { + runLocally("cd {{release_or_current_path}}; setfacl -L $recursive $setFaclUsers $dir"); + runLocally("cd {{release_or_current_path}}; setfacl -dL $recursive $setFaclUsers $dir"); + } + } + } else { + runLocally("cd {{release_or_current_path}}; $sudo setfacl -L $recursive $setFaclUsers $dirs"); + runLocally("cd {{release_or_current_path}}; $sudo setfacl -dL $recursive $setFaclUsers $dirs"); + } + } else { + $alias = currentHost()->getAlias(); + throw new \RuntimeException("Can't set writable dirs with ACL.\nInstall ACL with next command:\ndep run 'sudo apt-get install acl' -- $alias"); + } + } elseif ($mode === 'sticky') { + // Changes the group of the files, sets sticky bit to the directories + // and add the writable bit for all files + runLocally("cd {{release_or_current_path}}; for dir in $dirs;" . + 'do ' . + 'chgrp -L -R {{http_group}} ${dir}; ' . + 'find ${dir} -type d -exec chmod g+rwxs \{\} \;;' . + 'find ${dir} -type f -exec chmod g+rw \{\} \;;' . + 'done'); + } elseif ($mode === 'skip') { + // Does nothing, saves time if no changes are required for some environments + return; + } else { + throw new \RuntimeException("Unknown writable_mode `$mode`."); + } + } + + public function localWriteRelease(): void + { + runLocally('echo ' . getEnv('CI_COMMIT_REF_NAME') . ' > {{release_path}}/release'); } /** @@ -234,148 +738,52 @@ class Deployment extends AbstractConfiguration { $sudo = $this->getSudo(); - if (test('[ ! -d {{release_path}}/var/cache ]')) { - run('mkdir -p {{release_path}}/var/cache'); + if (testLocally('[ ! -d {{release_path}}/var/cache ]')) { + runLocally('mkdir -p {{release_path}}/var/cache'); } - run($sudo . ' chmod -R 775 {{release_path}}/var/cache'); + runLocally($sudo . ' chmod -R 775 {{release_path}}/var/cache'); - if (test('[ ! -d {{release_path}}/var/log ]')) { - run('mkdir -p {{release_path}}/var/log'); + if (testLocally('[ ! -d {{release_path}}/var/log ]')) { + runLocally('mkdir -p {{release_path}}/var/log'); } - run($sudo . ' chmod -R 775 {{release_path}}/var/log'); - } - - public function localWriteRelease(): void - { - run('echo ' . getEnv('CI_COMMIT_REF_NAME') . ' > {{release_path}}/release'); + runLocally($sudo . ' chmod -R 775 {{release_path}}/var/log'); } /** - * Creating symlinks for shared files and dirs in cms private folder + * Installing vendors */ - public function localShared(): void + public function localVendors(): void { - if ($this->hasArray('shared_dirs_private')) { - $this->sharedFolders($this->get('shared_dirs_private'), 'private'); + if (!$this->commandExist('unzip')) { + info( + 'To speed up composer installation setup "unzip" command' . + ' with PHP zip extension https://goo.gl/sxzFcD' + ); } - if ($this->hasArray('shared_dirs_public')) { - $this->sharedFolders($this->get('shared_dirs_public'), 'public'); - } - - if ($this->hasArray('shared_files_private')) { - $this->sharedFiles($this->get('shared_files_private'), 'private'); - } - - if ($this->hasArray('shared_files_public')) { - $this->sharedFiles($this->get('shared_files_public'), 'public'); + // if composer.json exists + if (testLocally('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) { + runLocally('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};'); } } - protected function sharedFolders(array $sharedDirs, string $folder): void - { - // Validate shared_dir, find duplicates - foreach ($sharedDirs as $a) { - foreach ($sharedDirs as $b) { - if ( - $a !== $b - && str_starts_with(rtrim($a, '/') . 'Deployment.php/', rtrim($b, '/') . '/') - ) { - throw new Exception("Can not share same dirs `$a` and `$b`."); - } - } - } - - foreach ($sharedDirs as $dir) { - // Check if shared dir does not exist. - if (test('[ ! -d {{deploy_path}}/shared/' . $dir . ' ]')) { - // Create shared dir if it does not exist. - run('mkdir -p {{deploy_path}}/shared/' . $dir); - - // If release contains shared dir, copy that dir from release to shared. - if (test('[ -d $(echo {{release_path}}/' . $folder . '/' . $dir . ') ]')) { - run('cp -rv {{release_path}}/' . $folder . '/$dir {{deploy_path}}/shared/' . dirname($dir)); - } - } - - // Remove from source. - run('rm -rf {{release_path}}/' . $folder . '/' . $dir); - - // 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}}/' . $folder . '/' . $dir . '`'); - - // Symlink shared dir to release dir - run('cd {{deploy_path}} && {{bin/symlink}} ../../../shared/' - . $dir . ' {{release_path}}/' . $folder . '/' . $dir); - } - } - - protected function sharedFiles(array $sharedFiles, string $folder): void - { - foreach ($sharedFiles as $file) { - $dirname = dirname($file); - - // Create dir of shared file - run('mkdir -p {{deploy_path}}/shared/' . $dirname); - - // Check if shared file does not exist in shared and file exist in release - if ( - test('[ ! -f {{deploy_path}}/shared/' . $file - . ' ] && [ -f {{release_path}}/' . $folder . '/' . $file . ' ]') - ) { - // Copy file in shared dir - run('cp -rv {{release_path}}/' . $folder . '/' . $file . ' {{deploy_path}}/shared/' . $file); - } - - // Remove from source. - if (test('[ -f $(echo {{release_path}}/' . $folder . '/' . $file . ') ]')) { - run('rm -rf {{release_path}}/' . $folder . '/' . $file); - } - - // Ensure dir is available in release - if (test('[ ! -d $(echo {{release_path}}/' . $folder . '/' . $dirname . ') ]')) { - run('mkdir -p {{release_path}}/' . $folder . '/' . $dirname); - } - - // Touch shared - run('touch {{deploy_path}}/shared/' . $file); - - // Symlink shared dir to release dir - run('cd {{deploy_path}} && {{bin/symlink}} ../../../shared/' - . $file . ' {{release_path}}/' . $folder . '/' . $file); - } - } /** - * Set writable bit on files in "writable_files" + * Cleanup files and/or directories */ - public function localWritableFiles(): void + public function localClearPaths(): void { - if ($this->hasArray('writable_files')) { - $paths = $this->get('writable_files'); - foreach ($paths as $path) { - if (test('[ -e "{{release_path}}/' . $path . '" ]')) { - run($this->getSudo() . ' chmod g+w {{release_path}}/' . $path); - } - } - } - } + $paths = get('clear_paths'); + $sudo = get('clear_use_sudo') ? 'sudo' : ''; + $batch = 100; - /** - * Set executable bit on files in "executable_files" - */ - public function localExecutableFiles(): void - { - if ($this->hasArray('executable_files')) { - $sudo = $this->getSudo(); - $paths = $this->get('executable_files'); - foreach ($paths as $path) { - if (test('[ -e "{{release_path}}/' . $path . '" ]')) { - run($sudo . ' chmod g+x {{release_path}}/' . $path); - run($sudo . ' chmod u+x {{release_path}}/' . $path); - } - } + $commands = []; + foreach ($paths as $path) { + $commands[] = "$sudo rm -rf {{release_path}}/$path"; + } + $chunks = array_chunk($commands, $batch); + foreach ($chunks as $chunk) { + runLocally(implode('; ', $chunk)); } } @@ -384,17 +792,33 @@ class Deployment extends AbstractConfiguration */ public function localSymlink(): void { - $sudo = $this->getSudo(); + if (get('use_atomic_symlink')) { + runLocally("mv -T {{deploy_path}}/release {{current_path}}"); + } else { + // Atomic symlink does not supported. + // Will use simple two steps switch. - if (test('[ -L "{{deploy_path}}/current" ] || [ -D "{{deploy_path}}/current" ]')) { - // Remove previous current - run($sudo . ' rm {{deploy_path}}/current'); + runLocally("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}"); // Atomic override symlink. + runLocally("cd {{deploy_path}} && rm release"); // Remove release link. } + } - // Atomic override symlink. - run($sudo . ' cd {{deploy_path}} && {{bin/symlink}} releases/{{release_name}} current'); - // Remove release link. - run($sudo . ' cd {{deploy_path}} && rm release'); + /** + * Cleaning up old releases + */ + public function localCleanup(): void + { + $releases = get('releases_list'); + $keep = get('keep_releases'); + $sudo = get('cleanup_use_sudo') ? 'sudo' : ''; + + runLocally("cd {{deploy_path}} && if [ -e release ]; then rm release; fi"); + + if ($keep > 0) { + foreach (array_slice($releases, $keep) as $release) { + runLocally("$sudo rm -rf {{deploy_path}}/releases/$release"); + } + } } /** @@ -403,18 +827,15 @@ class Deployment extends AbstractConfiguration public function remoteChangeGroup(): void { if ($this->get('http_group')) { - $callback = function () { - $sudo = $this->getSudo(); - $runOpts = []; - if ($sudo) { - $runOpts['tty'] = $this->get('writable_tty', false); - } + $sudo = get('writable_use_sudo') ? 'sudo' : ''; + $runOpts = []; + if ($sudo) { + $runOpts['tty'] = $this->get('writable_tty', false); + } - $path = parse('{{remote_path}}/releases/{{release_name}}/'); - run($sudo . ' chgrp -H -R ' . $this->get('http_group') . ' ' . $path, $runOpts); - run($sudo . ' chmod -R 2755 ' . $path, $runOpts); - }; - $this->processRemoteTask($callback); + $path = parse('{{remote_path}}/releases/{{release_name}}/'); + run($sudo . ' chgrp -H -R ' . $this->get('http_group') . ' ' . $path, $runOpts); + run($sudo . ' chmod -R 2755 ' . $path, $runOpts); } } @@ -424,43 +845,40 @@ class Deployment extends AbstractConfiguration public function remoteWritable(): void { if ($this->hasArray('writable_dirs') || $this->hasArray('writable_files')) { - $callback = function () { - $sudo = $this->getSudo(); - $runOpts = []; - if ($sudo) { - $runOpts['tty'] = get('writable_tty', false); - } - $recursive = $this->get('writable_recursive') ? '-R' : ''; - $httpGroup = $this->get('http_group', false); - $path = parse('{{remote_path}}/releases/{{release_name}}/'); + $sudo = get('writable_use_sudo') ? 'sudo' : ''; + $runOpts = []; + if ($sudo) { + $runOpts['tty'] = get('writable_tty', false); + } + $recursive = $this->get('writable_recursive') ? '-R' : ''; + $httpGroup = $this->get('http_group', false); + $path = parse('{{remote_path}}/releases/{{release_name}}/'); - if ($this->hasArray('writable_dirs')) { - foreach ($this->get('writable_dirs') as $dir) { - if (test('[ ! -d "' . $path . $dir . '" ]')) { - run('mkdir -p ' . $path . $dir); - } - run($sudo . ' chmod -R 2775 ' . $path . $dir); + if ($this->hasArray('writable_dirs')) { + foreach ($this->get('writable_dirs') as $dir) { + if (test('[ ! -d "' . $path . $dir . '" ]')) { + run('mkdir -p ' . $path . $dir); + } + run($sudo . ' chmod -R 2775 ' . $path . $dir); + if ($httpGroup) { + run( + $sudo . ' chgrp -H ' . $recursive . ' ' . $httpGroup . ' ' . $path . $dir, + $runOpts + ); + } + } + } + + if ($this->hasArray('writable_files')) { + foreach ($this->get('writable_files') as $file) { + if (test('[ -e "' . $path . $file . '" ]')) { + run($sudo . ' chmod g+w ' . $path . $file); if ($httpGroup) { - run( - $sudo . ' chgrp -H ' . $recursive . ' ' . $httpGroup . ' ' . $path . $dir, - $runOpts - ); + run($sudo . ' chgrp ' . $httpGroup . ' ' . $path . $file, $runOpts); } } } - - if ($this->hasArray('writable_files')) { - foreach ($this->get('writable_files') as $file) { - if (test('[ -e "' . $path . $file . '" ]')) { - run($sudo . ' chmod g+w ' . $path . $file); - if ($httpGroup) { - run($sudo . ' chgrp ' . $httpGroup . ' ' . $path . $file, $runOpts); - } - } - } - } - }; - $this->processRemoteTask($callback); + } } } @@ -469,20 +887,17 @@ class Deployment extends AbstractConfiguration */ public function remoteDbCompare(): void { - $callback = function () { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } - $result = run( - $php . $this->getTypo3Bin($path, parse('{{remote_path}}')) . ' database:updateschema "*.add,*.change"' - ); - writeln($result); - }; - $this->processRemoteTask($callback); + $result = run( + $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' database:updateschema "*.add,*.change"' + ); + writeln($result); } /** @@ -490,20 +905,17 @@ class Deployment extends AbstractConfiguration */ public function remoteFixFolderStructure(): void { - $callback = function () { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } - $result = run( - $php . $this->getTypo3Bin($path, parse('{{remote_path}}')) . ' install:fixfolderstructure' - ); - writeln($result); - }; - $this->processRemoteTask($callback); + $result = run( + $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' install:fixfolderstructure' + ); + writeln($result); } /** @@ -511,18 +923,15 @@ class Deployment extends AbstractConfiguration */ public function remoteClearCache(): void { - $callback = function () { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } - $result = run($php . $this->getTypo3Bin($path, parse('{{remote_path}}')) . ' cache:flush'); - writeln($result); - }; - $this->processRemoteTask($callback); + $result = run($php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' cache:flush'); + writeln($result); } /** @@ -535,126 +944,8 @@ class Deployment extends AbstractConfiguration run('curl ' . $htaccess . ' -sk ' . $webDomain . '/cache.php'); } - public function remoteHetznerSharedHosting(): void - { - if ( - ($this->has('hetzner_shared_hosting') && $this->get('hetzner_shared_hosting')) - && ($this->has('hetzner_cache_command') && $this->get('hetzner_cache_command')) - ) { - $callback = function () { - $result = run($this->get('hetzner_cache_command')); - writeln($result); - }; - $this->processRemoteTask($callback); - } - } - - /** - * Cleaning up old releases - */ - public function localCleanup(): void - { - [$list, $releases] = $this->releasesList(); - $sudo = $this->get('cleanup_use_sudo') ? 'sudo' : ''; - $runOpts = []; - - if ($sudo) { - $runOpts['tty'] = $this->get('cleanup_tty', false); - } - - foreach ($list as $release) { - run($sudo . ' rm -rf {{deploy_path}}/releases/' . $release, $runOpts); - } - - run('cd {{deploy_path}} && if [ -e release ]; then ' . $sudo . ' rm release; fi', $runOpts); - run('cd {{deploy_path}} && if [ -h release ]; then ' . $sudo . ' rm release; fi', $runOpts); - - $this->updateReleaseFile($releases); - } - public function echoReleaseNumber(): void { - writeln('This release folder {{release_path}}'); - } - - /** - * Override deployer releases_list - */ - protected function releasesList(): array - { - cd('{{deploy_path}}'); - $keep = (int)$this->get('keep_releases'); - - // If there is no releases return empty list. - if (!test('[ -d releases ] && [ "$(ls -A releases)" ]')) { - return []; - } - - // Will list only dirs in releases. - $list = explode("\n", run('cd releases && ls -t -1 --directory */')); - - // Prepare list. - $list = array_map(function ($release) { - return basename(rtrim(trim($release), '/')); - }, $list); - - // Releases list. - $releases = []; - // Other will be ignored. - $metaInfo = $this->getReleasesInformation(); - $i = 0; - foreach ($metaInfo as $release) { - if ($i++ < $keep && is_array($release) && count($release) > 1) { - $index = array_search($release[1], $list, true); - if ($index !== false) { - $releases[] = $release; - unset($list[$index]); - } - } - } - - return [$list, array_reverse($releases)]; - } - - /** - * Collect releases based on .dep/releases info. - * - * @return array - */ - protected function getReleasesInformation(): array - { - if (test('[ -f .dep/releases ]')) { - $csv = run('cat .dep/releases'); - $releaseInformation = Csv::parse($csv); - - usort($releaseInformation, function ($a, $b) { - return strcmp($b[0], $a[0]); - }); - } - return $releaseInformation ?? []; - } - - protected function getTypo3Bin(string $path, string $testPath): string - { - if (test('[ -f ' . $testPath . '/releases/{{release_name}}/vendor/bin/typo3cms ]')) { - $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3cms'; - } else { - $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3'; - } - return $bin; - } - - protected function updateReleaseFile(array $releases): void - { - cd('{{deploy_path}}'); - $first = true; - foreach ($releases as $release) { - if ($first) { - $first = false; - run('echo "' . implode(',', $release) . '" > .dep/releases'); - } else { - run('echo "' . implode(',', $release) . '" >> .dep/releases'); - } - } + info('Folder of this release is: {{release_path}}'); } } diff --git a/Classes/Config/Rsync.php b/Classes/Config/Rsync.php index a345e92..2bff62b 100644 --- a/Classes/Config/Rsync.php +++ b/Classes/Config/Rsync.php @@ -2,26 +2,30 @@ namespace Evoweb\DeployerConfig\Config; -use Deployer\Host\Localhost; -use Deployer\Task\Context; -use RuntimeException; - -use function Deployer\run; use function Deployer\runLocally; use function Deployer\test; -use function Deployer\writeln; +use function Deployer\testLocally; +use function Deployer\warning; class Rsync extends AbstractConfiguration { protected array $defaultConfig = [ 'exclude' => [ + '.dep/repo', '.git', + '.env', 'deploy.php', 'deploy.lock', 'current', 'shared/', 'var/*', ], + 'exclude_upload' => [ + '.dep/repo/', + 'deploy.lock', + 'current', + 'shared/', + ], 'exclude-file' => false, 'include' => [ 'var/', @@ -36,12 +40,9 @@ class Rsync extends AbstractConfiguration ]; protected array $tasks = [ - 'rsync:warmup' => ['methodName' => 'warmup'], - 'rsync:update_code' => ['methodName' => 'updateCode'], - 'rsync:remote' => ['methodName' => 'remote'], - 'rsync:remote_additional_targets' => ['methodName' => 'remoteAdditionalTargets'], - 'rsync:switch_current' => ['methodName' => 'switchCurrent'], - 'rsync:switch_current_additional_targets' => ['methodName' => 'switchCurrentAdditionalTargets'], + 'rsync:warmup' => ['body' => 'warmup'], + 'rsync:remote' => ['body' => 'remote'], + 'rsync:switch_current' => ['body' => 'switchCurrent'], ]; public function __construct() @@ -79,18 +80,19 @@ class Rsync extends AbstractConfiguration $this->set('rsync_timeout', $this->rsyncTimeout(...)); } + /** * Warmup remote Rsync target */ public function warmup(): void { - if (!test('[ -d "{{deploy_path}}" ]')) { + if (!testLocally('[ -d "{{deploy_path}}" ]')) { runLocally('mkdir -p {{deploy_path}}'); } - if (test('[ -d "{{deploy_path}}" ]')) { - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - run( + if (testLocally('[ -d "{{deploy_path}}" ]')) { + $identityFile = $this->get('identity_file') ? ' -i ' . $this->get('identity_file') : ''; + runLocally( 'rsync \ -e \'ssh -p {{port}}' . $identityFile . '\' \ {{rsync_flags}} \ @@ -103,36 +105,7 @@ class Rsync extends AbstractConfiguration {{rsync_dest}} {{deploy_path}}' ); } else { - writeln('No destination folder found.'); - } - } - - /** - * Copy repository content to release folder - */ - public function updateCode(): void - { - $pathConfigName = 'ignore_update_code_paths'; - if ($this->hasArray($pathConfigName)) { - $excludes = ''; - $ignoreUpdateCodePaths = $this->get($pathConfigName); - foreach ($ignoreUpdateCodePaths as $path) { - $excludes .= ' --exclude=\'' . $path . '\''; - } - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - run( - 'rsync \ - -e \'ssh -p {{port}}' . $identityFile . '\' \ - {{rsync_flags}} \ - {{rsync_options}} \ - {{rsync_timeout}} \ - --exclude=' . escapeshellarg('shared') . ' \ - ' . $excludes . ' \ - {{rsync_includes}} \ - {{rsync_filter}} \ - {{CI_PROJECT_DIR}}/ \ - {{release_path}}' - ); + warning('No destination folder found.'); } } @@ -142,27 +115,10 @@ class Rsync extends AbstractConfiguration public function remote(): void { $config = $this->get('rsync_config'); - $server = Context::get()->getHost(); - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - if ($server instanceof Localhost) { + $identityFile = $this->get('identity_file') ? ' -i ' . $this->get('identity_file') : ''; + if (test('[ -d "{{remote_path}}" ]')) { runLocally( 'rsync \ - -e \'ssh -p {{port}}' . $identityFile . '\' \ - {{rsync_flags}} \ - {{rsync_options}} \ - {{rsync_timeout}} \ - {{rsync_includes}} \ - --exclude=' . escapeshellarg('shared/') . ' \ - {{rsync_excludes_upload}} \ - {{rsync_filter}} \ - \'{{rsync_src}}/\' {{rsync_dest}}', - $config - ); - return; - } - - runLocally( - 'rsync \ -e \'ssh -p {{port}}' . $identityFile . '\' \ {{rsync_flags}} \ {{rsync_options}} \ @@ -172,40 +128,10 @@ class Rsync extends AbstractConfiguration {{rsync_excludes_upload}} \ {{rsync_filter}} \ \'{{rsync_src}}/\' {{user}}@{{hostname}}:\'{{remote_path}}/\'', - $config - ); - } - - /** - * Rsync local->additional targets - */ - public function remoteAdditionalTargets(): void - { - $configName = 'additional_remote_targets'; - if ($this->hasArray($configName)) { - $config = $this->get('rsync_config'); - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - $targets = $this->get($configName); - foreach ($targets as $target) { - $port = $target['port'] ?? '{{port}}'; - $user = $target['user'] ?? '{{user}}'; - $remotePath = $target['remote_path'] ?: '{{remote_path}}'; - runLocally( - 'rsync \ - -e \'ssh -p ' . $port . $identityFile . '\' \ - {{rsync_flags}} \ - {{rsync_options}} \ - {{rsync_timeout}} \ - {{rsync_includes}} \ - --exclude=' . escapeshellarg('shared/') . ' \ - {{rsync_excludes_upload}} \ - {{rsync_filter}} \ - \'{{rsync_src}}/\' ' . $user . '@' . $target['hostname'] . ':\'' . $remotePath . '/\'', - $config - ); - } + $config + ); } else { - writeln('No additional remote target configured'); + warning('No destination folder found.'); } } @@ -215,20 +141,7 @@ class Rsync extends AbstractConfiguration public function switchCurrent(): void { $config = $this->get('rsync_config'); - $server = Context::get()->getHost(); - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - if ($server instanceof Localhost) { - runLocally( - 'rsync \ - -e \'ssh -p {{port}}' . $identityFile . '\' \ - {{rsync_flags}} \ - {{rsync_options}} \ - \'{{rsync_src}}/current\' {{rsync_dest}}', - $config - ); - return; - } - + $identityFile = $this->get('identity_file') ? ' -i ' . $this->get('identity_file') : ''; runLocally( 'rsync \ -e \'ssh -p {{port}}' . $identityFile . '\' \ @@ -239,67 +152,6 @@ class Rsync extends AbstractConfiguration ); } - /** - * Sync current after release was uploaded - */ - public function switchCurrentAdditionalTargets(): void - { - $configName = 'additional_remote_targets'; - if ($this->hasArray($configName)) { - $config = $this->get('rsync_config'); - $identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : ''; - $targets = $this->get($configName); - foreach ($targets as $target) { - $port = $target['port'] ?? '{{port}}'; - $user = $target['user'] ?? '{{user}}'; - $remotePath = $target['remote_path'] ?: '{{remote_path}}'; - runLocally( - 'rsync \ - -e \'ssh -p ' . $port . $identityFile . '\' \ - {{rsync_flags}} \ - {{rsync_options}} \ - \'{{rsync_src}}/current\'' . ' ' . $user . '@' . $target['hostname'] . ':\'' . $remotePath . '/\'', - $config - ); - } - } else { - writeln('No additional remote target configured'); - } - } - - protected function getSource(): string - { - $source = $this->get('rsync_src'); - while (is_callable($source)) { - $source = $source(); - } - - if (!trim($source)) { - // if $src is not set here rsync is going to do a directory listing - // exiting with code 0, since only doing a directory listing clearly - // is not what we want to achieve we need to throw an exception - throw new RuntimeException('You need to specify a source path.'); - } - - return $source; - } - - protected function getDestination(): string - { - $destination = $this->get('rsync_dest'); - while (is_callable($destination)) { - $destination = $destination(); - } - - if (!trim($destination)) { - // if $dst is not set here we are going to sync to root - // and even worse - depending on rsync flags and permission - - // might end up deleting everything we have write permission to - throw new RuntimeException('You need to specify a destination path.'); - } - - return $destination; - } public function rsyncConfig(): array { diff --git a/composer.json b/composer.json index daaa08e..6966e67 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "project", "require": { - "deployer/deployer": "^v6.9 || dev-master", + "deployer/deployer": "^v7.5.0", "phpdocumentor/reflection-docblock": "^5.3" }, diff --git a/deploy.php b/deploy.php index e190861..dfdad7d 100644 --- a/deploy.php +++ b/deploy.php @@ -1,3 +1,4 @@