Compare commits
2 Commits
b997f70ecf
...
047235b84d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047235b84d | ||
|
|
d5ea903e4d |
@ -1,123 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Config;
|
||||
|
||||
use Deployer\Exception\ConfigurationException;
|
||||
use Deployer\Deployer;
|
||||
use Deployer\Exception\Exception;
|
||||
use Deployer\Exception\GracefulShutdownException;
|
||||
use Deployer\Host\Host;
|
||||
use Deployer\Task\Context;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Evoweb\DeployerConfig\Tasks\Deploy;
|
||||
use Evoweb\DeployerConfig\Tasks\Local;
|
||||
use Evoweb\DeployerConfig\Tasks\Remote;
|
||||
use Evoweb\DeployerConfig\Tasks\Rsync;
|
||||
|
||||
use function Deployer\after;
|
||||
use function Deployer\cd;
|
||||
use function Deployer\commandSupportsOption;
|
||||
use function Deployer\currentHost;
|
||||
use function Deployer\error;
|
||||
use RuntimeException;
|
||||
use function Deployer\get;
|
||||
use function Deployer\import;
|
||||
use function Deployer\info;
|
||||
use function Deployer\output;
|
||||
use function Deployer\parse;
|
||||
use function Deployer\run;
|
||||
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
|
||||
class Deployment
|
||||
{
|
||||
protected array $tasks = [
|
||||
'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'],
|
||||
|
||||
'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',
|
||||
]
|
||||
],
|
||||
|
||||
'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 = [])
|
||||
public function __construct()
|
||||
{
|
||||
if ($tasks !== []) {
|
||||
$this->tasks = $tasks;
|
||||
}
|
||||
$this->loadDeployerCommon();
|
||||
$this->setDefaultConfiguration();
|
||||
$this->loadHostConfiguration();
|
||||
$this->initializeTasks();
|
||||
$this->registerAfterTask();
|
||||
$this->registerTasks();
|
||||
|
||||
}
|
||||
|
||||
protected function loadDeployerCommon(): void
|
||||
{
|
||||
foreach (['/../../', '/../../../', '/../../../../'] as $path) {
|
||||
$file = realpath(__DIR__ . $path . 'vendor/deployer/deployer/recipe/common.php');
|
||||
if (file_exists($file)) {
|
||||
if ($file && file_exists($file)) {
|
||||
require $file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
new Rsync();
|
||||
}
|
||||
|
||||
protected function setDefaultConfiguration(): void
|
||||
@ -229,7 +152,7 @@ class Deployment extends AbstractConfiguration
|
||||
$releaseExists = testLocally('[ -h {{deploy_path}}/release ]');
|
||||
if ($releaseExists) {
|
||||
$link = runLocally("readlink {{deploy_path}}/release");
|
||||
return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link;
|
||||
return str_starts_with($link, '/') ? $link : get('deploy_path') . '/' . $link;
|
||||
} else {
|
||||
throw new Exception(parse('The "release_path" ({{deploy_path}}/release) does not exist.'));
|
||||
}
|
||||
@ -245,7 +168,7 @@ class Deployment extends AbstractConfiguration
|
||||
$httpUser = array_shift($candidates);
|
||||
|
||||
if (empty($httpUser)) {
|
||||
throw new \RuntimeException(
|
||||
throw new RuntimeException(
|
||||
"Can't detect http user name.\n" .
|
||||
"Please setup `http_user` config parameter."
|
||||
);
|
||||
@ -259,7 +182,7 @@ class Deployment extends AbstractConfiguration
|
||||
$httpGroup = array_shift($candidates);
|
||||
|
||||
if (empty($httpGroup)) {
|
||||
throw new \RuntimeException(
|
||||
throw new RuntimeException(
|
||||
"Can't detect http user name.\n" .
|
||||
"Please setup `http_group` config parameter."
|
||||
);
|
||||
@ -281,42 +204,14 @@ class Deployment extends AbstractConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerAfterTask(): void
|
||||
protected function registerTasks(): void
|
||||
{
|
||||
// [Optional] if deploy fails automatically unlock.
|
||||
after('deploy:failed', 'local:unlock');
|
||||
new Rsync();
|
||||
new Local();
|
||||
new Remote();
|
||||
new Deploy();
|
||||
}
|
||||
|
||||
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()->get('hostname');
|
||||
$host = new Host($hostname);
|
||||
// create new context with host
|
||||
$context = new Context($host);
|
||||
Context::push($context);
|
||||
|
||||
// execute the task
|
||||
\Closure::bind($callback, $this)();
|
||||
|
||||
// restore
|
||||
Context::pop();
|
||||
$config->set('local', true);
|
||||
Context::push($contextBackup);
|
||||
}
|
||||
}
|
||||
|
||||
protected function commandSupportsOption(string $command, string $option): bool
|
||||
{
|
||||
@ -341,611 +236,15 @@ class Deployment extends AbstractConfiguration
|
||||
// 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");
|
||||
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
|
||||
protected function set(string $name, $value): void
|
||||
{
|
||||
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 <fg=green;options=bold>{{what}}</> to <fg=magenta;options=bold>{{where}}</>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares host for deploy
|
||||
*/
|
||||
public function localSetup(): void
|
||||
{
|
||||
runLocally(
|
||||
<<<EOF
|
||||
[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}};
|
||||
cd {{deploy_path}};
|
||||
[ -d .dep ] || mkdir .dep;
|
||||
[ -d releases ] || mkdir releases;
|
||||
[ -d shared ] || mkdir shared;
|
||||
EOF
|
||||
);
|
||||
|
||||
// If current_path points to something like "/var/www/html", make sure it is
|
||||
// a symlink and not a directory.
|
||||
if (testLocally('[ ! -L {{current_path}} ] && [ -d {{current_path}} ]')) {
|
||||
throw error("There is a directory (not symlink) at {{current_path}}.\n Remove this directory so it can be replaced with a symlink for atomic deployments.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare remote path exists
|
||||
*/
|
||||
public function remotePrepare(): void
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks deploy
|
||||
*/
|
||||
public function localLock(): void
|
||||
{
|
||||
$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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating necessary folders
|
||||
*/
|
||||
public function localCreateFolder(): void
|
||||
{
|
||||
$sudo = $this->getSudo();
|
||||
|
||||
if (testLocally('[ ! -d {{release_path}}/var/cache ]')) {
|
||||
runLocally('mkdir -p {{release_path}}/var/cache');
|
||||
}
|
||||
runLocally($sudo . ' chmod -R 775 {{release_path}}/var/cache');
|
||||
|
||||
if (testLocally('[ ! -d {{release_path}}/var/log ]')) {
|
||||
runLocally('mkdir -p {{release_path}}/var/log');
|
||||
}
|
||||
runLocally($sudo . ' chmod -R 775 {{release_path}}/var/log');
|
||||
}
|
||||
|
||||
/**
|
||||
* Installing vendors
|
||||
*/
|
||||
public function localVendors(): void
|
||||
{
|
||||
if (!$this->commandExist('unzip')) {
|
||||
info(
|
||||
'<comment>To speed up composer installation setup "unzip" command' .
|
||||
' with PHP zip extension https://goo.gl/sxzFcD</comment>'
|
||||
);
|
||||
}
|
||||
|
||||
// if composer.json exists
|
||||
if (testLocally('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) {
|
||||
runLocally('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleanup files and/or directories
|
||||
*/
|
||||
public function localClearPaths(): void
|
||||
{
|
||||
$paths = get('clear_paths');
|
||||
$sudo = get('clear_use_sudo') ? 'sudo' : '';
|
||||
$batch = 100;
|
||||
|
||||
$commands = [];
|
||||
foreach ($paths as $path) {
|
||||
$commands[] = "$sudo rm -rf {{release_path}}/$path";
|
||||
}
|
||||
$chunks = array_chunk($commands, $batch);
|
||||
foreach ($chunks as $chunk) {
|
||||
runLocally(implode('; ', $chunk));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating symlink to release
|
||||
*/
|
||||
public function localSymlink(): void
|
||||
{
|
||||
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.
|
||||
|
||||
runLocally("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}"); // Atomic override symlink.
|
||||
runLocally("cd {{deploy_path}} && rm release"); // Remove release link.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change usergroup for the hole project
|
||||
*/
|
||||
public function remoteChangeGroup(): void
|
||||
{
|
||||
if ($this->get('http_group')) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set files and/or folders writable
|
||||
*/
|
||||
public function remoteWritable(): void
|
||||
{
|
||||
if ($this->hasArray('writable_dirs') || $this->hasArray('writable_files')) {
|
||||
$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 ($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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database migration
|
||||
*/
|
||||
public function remoteDbCompare(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix folder structure
|
||||
*/
|
||||
public function remoteFixFolderStructure(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache via typo3 cache:flush
|
||||
*/
|
||||
public function remoteClearCache(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 echoReleaseNumber(): void
|
||||
{
|
||||
info('Folder of this release is: {{release_path}}');
|
||||
set($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Evoweb\DeployerConfig\Config;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Tasks;
|
||||
|
||||
use Deployer\Deployer;
|
||||
use Deployer\Task\GroupTask;
|
||||
use Deployer\Task\Task;
|
||||
use Evoweb\DeployerConfig\Config\Deployment;
|
||||
use InvalidArgumentException;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use ReflectionMethod;
|
||||
|
||||
use function Deployer\get;
|
||||
use function Deployer\has;
|
||||
use function Deployer\set;
|
||||
use function Deployer\test;
|
||||
use function Deployer\testLocally;
|
||||
|
||||
abstract class AbstractConfiguration
|
||||
abstract class AbstractTasks
|
||||
{
|
||||
protected array $tasks = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaultConfiguration();
|
||||
$this->initializeTasks();
|
||||
}
|
||||
|
||||
protected function setDefaultConfiguration(): void
|
||||
{
|
||||
}
|
||||
|
||||
protected function initializeTasks(): void
|
||||
{
|
||||
array_walk($this->tasks, [$this, 'registerTask']);
|
||||
}
|
||||
|
||||
protected function registerTask(array $config, string $name): void
|
||||
protected function registerTask(array|string $body, string $name): void
|
||||
{
|
||||
$body = $config['body'] ?? '';
|
||||
if (is_string($body) && !method_exists($this, $body)) {
|
||||
return;
|
||||
if (!is_array($body)) {
|
||||
$name = $body;
|
||||
$body = lcfirst(str_replace('_', '', ucwords(strtolower(preg_replace('/[^:]+:/', '', $body)), '_')));
|
||||
if (!method_exists($this, $body)) {
|
||||
print_r('not found ' . $body . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable([$this, $body])) {
|
||||
@ -38,13 +57,9 @@ abstract class AbstractConfiguration
|
||||
} elseif (is_array($body)) {
|
||||
$task = new GroupTask($name, $body);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Task body should be a function or an array.');
|
||||
throw new InvalidArgumentException('Task body should be a function or an array.');
|
||||
}
|
||||
|
||||
array_map(function ($selector) use ($task) {
|
||||
$task->addSelector($selector);
|
||||
}, $config['stages'] ?? []);
|
||||
|
||||
$deployer = Deployer::get();
|
||||
$deployer->tasks->set($name, $task);
|
||||
}
|
||||
@ -63,9 +78,9 @@ abstract class AbstractConfiguration
|
||||
return $summary;
|
||||
}
|
||||
|
||||
protected function get(string $name, $default = null)
|
||||
protected function commandExist(string $command): bool
|
||||
{
|
||||
return get($name, $default);
|
||||
return testLocally("hash $command 2>/dev/null");
|
||||
}
|
||||
|
||||
protected function set(string $name, $value): void
|
||||
@ -73,6 +88,11 @@ abstract class AbstractConfiguration
|
||||
set($name, $value);
|
||||
}
|
||||
|
||||
protected function get(string $name, $default = null)
|
||||
{
|
||||
return get($name, $default);
|
||||
}
|
||||
|
||||
protected function has(string $name): bool
|
||||
{
|
||||
return has($name);
|
||||
@ -82,4 +102,26 @@ abstract class AbstractConfiguration
|
||||
{
|
||||
return $this->has($name) && is_array($this->get($name));
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
57
Classes/Tasks/Deploy.php
Normal file
57
Classes/Tasks/Deploy.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Tasks;
|
||||
|
||||
use function Deployer\after;
|
||||
|
||||
class Deploy extends AbstractTasks
|
||||
{
|
||||
protected array $tasks = [
|
||||
'deploy:prepare' => [
|
||||
'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',
|
||||
],
|
||||
|
||||
'deploy:publish' => [
|
||||
'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()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->registerAfterTask();
|
||||
}
|
||||
|
||||
protected function registerAfterTask(): void
|
||||
{
|
||||
// [Optional] if deploy fails automatically unlock.
|
||||
after('deploy:failed', 'local:unlock');
|
||||
}
|
||||
}
|
||||
486
Classes/Tasks/Local.php
Normal file
486
Classes/Tasks/Local.php
Normal file
@ -0,0 +1,486 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Tasks;
|
||||
|
||||
use Deployer\Exception\ConfigurationException;
|
||||
use Deployer\Exception\Exception;
|
||||
use Deployer\Exception\GracefulShutdownException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Deployer\currentHost;
|
||||
use function Deployer\error;
|
||||
use function Deployer\get;
|
||||
use function Deployer\info;
|
||||
use function Deployer\output;
|
||||
use function Deployer\parse;
|
||||
use function Deployer\runLocally;
|
||||
use function Deployer\set;
|
||||
use function Deployer\Support\escape_shell_argument;
|
||||
use function Deployer\testLocally;
|
||||
use function Deployer\timestamp;
|
||||
|
||||
class Local extends AbstractTasks
|
||||
{
|
||||
protected array $tasks = [
|
||||
'local:info',
|
||||
'local:setup',
|
||||
'local:lock',
|
||||
'local:unlock',
|
||||
'local:release',
|
||||
'local:update_code',
|
||||
'local:env',
|
||||
'local:shared',
|
||||
'local:writable',
|
||||
'local:write_release',
|
||||
'local:create_folder',
|
||||
'local:vendors',
|
||||
'local:clear_paths',
|
||||
'local:symlink',
|
||||
'local:cleanup',
|
||||
'local:echo_release_number',
|
||||
];
|
||||
|
||||
/**
|
||||
* Displays info about deployment
|
||||
*/
|
||||
public function info(): void
|
||||
{
|
||||
info("deploying <fg=green;options=bold>{{what}}</> to <fg=magenta;options=bold>{{where}}</>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares host for deploy
|
||||
*/
|
||||
public function setup(): void
|
||||
{
|
||||
runLocally(
|
||||
<<<EOF
|
||||
[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}};
|
||||
cd {{deploy_path}};
|
||||
[ -d .dep ] || mkdir .dep;
|
||||
[ -d releases ] || mkdir releases;
|
||||
[ -d shared ] || mkdir shared;
|
||||
EOF
|
||||
);
|
||||
|
||||
// If current_path points to something like "/var/www/html", make sure it is
|
||||
// a symlink and not a directory.
|
||||
if (testLocally('[ ! -L {{current_path}} ] && [ -d {{current_path}} ]')) {
|
||||
throw error("There is a directory (not symlink) at {{current_path}}.\n Remove this directory so it can be replaced with a symlink for atomic deployments.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks deploy
|
||||
*/
|
||||
public function lock(): void
|
||||
{
|
||||
$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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks deploy
|
||||
*/
|
||||
public function unlock(): void
|
||||
{
|
||||
// always success
|
||||
runLocally("rm -f {{deploy_path}}/.dep/deploy.lock");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares release
|
||||
*/
|
||||
public function release(): 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 updateCode(): 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 env(): 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 shared(): 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 writable(): 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 writeRelease(): void
|
||||
{
|
||||
runLocally('echo {{target}} > {{release_path}}/release');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating necessary folders
|
||||
*/
|
||||
public function createFolder(): void
|
||||
{
|
||||
$sudo = $this->getSudo();
|
||||
|
||||
if (testLocally('[ ! -d {{release_path}}/var/cache ]')) {
|
||||
runLocally('mkdir -p {{release_path}}/var/cache');
|
||||
}
|
||||
runLocally($sudo . ' chmod -R 775 {{release_path}}/var/cache');
|
||||
|
||||
if (testLocally('[ ! -d {{release_path}}/var/log ]')) {
|
||||
runLocally('mkdir -p {{release_path}}/var/log');
|
||||
}
|
||||
runLocally($sudo . ' chmod -R 775 {{release_path}}/var/log');
|
||||
}
|
||||
|
||||
/**
|
||||
* Installing vendors
|
||||
*/
|
||||
public function vendors(): void
|
||||
{
|
||||
if (!$this->commandExist('unzip')) {
|
||||
info(
|
||||
'<comment>To speed up composer installation setup "unzip" command' .
|
||||
' with PHP zip extension https://goo.gl/sxzFcD</comment>'
|
||||
);
|
||||
}
|
||||
|
||||
// if composer.json exists
|
||||
if (testLocally('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) {
|
||||
runLocally('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup files and/or directories
|
||||
*/
|
||||
public function clearPaths(): void
|
||||
{
|
||||
$paths = get('clear_paths');
|
||||
$sudo = get('clear_use_sudo') ? 'sudo' : '';
|
||||
$batch = 100;
|
||||
|
||||
$commands = [];
|
||||
foreach ($paths as $path) {
|
||||
$commands[] = "$sudo rm -rf {{release_path}}/$path";
|
||||
}
|
||||
$chunks = array_chunk($commands, $batch);
|
||||
foreach ($chunks as $chunk) {
|
||||
runLocally(implode('; ', $chunk));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating symlink to release
|
||||
*/
|
||||
public function symlink(): void
|
||||
{
|
||||
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.
|
||||
|
||||
runLocally("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}"); // Atomic override symlink.
|
||||
runLocally("cd {{deploy_path}} && rm release"); // Remove release link.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleaning up old releases
|
||||
*/
|
||||
public function cleanup(): 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function echoReleaseNumber(): void
|
||||
{
|
||||
info('Folder of this release is: {{release_path}}');
|
||||
}
|
||||
}
|
||||
165
Classes/Tasks/Remote.php
Normal file
165
Classes/Tasks/Remote.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Tasks;
|
||||
|
||||
use function Deployer\get;
|
||||
use function Deployer\parse;
|
||||
use function Deployer\run;
|
||||
use function Deployer\Support\str_contains;
|
||||
use function Deployer\test;
|
||||
use function Deployer\writeln;
|
||||
|
||||
class Remote extends AbstractTasks
|
||||
{
|
||||
protected array $tasks = [
|
||||
'remote:prepare',
|
||||
'remote:change_group',
|
||||
'remote:writable',
|
||||
'remote:db_compare',
|
||||
'remote:fix_folder_structure',
|
||||
'remote:clear_cache',
|
||||
'remote:clear_opcache',
|
||||
];
|
||||
|
||||
/**
|
||||
* Prepare remote path exists
|
||||
*/
|
||||
public function prepare(): void
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change usergroup for the hole project
|
||||
*/
|
||||
public function changeGroup(): void
|
||||
{
|
||||
if ($this->get('http_group')) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set files and/or folders writable
|
||||
*/
|
||||
public function writable(): void
|
||||
{
|
||||
if ($this->hasArray('writable_dirs') || $this->hasArray('writable_files')) {
|
||||
$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 ($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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database migration
|
||||
*/
|
||||
public function dbCompare(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix folder structure
|
||||
*/
|
||||
public function fixFolderStructure(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache via typo3 cache:flush
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear opcache via curl on {CI_HOST}/cache.php
|
||||
*/
|
||||
public function clearOpcache(): void
|
||||
{
|
||||
$webDomain = rtrim($this->get('web_domain'), '/');
|
||||
$htaccess = $this->has('htaccess') ? '--user ' . $this->get('htaccess') : '';
|
||||
run('curl ' . $htaccess . ' -sk ' . $webDomain . '/cache.php');
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Evoweb\DeployerConfig\Config;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evoweb\DeployerConfig\Tasks;
|
||||
|
||||
use function Deployer\runLocally;
|
||||
use function Deployer\test;
|
||||
use function Deployer\testLocally;
|
||||
use function Deployer\warning;
|
||||
|
||||
class Rsync extends AbstractConfiguration
|
||||
class Rsync extends AbstractTasks
|
||||
{
|
||||
protected array $defaultConfig = [
|
||||
'exclude' => [
|
||||
@ -40,17 +42,11 @@ class Rsync extends AbstractConfiguration
|
||||
];
|
||||
|
||||
protected array $tasks = [
|
||||
'rsync:warmup' => ['body' => 'warmup'],
|
||||
'rsync:remote' => ['body' => 'remote'],
|
||||
'rsync:switch_current' => ['body' => 'switchCurrent'],
|
||||
'rsync:warmup',
|
||||
'rsync:remote',
|
||||
'rsync:switch_current',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaultConfiguration();
|
||||
$this->initializeTasks();
|
||||
}
|
||||
|
||||
protected function setDefaultConfiguration(): void
|
||||
{
|
||||
$this->set('port', 22);
|
||||
Loading…
Reference in New Issue
Block a user