661 lines
22 KiB
PHP
661 lines
22 KiB
PHP
<?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');
|
|
}
|
|
}
|
|
}
|
|
}
|