['methodName' => 'remotePrepare'], 'rsync:warmup' => [], 'deploy:prepare' => [], 'deploy:lock' => [], 'deploy:release' => [], 'deploy:update_code' => [], //'rsync:update_code' => [], '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'], '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' => [], ]; public function __construct(array $tasks = []) { if (count($tasks)) { $this->tasks = $tasks; } $this->loadBasicDeployerFiles(); $this->setDefaultConfiguration(); $this->loadHostConfiguration(); $this->initializeTasks(); $this->initializeMainTask(); } protected function loadBasicDeployerFiles(): void { $commonFile = realpath(__DIR__ . '/../../../../Build/vendor/deployer/deployer/recipe/common.php'); if (file_exists($commonFile)) { require $commonFile; } new Rsync(); } protected function setDefaultConfiguration(): void { $this->set('CI_PROJECT_DIR', getEnv('CI_PROJECT_DIR')); $this->set('CI_HOST', getEnv('CI_HOST')); $this->set('ENVIRONMENT_NAME', getEnv('ENVIRONMENT_NAME')); $this->set('INSTANCE_ID', getEnv('INSTANCE_ID')); $this->set('app_container_path', ''); $this->set('prepare_dirs', [ '.dep', 'releases', 'shared', ]); $this->set('ignore_update_code_paths', [ 'var/cache', 'var/log/*', 'Build', 'resources', '.idea', '.editorconfig', '.git', '.gitignore', '.gitlab-ci.yml', ]); $this->set('writable_dirs', [ 'var', ]); $this->set('clear_paths', [ 'composer.json', 'composer.lock', ]); } protected function loadHostConfiguration(): void { // read configuration inventory(__DIR__ . '/../../../../Build/hosts.yaml'); } protected function initializeMainTask(): 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') . '" ' : ''; } protected function getSudo(): string { return $this->get('clear_use_sudo') ? 'sudo ' : ''; } protected function processRemoteTask(callable $callback): void { $config = Context::get()->getConfig(); 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); $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::push($context); // execute the task \Closure::bind($callback, $this)(); // restore Context::pop(); $config->set('local', true); Context::push($contextBackup); } } /** * Prepare remote path exists */ public function remotePrepare(): void { if ($this->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); } } /** * Installing vendors */ public function localVendors(): void { if (!commandExist('unzip')) { writeln( 'To speed up composer installation setup "unzip" command' . ' with PHP zip extension https://goo.gl/sxzFcD' ); } // if composer.json exists if (test('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) { run('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};'); } } /** * Creating necessary folders */ public function localCreateFolder(): void { $sudo = $this->getSudo(); if (test('[ ! -d {{release_path}}/var/cache ]')) { run('mkdir -p {{release_path}}/var/cache'); } run($sudo . ' chmod -R 775 {{release_path}}/var/cache'); if (test('[ ! -d {{release_path}}/var/log ]')) { run('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'); } /** * Creating symlinks for shared files and dirs in cms private folder */ public function localShared(): void { if ($this->hasArray('shared_dirs_private')) { $this->sharedFolders($this->get('shared_dirs_private'), 'private'); } 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'); } } 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" */ public function localWritableFiles(): 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); } } } } /** * 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); } } } } /** * Creating symlink to release */ public function localSymlink(): void { $sudo = $this->getSudo(); if (test('[ -L "{{deploy_path}}/current" ] || [ -D "{{deploy_path}}/current" ]')) { // Remove previous current run($sudo . ' rm {{deploy_path}}/current'); } // Atomic override symlink. run($sudo . ' cd {{deploy_path}} && {{bin/symlink}} releases/{{release_name}} current'); // Remove release link. run($sudo . ' cd {{deploy_path}} && rm release'); } /** * Change usergroup for the hole project */ public function remoteChangeGroup(): void { if ($this->get('http_group')) { $callback = function () { $sudo = $this->getSudo(); $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); } } /** * Set files and/or folders writable */ 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}}/'); 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 ' . $httpGroup . ' ' . $path . $file, $runOpts); } } } } }; $this->processRemoteTask($callback); } } /** * Database migration */ public function remoteDbCompare(): void { $callback = function () { $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); $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); } /** * Fix folder structure */ public function remoteFixFolderStructure(): void { $callback = function () { $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); $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); } /** * Clear cache via typo3 cache:flush */ public function remoteClearCache(): void { $callback = function () { $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); $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); } /** * Clear opcache via curl on {CI_HOST}/cache.php */ public function remoteClearOpcache(): void { $webDomain = rtrim($this->get('web_domain'), '/'); $htaccess = $this->has('htaccess') ? '--user ' . $this->get('htaccess') : ''; 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'); } } } }