[TASK] Add base files

This commit is contained in:
Sebastian Fischer 2024-12-14 14:10:55 +01:00
commit b5ad2bb9c8
11 changed files with 1438 additions and 0 deletions

51
.editorconfig Normal file
View File

@ -0,0 +1,51 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
# TS/JS-Files
[*.{ts,js,es6}]
indent_size = 2
# JSON-Files
[*.json]
indent_style = tab
# ReST-Files
[*.rst]
indent_size = 3
max_line_length = 80
# YAML-Files
[*.{yaml,yml}]
indent_size = 2
# package.json
[package.json]
indent_size = 2
# TypoScript
[*.{typoscript,tsconfig}]
indent_size = 2
# XLF-Files
[*.xlf]
indent_style = tab
# SQL-Files
[*.sql]
indent_style = tab
indent_size = 2
# .htaccess
[{_.htaccess,.htaccess}]
indent_style = tab

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.cache/
.idea/
vendor/
composer.lock

26
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,26 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Composer.gitlab-ci.yml
# Publishes a tag/branch to Composer Packages of the current project
publish:
image: curlimages/curl:latest
stage: build
rules:
- if: '$CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "main" || $CI_COMMIT_TAG =~ /^\d+.\d+.\d+/'
variables:
URL: "$CI_SERVER_PROTOCOL://$CI_SERVER_HOST:$CI_SERVER_PORT/api/v4/projects/$CI_PROJECT_ID/packages/composer?job_token=$CI_JOB_TOKEN"
script:
- version=$([[ -z "$CI_COMMIT_TAG" ]] && echo "branch=$CI_COMMIT_REF_NAME" || echo "tag=$CI_COMMIT_TAG")
- insecure=$([ "$CI_SERVER_PROTOCOL" = "http" ] && echo "--insecure" || echo "")
- response=$(curl -s -w "\n%{http_code}" $insecure --data $version $URL)
- code=$(echo "$response" | tail -n 1)
- body=$(echo "$response" | head -n 1)
# Output state information
- if [ $code -eq 201 ]; then
echo "Package created - Code $code - $body";
else
echo "Could not create package - Code $code - $body";
exit 1;
fi

View File

@ -0,0 +1,91 @@
<?php
namespace Evoweb\DeployerConfig\Config;
use Deployer\Deployer;
use Deployer\Task\Task;
use phpDocumentor\Reflection\DocBlockFactory;
use ReflectionMethod;
use function Deployer\get;
use function Deployer\has;
use function Deployer\set;
abstract class AbstractConfiguration
{
protected array $tasks = [];
protected function initializeTasks(): void
{
foreach ($this->tasks as $name => $config) {
$this->registerTask($name, $config);
}
}
protected function registerTask(string $name, array $config): void
{
$methodName = $config['methodName'] ?? '';
if ($methodName === '' || !method_exists(static::class, $methodName)) {
return;
}
$task = new Task($name, $this->$methodName(...));
$description = $this->getFunctionSummary($methodName);
if ($description) {
$task->desc($description);
}
$stages = $config['stages'] ?? [];
if (count($stages)) {
$task->onStage(...$stages);
}
$deployer = Deployer::get();
$deployer->tasks->set($name, $task);
}
protected function getFunctionSummary(string $function): string
{
$summary = '';
try {
$reflector = new ReflectionMethod(Deployment::class, $function);
$comment = $reflector->getDocComment();
if ($comment) {
$summary = DocBlockFactory::createInstance()->create($comment)->getSummary();
}
} catch (\Exception) {
}
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
);
}
protected function set(string $name, $value): void
{
set($name, $value);
}
protected function has(string $name): bool
{
return has($name) || Deployer::get()->config->has($name);
}
protected function hasArray(string $name): bool
{
return $this->has($name) && is_array($this->get($name));
}
}

View File

@ -0,0 +1,660 @@
<?php
namespace Evoweb\DeployerConfig\Config;
use Deployer\Exception\Exception;
use Deployer\Host\Host;
use Deployer\Task\Context;
use Deployer\Type\Csv;
use function Deployer\after;
use function Deployer\cd;
use function Deployer\commandExist;
use function Deployer\get;
use function Deployer\inventory;
use function Deployer\parse;
use function Deployer\run;
use function Deployer\task;
use function Deployer\test;
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: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(
'<comment>To speed up composer installation setup "unzip" command' .
' with PHP zip extension https://goo.gl/sxzFcD</comment>'
);
}
// 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');
}
}
}
}

457
Classes/Config/Rsync.php Normal file
View File

@ -0,0 +1,457 @@
<?php
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;
class Rsync extends AbstractConfiguration
{
protected array $defaultConfig = [
'exclude' => [
'.git',
'deploy.php',
'deploy.lock',
'current',
'shared/',
'var/*',
],
'exclude-file' => false,
'include' => [
'var/',
],
'include-file' => false,
'filter' => [],
'filter-file' => false,
'filter-perdir' => false,
'flags' => 'rzlc',
'options' => ['delete', 'delete-after'],
'timeout' => 600,
];
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'],
];
public function __construct()
{
$this->setDefaultConfiguration();
$this->initializeTasks();
}
protected function setDefaultConfiguration(): void
{
$this->set('port', 22);
$this->set('rsync', []);
$this->set('rsync_default', $this->defaultConfig);
$this->set('rsync_src', '{{deploy_path}}');
$this->set('rsync_dest', '{{user}}@{{hostname}}:\'{{remote_path}}/\'');
$this->set('rsync_config', $this->rsyncConfig(...));
$this->set('rsync_flags', $this->rsyncFlags(...));
$this->set('rsync_excludes', $this->rsyncExcludes(...));
$this->set('rsync_excludes_download', $this->rsyncExcludesDownload(...));
$this->set('rsync_excludes_upload', $this->rsyncExcludesUpload(...));
$this->set('rsync_includes', $this->rsyncIncludes(...));
$this->set('rsync_filter', $this->rsyncFilter(...));
$this->set('rsync_options', $this->rsyncOptions(...));
$this->set('rsync_timeout', $this->rsyncTimeout(...));
}
/**
* Warmup remote Rsync target
*/
public function warmup(): void
{
if (!test('[ -d "{{deploy_path}}" ]')) {
runLocally('mkdir -p {{deploy_path}}');
}
if (test('[ -d "{{deploy_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') . ' \
{{rsync_excludes_download}} \
{{rsync_includes}} \
{{rsync_filter}} \
{{rsync_dest}} {{deploy_path}}'
);
} else {
writeln('<comment>No destination folder found.</comment>');
}
}
/**
* 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}}'
);
}
}
/**
* Rsync local->remote
*/
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) {
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}} \
{{rsync_timeout}} \
{{rsync_includes}} \
--exclude=' . escapeshellarg('shared/') . ' \
{{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
);
}
} else {
writeln('No additional remote target configured');
}
}
/**
* Sync current after release was uploaded
*/
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;
}
runLocally(
'rsync \
-e \'ssh -p {{port}}' . $identityFile . '\' \
{{rsync_flags}} \
{{rsync_options}} \
\'{{rsync_src}}/current\' {{user}}@{{hostname}}:\'{{remote_path}}/\'',
$config
);
}
/**
* 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
{
$default = $this->get('rsync_default');
$config = $this->get('rsync');
return Rsync::arrayMergeRecursiveDistinct($default, $config);
}
public function rsyncFlags(): string
{
$config = $this->get('rsync_config');
$flags = $config['flags'] ?? '';
return $flags ? ' -' . $flags : '';
}
public function rsyncExcludes(): string
{
$config = $this->get('rsync_config');
$excludes = $config['exclude'] ?? [];
$excludeFile = $config['exclude-file'] ?? '';
$excludesRsync = '';
foreach ($excludes as $exclude) {
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
}
if (
!empty($excludeFile)
&& file_exists($excludeFile)
&& is_file($excludeFile)
&& is_readable($excludeFile)
) {
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
}
return $excludesRsync;
}
public function rsyncExcludesDownload(): string
{
$config = $this->get('rsync_config');
$excludes = $config['exclude_download'] ?? $config['exclude'] ?? [];
$excludeFile = $config['exclude-file'] ?? '';
$excludesRsync = '';
foreach ($excludes as $exclude) {
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
}
if (
!empty($excludeFile)
&& file_exists($excludeFile)
&& is_file($excludeFile)
&& is_readable($excludeFile)
) {
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
}
return $excludesRsync;
}
public function rsyncExcludesUpload(): string
{
$config = $this->get('rsync_config');
$excludes = $config['exclude_upload'] ?? $config['exclude'] ?? [];
$excludeFile = $config['exclude-file'] ?? '';
$excludesRsync = '';
foreach ($excludes as $exclude) {
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
}
if (
!empty($excludeFile)
&& file_exists($excludeFile)
&& is_file($excludeFile)
&& is_readable($excludeFile)
) {
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
}
return $excludesRsync;
}
public function rsyncIncludes(): string
{
$config = $this->get('rsync_config');
$includes = $config['include'] ?? [];
$includeFile = $config['include-file'] ?? '';
$includesRsync = '';
foreach ($includes as $include) {
$includesRsync .= ' --include=' . escapeshellarg($include);
}
if (
!empty($includeFile)
&& file_exists($includeFile)
&& is_file($includeFile)
&& is_readable($includeFile)
) {
$includesRsync .= ' --include-from=' . escapeshellarg($includeFile);
}
return $includesRsync;
}
public function rsyncFilter(): string
{
$config = $this->get('rsync_config');
$filters = $config['filter'] ?? [];
$filterFile = $config['filter-file'] ?? '';
$filterPerDir = $config['filter-perdir'] ?? '';
$filtersRsync = '';
foreach ($filters as $filter) {
$filtersRsync .= " --filter='$filter'";
}
if (!empty($filterFile)) {
$filtersRsync .= " --filter='merge $filterFile'";
}
if (!empty($filterPerDir)) {
$filtersRsync .= " --filter='dir-merge $filterPerDir'";
}
return $filtersRsync;
}
public function rsyncOptions(): string
{
$config = $this->get('rsync_config');
$options = $config['options'] ?? [];
$optionsRsync = [];
foreach ($options as $option) {
$optionsRsync[] = '--' . $option;
}
return ' ' . implode(' ', $optionsRsync);
}
public function rsyncTimeout(): string
{
$config = $this->get('rsync_config');
$timeout = $config['timeout'] ?? 0;
return $timeout ? ' --timeout=' . $timeout : '';
}
public static function arrayMergeRecursiveDistinct(array $array1, array &$array2): array
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Evoweb\DeployerConfig\Services;
use Composer\Script\Event;
class Environment
{
public static function setContext(Event $event)
{
putenv('CI_PROJECT_DIR=' . dirname(getcwd()));
putenv('ENVIRONMENT_NAME=' . strtoupper(end($_SERVER['argv'])));
$extras = $event->getComposer()->getPackage()->getExtra();
foreach (($extras['deployer'] ?? []) as $key => $value) {
putenv(strtoupper($key) . '=' . $value);
}
}
}

85
README.md Normal file
View File

@ -0,0 +1,85 @@
## To install add Build/composer.json to the project
```json
{
"name": "cp/build",
"description": "Project deployment with support for deployphp",
"repositories": [
{
"type": "git",
"url": "https://gitlab.cp-compartner.de/cpcompartner/build-config.git"
}
],
"require": {
"cp/build-config": "^1.7.3"
},
"scripts": {
"staging": "dep -vv --file=vendor/cp/build-config/deploy.php deploy staging --branch",
"production": "dep --file=vendor/cp/build-config/deploy.php deploy production --tag",
"phpcs-fixer": "php-cs-fixer fix --config=vendor/cp/build-config/php_cs.php",
"phpstan": "phpstan analyse --configuration=vendor/cp/build-config/phpstan.neon",
"rector": "rector process --config=vendor/cp/build-config/rector.php",
"php72-lint": "find ../packages ../private/typo3conf -name '*.php' -exec php7.2 -l {} 1> /dev/null \\;",
"php74-lint": "find ../packages ../private/typo3conf -name '*.php' -exec php7.4 -l {} 1> /dev/null \\;"
}
}
```
## Usage per composer
Install it with:
```bash
./composer update
```
Deploy branch develop to staging
```bash
./composer staging develop
```
Deploy tag 1.0.0 to production
```bash
./composer production 1.0.0
```
PHPCS fixer usage:
```bash
./composer phpcs-fixer ../package/cp_sitepackage
```
PHPStan usage:
```bash
./composer phpstan ../package/cp_sitepackage
```
TYPO3 Rector usage:
```bash
./composer rector ../package/cp_sitepackage
```
## Overriding default values per host in hosts.yaml
```yaml
.base: &base
local: true
alias: '{{CI_HOST}}'
deploy_path: '{{CI_PROJECT_DIR}}/cache/{{ENVIRONMENT_NAME}}'
composer_options: '{{composer_action}} --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader --no-suggest --ignore-platform-reqs'
prepare_dirs:
- .dep
- releases
- shared/fileadmin
shared_dirs_private:
- fileadmin
writable_dirs:
- private/typo3temp
- var
writable_files:
- private/typo3conf/PackageStates.php
rsync:
exclude:
- '-,p private/typo3temp/var/Cache'
```

27
alias.sh Normal file
View File

@ -0,0 +1,27 @@
#!/bin/bash
function composer() {
mkdir -p "${HOME}/.config/composer"
mkdir -p "${HOME}/.cache/composer"
docker run -t \
--user $(id -u):33 \
--env COMPOSER_CACHE_DIR=/cache \
--env SSH_AUTH_SOCK=/ssh-agent \
--env CI_HOST \
--env CI_PROJECT_DIR \
--env ENVIRONMENT_NAME \
--env INSTANCE_ID \
--env ADDITIONAL_CONFIG_FILE \
--env TYPO3_CONTEXT \
--env STAGE \
--network db \
--volume "$(readlink -f ${SSH_AUTH_SOCK})":/ssh-agent \
--volume /etc/passwd:/etc/passwd:ro \
--volume "${HOME}":"${HOME}" \
--volume "${HOME}/.config/composer":/tmp \
--volume "${HOME}/.cache/composer":/cache \
--volume "${CI_PROJECT_DIR}":"${CI_PROJECT_DIR}" \
--volume "${PWD%}":/app \
evoweb/php:composer $@
}
alias composer=composer

16
composer.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "evoweb/deployer-config",
"description": "TYPO3 CMS Distribution with utilization of deployphp",
"type": "project",
"require": {
"deployer/deployer": "^v6.9 || dev-master",
"phpdocumentor/reflection-docblock": "^5.3"
},
"autoload": {
"psr-4": {
"Evoweb\\DeployerConfig\\": "Classes/"
}
}
}

3
deploy.php Normal file
View File

@ -0,0 +1,3 @@
<?php
new \Evoweb\DeployerConfig\Config\Deployment();