Add Deployer production setup for obvody.kmetix.cz.

Co-authored-by: Cursor <cursoragent@cursor.com>
master
Kowalski 2026-11-07 07:28:59 +01:00
parent d6158d1c9f
commit aa490e2ba0
502 changed files with 82174 additions and 9 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
# KOW # KOW
temp/ temp/
log/ log/
config/local.neon
#ZAZA #ZAZA

25
Obvody DEPLOY.bat Normal file
View File

@ -0,0 +1,25 @@
@echo off
title Deploying Obvody to Production...
cd /d "%~dp0"
echo ========================================
echo Spoustim deployment na produkcni server...
echo ========================================
echo.
php vendor/bin/dep deploy production
if %ERRORLEVEL% EQU 0 (
echo.
echo ========================================
echo Hotovo! Deployment probehl uspesne.
echo ========================================
) else (
echo.
echo ========================================
echo CHYBA: Deployment selhal!
echo ========================================
)
echo.
pause

View File

@ -53,5 +53,10 @@ class Bootstrap
$configDir = $this->rootDir . '/config'; $configDir = $this->rootDir . '/config';
$this->configurator->addConfig($configDir . '/common.neon'); $this->configurator->addConfig($configDir . '/common.neon');
$this->configurator->addConfig($configDir . '/services.neon'); $this->configurator->addConfig($configDir . '/services.neon');
$localConfig = $configDir . '/local.neon';
if (is_file($localConfig) && filesize($localConfig) > 0) {
$this->configurator->addConfig($localConfig);
}
} }
} }

View File

@ -22,7 +22,8 @@
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.5", "nette/tester": "^2.5",
"symfony/thanks": "^1" "symfony/thanks": "^1",
"deployer/deployer": "^8.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

997
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
# Zkopíruj na server do shared/config/local.neon (Deployer symlinkuje).
# Přepisuje hodnoty z common.neon — sem patří produkční hesla a DB.
database:
dsn: 'sqlsrv:server=localhost;Database=obvody;TrustServerCertificate=true;LoginTimeout=6;'
user: obvody_user
password: 'ZMEN_ME'

51
deploy.php Normal file
View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Deployer;
require 'recipe/composer.php';
set('application', 'obvody');
// HTTPS — Gitea SSH na portu 22 neběží (systémový SSH je 52222)
set('repository', 'https://git.kmetix.cz/kowalski/Obvody.git');
set('keep_releases', 5);
set('git_tty', false);
set('public_path', 'www');
set('shared_dirs', [
'log',
]);
set('shared_files', [
'config/local.neon',
]);
set('writable_dirs', [
'log',
'temp',
]);
// ACL: nové soubory od www-data zůstane deploy smět mazat (bez sudo hesla)
set('writable_mode', 'acl');
set('http_user', 'www-data');
set('http_group', 'www-data');
set('cleanup_use_sudo', false);
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');
host('production')
->setHostname('kmetix.cz')
->setPort(52222)
->setRemoteUser('deploy')
->setSshMultiplexing(false) // Windows + ControlMaster = getsockname failed
->set('deploy_path', '/var/www/html/obvody.kmetix.cz')
->set('branch', 'master');
desc('Vyčistí Nette temp cache v aktuální release');
task('deploy:clear_cache', function () {
// Nová release patří deploy — sudo není potřeba
run('rm -rf {{release_path}}/temp/cache/*');
});
after('deploy:symlink', 'deploy:clear_cache');
after('deploy:failed', 'deploy:unlock');

View File

@ -0,0 +1,33 @@
# Ukázkový Apache vhost pro Ubuntu.
# DocumentRoot musí mířit na Deployer symlink current/www.
#
# 1) Zkopíruj / uprav do /etc/apache2/sites-available/obvody.kmetix.cz.conf
# 2) Uprav ServerName a cesty
# 3) a2ensite obvody.kmetix.cz && systemctl reload apache2
#
# V php.ini (nebo poolu) nastav:
# opcache.revalidate_path=1
# jinak Apache po symlinku current neuvidí novou release.
<VirtualHost *:80>
ServerName obvody.kmetix.cz
DocumentRoot /var/www/html/obvody.kmetix.cz/current/www
<Directory /var/www/html/obvody.kmetix.cz/current/www>
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks
</Directory>
# Zbytek projektu (app/, config/, vendor/, …) nesmí být webově dostupný.
<Directory /var/www/html/obvody.kmetix.cz/current>
Require all denied
</Directory>
<Directory /var/www/html/obvody.kmetix.cz/current/www>
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/obvody-error.log
CustomLog ${APACHE_LOG_DIR}/obvody-access.log combined
</VirtualHost>

119
vendor/bin/dep vendored Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../deployer/deployer/bin/dep)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/deployer/deployer/bin/dep');
}
}
return include __DIR__ . '/..'.'/deployer/deployer/bin/dep';

5
vendor/bin/dep.bat vendored Normal file
View File

@ -0,0 +1,5 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/dep
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*

119
vendor/bin/yaml-lint vendored Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
}
}
return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';

5
vendor/bin/yaml-lint.bat vendored Normal file
View File

@ -0,0 +1,5 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/yaml-lint
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*

View File

@ -613,6 +613,7 @@ return array(
'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\UnknownImageFileException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => $vendorDir . '/nette/utils/src/Utils/Validators.php', 'Nette\\Utils\\Validators' => $vendorDir . '/nette/utils/src/Utils/Validators.php',
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'Tester\\Assert' => $vendorDir . '/nette/tester/src/Framework/Assert.php', 'Tester\\Assert' => $vendorDir . '/nette/tester/src/Framework/Assert.php',
'Tester\\AssertException' => $vendorDir . '/nette/tester/src/Framework/AssertException.php', 'Tester\\AssertException' => $vendorDir . '/nette/tester/src/Framework/AssertException.php',
'Tester\\CodeCoverage\\Collector' => $vendorDir . '/nette/tester/src/CodeCoverage/Collector.php', 'Tester\\CodeCoverage\\Collector' => $vendorDir . '/nette/tester/src/CodeCoverage/Collector.php',

View File

@ -6,5 +6,13 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'135133ad0ca20ef21cc262b30da9a000' => $vendorDir . '/deployer/deployer/src/functions.php',
'eee1afd8f38c52a8d46f7c5bbb92afdd' => $vendorDir . '/deployer/deployer/src/Support/helpers.php',
'd507e002f7fce7f0c6dbf1f22edcb902' => $vendorDir . '/tracy/tracy/src/Tracy/functions.php', 'd507e002f7fce7f0c6dbf1f22edcb902' => $vendorDir . '/tracy/tracy/src/Tracy/functions.php',
); );

View File

@ -7,5 +7,17 @@ $baseDir = dirname($vendorDir);
return array( return array(
'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'), 'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Maml\\' => array($vendorDir . '/maml/maml/src'),
'Deployer\\' => array($vendorDir . '/deployer/deployer/src'),
'App\\' => array($baseDir . '/app'), 'App\\' => array($baseDir . '/app'),
); );

View File

@ -7,6 +7,14 @@ namespace Composer\Autoload;
class ComposerStaticInit9c49a418f02ffb235919088891200a26 class ComposerStaticInit9c49a418f02ffb235919088891200a26
{ {
public static $files = array ( public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'135133ad0ca20ef21cc262b30da9a000' => __DIR__ . '/..' . '/deployer/deployer/src/functions.php',
'eee1afd8f38c52a8d46f7c5bbb92afdd' => __DIR__ . '/..' . '/deployer/deployer/src/Support/helpers.php',
'd507e002f7fce7f0c6dbf1f22edcb902' => __DIR__ . '/..' . '/tracy/tracy/src/Tracy/functions.php', 'd507e002f7fce7f0c6dbf1f22edcb902' => __DIR__ . '/..' . '/tracy/tracy/src/Tracy/functions.php',
); );
@ -14,6 +22,27 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26
'S' => 'S' =>
array ( array (
'Symfony\\Thanks\\' => 15, 'Symfony\\Thanks\\' => 15,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31,
'Symfony\\Polyfill\\Ctype\\' => 23,
'Symfony\\Contracts\\Service\\' => 26,
'Symfony\\Component\\Yaml\\' => 23,
'Symfony\\Component\\String\\' => 25,
'Symfony\\Component\\Process\\' => 26,
'Symfony\\Component\\Console\\' => 26,
),
'P' =>
array (
'Psr\\Container\\' => 14,
),
'M' =>
array (
'Maml\\' => 5,
),
'D' =>
array (
'Deployer\\' => 9,
), ),
'A' => 'A' =>
array ( array (
@ -26,6 +55,54 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26
array ( array (
0 => __DIR__ . '/..' . '/symfony/thanks/src', 0 => __DIR__ . '/..' . '/symfony/thanks/src',
), ),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
),
'Symfony\\Polyfill\\Intl\\Grapheme\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Symfony\\Contracts\\Service\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/service-contracts',
),
'Symfony\\Component\\Yaml\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/yaml',
),
'Symfony\\Component\\String\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/string',
),
'Symfony\\Component\\Process\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/process',
),
'Symfony\\Component\\Console\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/console',
),
'Psr\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'Maml\\' =>
array (
0 => __DIR__ . '/..' . '/maml/maml/src',
),
'Deployer\\' =>
array (
0 => __DIR__ . '/..' . '/deployer/deployer/src',
),
'App\\' => 'App\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/app', 0 => __DIR__ . '/../..' . '/app',
@ -640,6 +717,7 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26
'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\UnknownImageFileException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => __DIR__ . '/..' . '/nette/utils/src/Utils/Validators.php', 'Nette\\Utils\\Validators' => __DIR__ . '/..' . '/nette/utils/src/Utils/Validators.php',
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'Tester\\Assert' => __DIR__ . '/..' . '/nette/tester/src/Framework/Assert.php', 'Tester\\Assert' => __DIR__ . '/..' . '/nette/tester/src/Framework/Assert.php',
'Tester\\AssertException' => __DIR__ . '/..' . '/nette/tester/src/Framework/AssertException.php', 'Tester\\AssertException' => __DIR__ . '/..' . '/nette/tester/src/Framework/AssertException.php',
'Tester\\CodeCoverage\\Collector' => __DIR__ . '/..' . '/nette/tester/src/CodeCoverage/Collector.php', 'Tester\\CodeCoverage\\Collector' => __DIR__ . '/..' . '/nette/tester/src/CodeCoverage/Collector.php',

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,24 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'nette/web-project', 'name' => 'nette/web-project',
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-master',
'version' => '1.0.0.0', 'version' => 'dev-master',
'reference' => null, 'reference' => 'd6158d1c9f79ad3bc0a8f0a63047716ac6929029',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
'dev' => true, 'dev' => true,
), ),
'versions' => array( 'versions' => array(
'deployer/deployer' => array(
'pretty_version' => 'v8.0.5',
'version' => '8.0.5.0',
'reference' => '199273500b4f14ff5eafd76bea524e5d44bcaff3',
'type' => 'library',
'install_path' => __DIR__ . '/../deployer/deployer',
'aliases' => array(),
'dev_requirement' => true,
),
'latte/latte' => array( 'latte/latte' => array(
'pretty_version' => 'v3.0.20', 'pretty_version' => 'v3.0.20',
'version' => '3.0.20.0', 'version' => '3.0.20.0',
@ -19,6 +28,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'maml/maml' => array(
'pretty_version' => 'v3.2.0',
'version' => '3.2.0.0',
'reference' => 'f11cdff4325bf399f23c33fc1995450e40ac0854',
'type' => 'library',
'install_path' => __DIR__ . '/../maml/maml',
'aliases' => array(),
'dev_requirement' => true,
),
'nette/application' => array( 'nette/application' => array(
'pretty_version' => 'v3.2.6', 'pretty_version' => 'v3.2.6',
'version' => '3.2.6.0', 'version' => '3.2.6.0',
@ -173,14 +191,110 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'nette/web-project' => array( 'nette/web-project' => array(
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-master',
'version' => '1.0.0.0', 'version' => 'dev-master',
'reference' => null, 'reference' => 'd6158d1c9f79ad3bc0a8f0a63047716ac6929029',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'psr/container' => array(
'pretty_version' => '2.0.2',
'version' => '2.0.2.0',
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/container',
'aliases' => array(),
'dev_requirement' => true,
),
'psr/log-implementation' => array(
'dev_requirement' => true,
'provided' => array(
0 => '1.0|2.0|3.0',
),
),
'symfony/console' => array(
'pretty_version' => 'v7.4.23',
'version' => '7.4.23.0',
'reference' => '81957c2440c05c962fe8291f318de6e85f8db330',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/console',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.8.1',
'version' => '3.8.1.0',
'reference' => 'feb1edf4e546af3b85c5caebe8359a20db230ca3',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-intl-grapheme' => array(
'pretty_version' => 'v1.43.3',
'version' => '1.43.3.0',
'reference' => '5a8f33c87aeecffce24e721b5064fab05f4becf8',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-intl-normalizer' => array(
'pretty_version' => 'v1.43.0',
'version' => '1.43.0.0',
'reference' => 'ebd57ccc7edb93d632e184efbe7693d2517bab3b',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.43.3',
'version' => '1.43.3.0',
'reference' => '122889ed24dc000165bcbfd77ec412f15cbdfc6a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/process' => array(
'pretty_version' => 'v7.4.22',
'version' => '7.4.22.0',
'reference' => 'a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/process',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/service-contracts' => array(
'pretty_version' => 'v3.8.1',
'version' => '3.8.1.0',
'reference' => '052852a96fd5e5ebdb617dba22031e41a3300fb5',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/service-contracts',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/string' => array(
'pretty_version' => 'v7.4.22',
'version' => '7.4.22.0',
'reference' => '8dff7da3a56638f1d189c5368183294102de4ced',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/string',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/thanks' => array( 'symfony/thanks' => array(
'pretty_version' => 'v1.4.0', 'pretty_version' => 'v1.4.0',
'version' => '1.4.0.0', 'version' => '1.4.0.0',
@ -190,6 +304,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'symfony/yaml' => array(
'pretty_version' => 'v7.4.23',
'version' => '7.4.23.0',
'reference' => '1e601207ffe627a2e621faa9d661f1d32dcf1cec',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/yaml',
'aliases' => array(),
'dev_requirement' => true,
),
'tracy/tracy' => array( 'tracy/tracy' => array(
'pretty_version' => 'v2.10.9', 'pretty_version' => 'v2.10.9',
'version' => '2.10.9.0', 'version' => '2.10.9.0',

View File

@ -0,0 +1,4 @@
*
!.dockerignore
!Dockerfile
!deployer.phar

View File

@ -0,0 +1,23 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__ . '/bin')
->in(__DIR__ . '/src')
->in(__DIR__ . '/recipe')
->in(__DIR__ . '/contrib')
->in(__DIR__ . '/tests');
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS' => true,
// Due to historical reasons we have to keep this.
// Docs parser expects comment right after php tag.
'blank_line_after_opening_tag' => false,
// For PHP 7.4 compatibility.
'trailing_comma_in_multiline' => [
'elements' => ['arguments', 'array_destructuring', 'arrays']
],
])
->setFinder($finder);

22
vendor/deployer/deployer/Dockerfile vendored Normal file
View File

@ -0,0 +1,22 @@
FROM php:8.5-cli-alpine
RUN set -eux; \
apk add --no-cache \
bash \
git \
libzip \
openssh-client \
rsync \
unzip \
zip; \
apk add --no-cache --virtual .build-deps \
libzip-dev \
linux-headers; \
docker-php-ext-install -j"$(nproc)" pcntl sockets zip; \
apk del .build-deps
COPY --chmod=755 deployer.phar /bin/dep
WORKDIR /app
ENTRYPOINT ["/bin/dep"]

21
vendor/deployer/deployer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2013 Anton Medvedev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

52
vendor/deployer/deployer/README.md vendored Normal file
View File

@ -0,0 +1,52 @@
<h1>
<a href="https://deployer.org">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://deployer.org/img/logo-white.svg" height="30">
<img src="https://deployer.org/img/logo.svg" alt="Deployer Logo" height="30">
</picture>
</a>
Deployer
</h1>
<p>The PHP deployment tool with support for popular frameworks out of the box.</p>
<p align="center"><br><br><a href="https://deployer.org"><img src="https://medv.io/assets/deployer/deployer.gif" alt="Deployer Screenshot" width="530"></a><br><br><br></p>
---
<p style="font-size: 21px; color:black;">
Browser testing via<br>
<a href="https://www.testmu.ai" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/testmu-black-logo.png" width="800">
<img src=".github/testmu-white-logo.png" alt="Testmu AI" width="800">
</picture>
</a>
</p>
---
<a href="https://github.com/deployphp/deployer/actions?query=workflow%3Atest"><img src="https://github.com/deployphp/deployer/workflows/test/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/deployer/deployer"><img src="https://img.shields.io/packagist/v/deployer/deployer.svg?style=flat" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/deployer/deployer"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat" alt="License"></a>
See [deployer.org](https://deployer.org) for more information and documentation.
## Features
- Automatic server **provisioning**.
- **Zero downtime** deployments.
- Ready to use recipes for **most frameworks**.
## Additional resources
* [GitHub Action for Deployer](https://github.com/deployphp/action)
* [Deployer Docker Image](https://hub.docker.com/r/deployphp/deployer)
## License
[MIT](https://github.com/deployphp/deployer/blob/master/LICENSE)
<p align="center">
<a href="https://crow.watch/join/deployer">
<img src="https://github.com/user-attachments/assets/37c84073-6533-4746-951d-d879f90a7fd2" alt="Join Crow Watch" width="900" hight="600">
</a>
</p>

22
vendor/deployer/deployer/SECURITY.md vendored Normal file
View File

@ -0,0 +1,22 @@
# Security Policy
## Supported Versions
Deployer is generally backwards compatible with very few exceptions, so we
recommend users to always use the latest version to experience stability,
performance and security.
We generally backport security issues to a single previous major version,
unless this is not possible or feasible with a reasonable effort.
| Version | Supported |
|---------|--------------------|
| 8 | :white_check_mark: |
| <= 7 | :x: |
## Reporting a Vulnerability
If you believe you've discovered a serious vulnerability, please contact the
Expr core team at anton+security@medv.io. We will evaluate your report and if
necessary issue a fix and an advisory. If the issue was previously undisclosed,
we'll also mention your name in the credits.

101
vendor/deployer/deployer/bin/build vendored Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env php
<?php
/* (c) Anton Medvedev <anton@medv.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (ini_get('phar.readonly') === '1') {
fwrite(STDERR, "Writing to phar files is disabled. Change your `php.ini` or use `-d phar.readonly=false`.\n");
exit(1);
}
define('__ROOT__', realpath(__DIR__ . '/..'));
chdir(__ROOT__);
$opt = getopt('v:', ['nozip']);
$version = $opt['v'] ?? null;
if (empty($version)) {
fwrite(STDERR, "Please, specify version as \"-v8.0.0\".\n");
exit(1);
}
if (!preg_match('/^\d+\.\d+\.\d+(-\w+(\.\d+)?)?$/', $version)) {
fwrite(STDERR, "Version must be \"7.0.0-beta.42\". Got \"$version\".\n");
exit(1);
}
passthru('composer install --no-dev --prefer-dist --optimize-autoloader');
$pharName = "deployer.phar";
$pharFile = __ROOT__ . '/' . $pharName;
if (file_exists($pharFile)) {
unlink($pharFile);
}
$ignore = [
'.anton',
'.git',
'Tests',
'tests',
'deploy.php',
'.php-cs-fixer.dist.php',
];
$compress = !isset($opt['nozip']);
$phar = new Phar($pharFile, 0, $pharName);
$phar->setSignatureAlgorithm(Phar::SHA256);
$phar->startBuffering();
$iterator = new RecursiveDirectoryIterator(__ROOT__, FilesystemIterator::SKIP_DOTS);
$iterator = new RecursiveCallbackFilterIterator($iterator,
fn(SplFileInfo $fileInfo) => !in_array($fileInfo->getBasename(), $ignore, true),
);
$iterator = new RecursiveIteratorIterator($iterator);
$iterator = new CallbackFilterIterator($iterator,
// 'bash', 'fish', 'zsh' are completion templates
fn(SplFileInfo $fileInfo) => in_array($fileInfo->getExtension(), ['php', 'exe', 'bash', 'fish', 'zsh'], true),
);
foreach ($iterator as $fileInfo) {
$file = str_replace(__ROOT__, '', $fileInfo->getRealPath());
echo "+ $file\n";
$phar->addFile($fileInfo->getRealPath(), $file);
if ($compress) {
$phar[$file]->compress(Phar::GZ);
if (!$phar[$file]->isCompressed()) {
fwrite(STDERR, "Could not compress: $file\n");
}
}
}
// Add non-PHP files that the iterator skips.
foreach ([
'/recipe/provision/Caddyfile',
'/recipe/provision/404.html',
] as $extra) {
echo "+ $extra\n";
$phar->addFile(realpath(__ROOT__ . $extra), $extra);
}
// Add bin/dep with version baked in.
echo "+ /bin/dep\n";
$depContent = file_get_contents(__ROOT__ . '/bin/dep');
$depContent = str_replace("#!/usr/bin/env php\n", '', $depContent);
$depContent = str_replace('__FILE__', 'str_replace("phar://", "", Phar::running())', $depContent);
$depContent = preg_replace("/run\('.+?'/", "run('$version'", $depContent);
$phar->addFromString('bin/dep', $depContent);
$phar->setStub(<<<STUB
#!/usr/bin/env php
<?php
Phar::mapPhar('{$pharName}');
require 'phar://{$pharName}/bin/dep';
__HALT_COMPILER();
STUB);
$phar->stopBuffering();
unset($phar);
echo "$pharName was created successfully.\n";

99
vendor/deployer/deployer/bin/dep vendored Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env php
<?php
/* (c) Anton Medvedev <anton@medv.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// Check PHP version
if (PHP_VERSION_ID < 80200) {
fwrite(STDERR, "PHP 8.2 or higher is required.\n");
exit(1);
}
// Detect deploy.php location
$deployFile = null;
foreach ($argv as $i => $arg) {
if (preg_match('/^(-f|--file)$/', $arg, $match) && $i + 1 < count($argv)) {
$deployFile = $argv[$i + 1];
break;
}
if (preg_match('/^--file=(?<file>.+)$/', $arg, $match)) {
$deployFile = $match['file'];
break;
}
if (preg_match('/^-f=?(?<file>.+)$/', $arg, $match)) {
$deployFile = $match['file'];
break;
}
}
if (!empty($deployFile)) {
$deployFile = realpath($deployFile);
}
$lookUp = function (string $name): ?string {
$path = getcwd() . '/' . $name;
return is_readable($path) ? $path : null;
};
if (empty($deployFile)) {
$deployFile = $lookUp('deploy.php');
}
if (empty($deployFile)) {
$deployFile = $lookUp('deploy.maml');
}
if (empty($deployFile)) {
$deployFile = $lookUp('deploy.yaml');
}
if (empty($deployFile)) {
$deployFile = $lookUp('deploy.yml');
}
// Detect autoload location
$autoload = [
__DIR__ . '/../vendor/autoload.php', // The dep located at "deployer.phar/bin" or in development.
__DIR__ . '/../../../autoload.php', // The dep located at "vendor/deployer/deployer/bin".
__DIR__ . '/../autoload.php', // The dep located at "vendor/bin".
];
$includes = [
__DIR__ . '/..',
__DIR__ . '/../../../deployer/deployer',
__DIR__ . '/../deployer/deployer',
];
$includePath = false;
for ($i = 0; $i < count($autoload); $i++) {
if (file_exists($autoload[$i]) && is_dir($includes[$i])) {
require $autoload[$i];
$includePath = $includes[$i];
break;
}
}
if (empty($includePath)) {
fwrite(STDERR, "Error: The `autoload.php` file not found in:\n");
for ($i = 0; $i < count($autoload); $i++) {
$a = file_exists($autoload[$i]) ? 'true' : 'false';
$b = is_dir($includes[$i]) ? 'true' : 'false';
fwrite(STDERR, " - file_exists($autoload[$i]) = $a\n");
fwrite(STDERR, " is_dir($includes[$i]) = $b\n");
}
exit(1);
}
// Errors to exception
set_error_handler(function ($severity, $message, $filename, $lineno) {
if (error_reporting() == 0) {
return;
}
if (error_reporting() & $severity) {
throw new ErrorException($message, 0, $severity, $filename, $lineno);
}
});
// Enable recipe loading
set_include_path($includePath . PATH_SEPARATOR . get_include_path());
// Deployer constants
define('DEPLOYER', true);
define('DEPLOYER_BIN', __FILE__);
define('DEPLOYER_DEPLOY_FILE', $deployFile);
Deployer\Deployer::run('master', $deployFile);

73
vendor/deployer/deployer/bin/docgen vendored Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env php
<?php
/* (c) Anton Medvedev <anton@medv.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Deployer;
use Deployer\Documentation\ApiGen;
use Deployer\Documentation\DocGen;
use Deployer\Import\MamlRecipe;
use Maml\Maml;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
require __DIR__ . '/../vendor/autoload.php';
chdir(realpath(__DIR__ . '/..'));
$input = new ArgvInput();
$output = new ConsoleOutput();
$app = new Application('DocGen', '1.0.0');
$app->setDefaultCommand('all');
$api = function () use ($output): int {
$parser = new ApiGen();
$parser->parse(file_get_contents(__DIR__ . '/../src/functions.php'));
$md = $parser->markdown();
file_put_contents(__DIR__ . '/../docs/api.md', $md);
$output->writeln('API Reference documentation updated.');
return 0;
};
$recipes = function () use ($input, $output): int {
$docgen = new DocGen(__DIR__ . '/..');
$docgen->parse(__DIR__ . '/../recipe');
$docgen->parse(__DIR__ . '/../contrib');
if ($input->getOption('json')) {
echo json_encode($docgen->recipes, JSON_PRETTY_PRINT);
return 0;
}
$docgen->gen(__DIR__ . '/../docs');
$output->writeln('Recipes documentation updated.');
return 0;
};
$schema = function () use ($output): int {
$newSchema = [
'$id' => "https://deployer.org/schema.json#",
...Maml::jsonSchema(MamlRecipe::schema()),
];
file_put_contents(__DIR__ . '/../src/schema.json', json_encode($newSchema, JSON_PRETTY_PRINT));
$output->writeln('Schema updated.');
return 0;
};
$app->register('api')->setCode($api);
$app->register('recipes')->setCode($recipes)->addOption('json');
$app->register('schema')->setCode($schema);
$app->register('all')->setCode(function () use ($schema, $recipes, $api): int {
$api();
$recipes();
$schema();
echo shell_exec('git status');
return 0;
})->addOption('json');
$app->run($input, $output);

63
vendor/deployer/deployer/composer.json vendored Normal file
View File

@ -0,0 +1,63 @@
{
"name": "deployer/deployer",
"description": "Deployment Tool",
"license": "MIT",
"homepage": "https://deployer.org",
"support": {
"docs": "https://deployer.org/docs",
"source": "https://github.com/deployphp/deployer",
"issues": "https://github.com/deployphp/deployer/issues"
},
"authors": [
{
"name": "Anton Medvedev",
"email": "anton@medv.io"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/antonmedv"
}
],
"autoload": {
"psr-4": {
"Deployer\\": "src/"
},
"files": [
"src/functions.php",
"src/Support/helpers.php"
]
},
"scripts": {
"test": "phpunit",
"check": "php-cs-fixer check",
"fix": "php-cs-fixer fix",
"docgen": "php bin/docgen",
"phpstan": "phpstan analyse -c phpstan.neon --memory-limit 1G",
"phpstan:baseline": "@phpstan --generate-baseline tests/phpstan-baseline.neon"
},
"bin": [
"bin/dep"
],
"require": {
"php": "^8.3",
"maml/maml": "^3.2",
"symfony/console": "^7.4.0 || ^8.0.0",
"symfony/process": "^7.4.0 || ^8.0.0",
"symfony/yaml": "^7.4.0 || ^8.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.68",
"phpstan/phpstan": "^2.1",
"phpunit/php-code-coverage": "^12.0",
"phpunit/phpunit": "^12.5"
},
"config": {
"sort-packages": true,
"process-timeout": 0,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
## Configuration
- *bugsnag_api_key* – the API Key associated with the project. Informs Bugsnag which project has been deployed. This is the only required field.
- *bugsnag_provider* – the name of your source control provider. Required when repository is supplied and only for on-premise services.
- *bugsnag_app_version* – the app version of the code you are currently deploying. Only set this if you tag your releases with semantic version numbers and deploy infrequently. (Optional.)
## Usage
Since you should only notify Bugsnag of a successful deployment, the `bugsnag:notify` task should be executed right at the end.
```php
after('deploy', 'bugsnag:notify');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
desc('Notifies Bugsnag of deployment');
task('bugsnag:notify', function () {
$data = [
'apiKey' => get('bugsnag_api_key'),
'releaseStage' => get('target'),
'repository' => get('repository'),
'provider' => get('bugsnag_provider', ''),
'branch' => get('branch'),
'revision' => runLocally('git log -n 1 --format="%h"'),
'appVersion' => get('bugsnag_app_version', ''),
];
Httpie::post('https://notify.bugsnag.com/deploy')
->jsonBody($data)
->send();
});

View File

@ -0,0 +1,116 @@
<?php
/*
## Configuration
- **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file.
```php
set('cachetool', '/var/run/php-fpm.sock');
// or
set('cachetool', '127.0.0.1:9000');
// or
set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']);
```
You can also specify different cachetool settings for each host:
```php
host('staging')
->set('cachetool', '127.0.0.1:9000');
host('production')
->set('cachetool', '/var/run/php-fpm.sock');
```
By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting.
If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use
the web adapter that creates a temporary php file and makes a web request to it with a configuration like
```php
set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}');
```
## Usage
Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release:
```php
after('deploy:symlink', 'cachetool:clear:opcache');
// or
after('deploy:symlink', 'cachetool:clear:apcu');
```
## Read more
Read more information about cachetool on the website:
http://gordalina.github.io/cachetool/
*/
namespace Deployer;
set('cachetool', '');
/**
* URL to download cachetool from if it is not available
*
* CacheTool 9.x works with PHP >=8.1
* CacheTool 8.x works with PHP >=8.0
* CacheTool 7.x works with PHP >=7.3
*/
set('cachetool_url', 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar');
set('cachetool_args', '');
set('bin/cachetool', function () {
if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) {
run("cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}");
}
return '{{release_or_current_path}}/cachetool.phar';
});
set('cachetool_options', function () {
$options = (array) get('cachetool');
$fullOptions = (string) get('cachetool_args');
$return = [];
if ($fullOptions !== '') {
$return = [$fullOptions];
} elseif (count($options) > 0) {
foreach ($options as $option) {
if (is_string($option) && $option !== '') {
$return[] = "--fcgi={$option}";
}
}
}
return $return ?: [''];
});
/**
* Clear opcache cache
*/
desc('Clears OPcode cache');
task('cachetool:clear:opcache', function () {
$options = get('cachetool_options');
foreach ($options as $option) {
run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} opcache:reset $option");
}
});
/**
* Clear APCu cache
*/
desc('Clears APCu system cache');
task('cachetool:clear:apcu', function () {
$options = get('cachetool_options');
foreach ($options as $option) {
run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} apcu:cache:clear $option");
}
});
/**
* Clear file status cache, including the realpath cache
*/
desc('Clears file status and realpath caches');
task('cachetool:clear:stat', function () {
$options = get('cachetool_options');
foreach ($options as $option) {
run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} stat:clear $option");
}
});

View File

@ -0,0 +1,173 @@
<?php
/*
# Chatwork Recipe
## Installing
1. Create chatwork account by any manual in the internet
2. Take chatwork token (Like: b29a700e2d15bef3f26ae6a5c142d1ea) and set `chatwork_token` parameter
3. Take chatwork room id from url after clicked on the room, and set `chatwork_room_id` parameter
4. If you want, you can edit `chatwork_notify_text`, `chatwork_success_text` or `chatwork_failure_text`
5. Require chatwork recipe in your `deploy.php` file
```php
# https://deployer.org/recipes.html
require 'recipe/chatwork.php';
```
Add hook on deploy:
```php
before('deploy', 'chatwork:notify');
```
## Configuration
- `chatwork_token` – chatwork bot token, **required**
- `chatwork_room_id` — chatwork room to push messages to **required**
- `chatwork_notify_text` – notification message template
```
[info]
[title](*) Deployment Status: Deploying[/title]
Repo: {{repository}}
Branch: {{branch}}
Server: {{hostname}}
Release Path: {{release_path}}
Current Path: {{current_path}}
[/info]
```
- `chatwork_success_text` – success template, default:
```
[info]
[title](*) Deployment Status: Successfully[/title]
Repo: {{repository}}
Branch: {{branch}}
Server: {{hostname}}
Release Path: {{release_path}}
Current Path: {{current_path}}
[/info]"
```
- `chatwork_failure_text` – failure template, default:
```
[info]
[title](*) Deployment Status: Failed[/title]
Repo: {{repository}}
Branch: {{branch}}
Server: {{hostname}}
Release Path: {{release_path}}
Current Path: {{current_path}}
[/info]"
```
## Tasks
- `chatwork:notify` – send message to chatwork
- `chatwork:notify:success` – send success message to chatwork
- `chatwork:notify:failure` – send failure message to chatwork
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'chatwork:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'chatwork:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'chatwork:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Chatwork settings
set('chatwork_token', function () {
throw new \RuntimeException('Please configure "chatwork_token" parameter.');
});
set('chatwork_room_id', function () {
throw new \RuntimeException('Please configure "chatwork_room_id" parameter.');
});
set('chatwork_api', function () {
return 'https://api.chatwork.com/v2/rooms/' . get('chatwork_room_id') . '/messages';
});
// The Messages
set('chatwork_notify_text', "[info]\n[title](*) Deployment Status: Deploying[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]");
set('chatwork_success_text', "[info]\n[title](*) Deployment Status: Successfully[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]");
set('chatwork_failure_text', "[info]\n[title](*) Deployment Status: Failed[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]");
// Helpers
task('chatwork_send_message', function () {
Httpie::post(get('chatwork_api'))
->formBody(['body' => get('chatwork_message')])
->header("X-ChatWorkToken", get('chatwork_token'))
->send();
});
// Tasks
desc('Tests messages');
task('chatwork:test', function () {
set('chatwork_message', get('chatwork_notify_text'));
invoke('chatwork_send_message');
set('chatwork_message', get('chatwork_success_text'));
invoke('chatwork_send_message');
set('chatwork_message', get('chatwork_failure_text'));
invoke('chatwork_send_message');
})
->once();
desc('Notifies Chatwork');
task('chatwork:notify', function () {
if (!get('chatwork_token', false)) {
return;
}
if (!get('chatwork_room_id', false)) {
return;
}
set('chatwork_message', get('chatwork_notify_text'));
invoke('chatwork_send_message');
})
->once()
->hidden();
desc('Notifies Chatwork about deploy finish');
task('chatwork:notify:success', function () {
if (!get('chatwork_token', false)) {
return;
}
if (!get('chatwork_room_id', false)) {
return;
}
set('chatwork_message', get('chatwork_success_text'));
invoke('chatwork_send_message');
})
->once()
->hidden();
desc('Notifies Chatwork about deploy failure');
task('chatwork:notify:failure', function () {
if (!get('chatwork_token', false)) {
return;
}
if (!get('chatwork_room_id', false)) {
return;
}
set('chatwork_message', get('chatwork_failure_text'));
invoke('chatwork_send_message');
})
->once()
->hidden();

View File

@ -0,0 +1,166 @@
<?php
/*
Monitor your deployments on [CIMonitor](https://github.com/CIMonitor/CIMonitor).
![CIMonitorGif](https://www.steefmin.xyz/deployer-example.gif)
Add tasks on deploy:
```php
before('deploy', 'cimonitor:notify');
after('deploy:success', 'cimonitor:notify:success');
after('deploy:failed', 'cimonitor:notify:failure');
```
## Configuration
- `cimonitor_webhook` – CIMonitor server webhook url, **required**
```
set('cimonitor_webhook', 'https://cimonitor.enrise.com/webhook/deployer');
```
- `cimonitor_title` – the title of application, default the username\reponame combination from `{{repository}}`
```
set('cimonitor_title', '');
```
- `cimonitor_user` – User object with name and email, default gets information from `git config`
```
set('cimonitor_user', function () {
return [
'name' => 'John Doe',
'email' => 'john@enrise.com',
];
});
```
Various cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states.
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'cimonitor:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'cimonitor:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'cimonitor:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Title of project based on git repo
set('cimonitor_title', function () {
$repo = get('repository');
$pattern = '/\w+\/\w+/';
return preg_match($pattern, $repo, $titles) ? $titles[0] : $repo;
});
set('cimonitor_user', function () {
return [
'name' => runLocally('git config --get user.name'),
'email' => runLocally('git config --get user.email'),
];
});
// CI monitor status states and job states
set('cimonitor_status_info', 'info');
set('cimonitor_status_warning', 'warning');
set('cimonitor_status_error', 'error');
set('cimonitor_status_success', 'success');
set('cimonitor_job_state_info', get('cimonitor_status_info'));
set('cimonitor_job_state_pending', 'pending');
set('cimonitor_job_state_running', 'running');
set('cimonitor_job_state_warning', get('cimonitor_status_warning'));
set('cimonitor_job_state_error', get('cimonitor_status_error'));
set('cimonitor_job_state_success', get('cimonitor_status_success'));
desc('Notifies CIMonitor');
task('cimonitor:notify', function () {
if (!get('cimonitor_webhook', false)) {
return;
}
$body = [
'state' => get('cimonitor_status_warning'),
'branch' => get('branch'),
'title' => get('cimonitor_title'),
'user' => get('cimonitor_user'),
'stages' => [get('stage', '')],
'jobs' => [
[
'name' => 'Deploying...',
'stage' => '',
'state' => get('cimonitor_job_state_running'),
],
],
];
Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();
})
->once()
->hidden();
desc('Notifies CIMonitor about deploy finish');
task('cimonitor:notify:success', function () {
if (!get('cimonitor_webhook', false)) {
return;
}
$depstage = 'Deployed to ' . get('stage', '');
$body = [
'state' => get('cimonitor_status_success'),
'branch' => get('branch'),
'title' => get('cimonitor_title'),
'user' => get('cimonitor_user'),
'stages' => [$depstage],
'jobs' => [
[
'name' => 'Deploy',
'stage' => $depstage,
'state' => get('cimonitor_job_state_success'),
],
],
];
Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();
})
->once()
->hidden();
desc('Notifies CIMonitor about deploy failure');
task('cimonitor:notify:failure', function () {
if (!get('cimonitor_webhook', false)) {
return;
}
$body = [
'state' => get('cimonitor_status_error'),
'branch' => get('branch'),
'title' => get('cimonitor_title'),
'user' => get('cimonitor_user'),
'stages' => [get('stage', '')],
'jobs' => [
[
'name' => 'Deploy',
'stage' => '',
'state' => get('cimonitor_job_state_error'),
],
],
];
Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();
})
->once()
->hidden();

View File

@ -0,0 +1,102 @@
<?php
/*
### Configuration
- `cloudflare` – array with configuration for cloudflare
- `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email.
- `api_key` – Cloudflare API key generated on the "My Account" page.
- `email` – Cloudflare Email address associated with your account.
- `api_token` – Cloudflare API Token generated on the "My Account" page.
- `domain` – The domain you want to clear (optional if zone_id is provided).
- `zone_id` – Cloudflare Zone ID (optional).
### Usage
Since the website should be built and some load is likely about to be applied to your server, this should be one of,
if not the, last tasks before cleanup
*/
namespace Deployer;
desc('Clears Cloudflare Cache');
task('deploy:cloudflare', function () {
$config = get('cloudflare', []);
// validate config and set headers
if (!empty($config['service_key'])) {
$headers = [
'X-Auth-User-Service-Key' => $config['service_key'],
];
} elseif (!empty($config['email']) && !empty($config['api_key'])) {
$headers = [
'X-Auth-Key' => $config['api_key'],
'X-Auth-Email' => $config['email'],
];
} elseif (!empty($config['api_token'])) {
$headers = [
'Authorization' => 'Bearer ' . $config['api_token'],
];
} else {
throw new \RuntimeException("Set a service key or email / api key");
}
$headers['Content-Type'] = 'application/json';
$makeRequest = function ($url, $opts = []) use ($headers) {
$ch = curl_init("https://api.cloudflare.com/client/v4/$url");
$parsedHeaders = [];
foreach ($headers as $key => $value) {
$parsedHeaders[] = "$key: $value";
}
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $parsedHeaders,
CURLOPT_RETURNTRANSFER => true,
]);
curl_setopt_array($ch, $opts);
$res = curl_exec($ch);
if (curl_errno($ch)) {
throw new \RuntimeException("Error making curl request (result: $res)");
}
return $res;
};
$zoneId = $config['zone_id'];
if (empty($zoneId)) {
if (empty($config['domain'])) {
throw new \RuntimeException("Set a domain");
}
// get the mysterious zone id from Cloud Flare
$zones = json_decode($makeRequest(
"zones?name={$config['domain']}",
), true);
if (!empty($zones['errors'])) {
throw new \RuntimeException('Problem with zone data');
} else {
$zoneId = current($zones['result'])['id'];
}
}
// make purge request
$makeRequest(
"zones/$zoneId/purge_cache",
[
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_POSTFIELDS => json_encode(
[
'purge_everything' => true,
],
),
],
);
});

View File

@ -0,0 +1,269 @@
<?php
/*
### Description
This is a recipe that uses the [cPanel 2 API](https://documentation.cPanel.net/display/DD/Guide+to+cPanel+API+2).
Unfortunately the [UAPI](https://documentation.cPanel.net/display/DD/Guide+to+UAPI) that is recommended does not have support for creating addon domains.
The main idea behind is for staging purposes but I guess you can use it for other interesting concepts.
The idea is, every branch possibly has its own staging domain/subdomain (staging-neat-feature.project.com) and database db_neat-feature_project so it can be tested.
This recipe can make the domain/subdomain and database creation part of the deployment process so you don't have to manually create them through an interface.
### Configuration
The example uses a .env file and Dotenv for configuration, but you can set the parameters as you wish
```
set('cpanel', [
'host' => getenv('CPANEL_HOST'),
'port' => getenv('CPANEL_PORT'),
'username' => getenv('CPANEL_USERNAME'),
'auth_type' => getenv('CPANEL_AUTH_TYPE'),
'token' => getenv('CPANEL_TOKEN'),
'user' => getenv('CPANEL_USER'),
'db_user' => getenv('CPANEL_DB_USER'),
'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),
'timeout' => 500,
'allowInStage' => ['staging', 'beta', 'alpha'],
'create_domain_format' => '%s-%s-%s',
'create_domain_values' => ['staging', 'master', get('application')],
'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',
'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),
'create_db_format' => '%s_%s-%s-%s',
'create_db_values' => ['apps', 'staging','master', get('application')],
]);
```
- `cpanel` – array with configuration for cPanel
- `username` – WHM account
- `user` – cPanel account that you want in charge of the domain
- `token` – WHM API token
- `create_domain_format` – Format for name creation of domain
- `create_domain_values` – The actual value reference for naming
- `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name)
- `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes
- `addondir` – addon dir is different from the deploy path because cPanel "injects" /home/user/ into the path, so tilde cannot be used
- `allowInStage` – Define the stages that cPanel recipe actions are allowed in
#### .env file example
```
CPANEL_HOST=xxx.xxx.xxx.xxx
CPANEL_PORT=2087
CPANEL_USERNAME=root
CPANEL_TOKEN=xxxx
CPANEL_USER=xxx
CPANEL_AUTH_TYPE=hash
CPANEL_DB_USER=db_user
CPANEL_DB_PRIVILEGES="ALL PRIVILEGES"
SUDOMAIN_SUFFIX=.mymaindomain.com
```
### Tasks
- `cpanel:createaddondomain` Creates an addon domain
- `cpanel:deleteaddondomain` Removes an addon domain
- `cpanel:createdb` Creates a new database
### Usage
A complete example with configs, staging and deployment
```
<?php
namespace Deployer;
use Dotenv\Dotenv;
require 'vendor/autoload.php';
(Dotenv::create(__DIR__))->load(); // this is used just so an .env file can be used for credentials
require 'cpanel.php';
// Project name
set('application', 'myproject.com');
// Project repository
set('repository', 'git@github.com:myorg/myproject.com');
set('cpanel', [
'host' => getenv('CPANEL_HOST'),
'port' => getenv('CPANEL_PORT'),
'username' => getenv('CPANEL_USERNAME'),
'auth_type' => getenv('CPANEL_AUTH_TYPE'),
'token' => getenv('CPANEL_TOKEN'),
'user' => getenv('CPANEL_USER'),
'db_user' => getenv('CPANEL_DB_USER'),
'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),
'timeout' => 500,
'allowInStage' => ['staging', 'beta', 'alpha'],
'create_domain_format' => '%s-%s-%s',
'create_domain_values' => ['staging', 'master', get('application')],
'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',
'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),
'create_db_format' => '%s_%s-%s-%s',
'create_db_values' => ['apps', 'staging','master', get('application')],
]);
host('myproject.com')
->stage('staging')
->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values']))
->set('branch', 'dev-branch')
->set('deploy_path', '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']))
->set('addondir', 'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']));
// Tasks
task('build', function () {
run('cd {{release_path}} && build');
});
after('deploy:prepare', 'cpanel:createaddondomain');
after('deploy:prepare', 'cpanel:createdb');
```
*/
namespace Deployer;
use Deployer\Task\Context;
use Gufy\CpanelPhp\Cpanel;
/**
* @return Cpanel
* @throws Exception\Exception
*/
function getCpanel()
{
$config = get('cpanel', []);
$allowInStage = $config['allowInStage'];
$stage = input()->getArgument('stage');
if (!class_exists('\Gufy\CpanelPhp\Cpanel')) {
throw new \RuntimeException("<comment>Please install php package</comment> <info>gufy/cpanel-php</info> <comment>to use CPanel API</comment>");
}
if (!in_array($stage, $allowInStage)) {
throw new \RuntimeException(sprintf("Since it creates addon domains and databases, CPanel recipe is available only in the %s environments", implode($allowInStage)));
}
if (!is_array($config)
|| !isset($config['host'])
|| !isset($config['port'])
|| !isset($config['username'])
|| !isset($config['token'])
|| !isset($config['user'])) {
throw new \RuntimeException("<comment>Please configure CPanel config:</comment> <info>set('cpanel', array('host' => 'xxx.xxx.xxx.xxx:', 'port' => 2087 , 'username' => 'root', 'token' => 'asdfasdf', 'cpaneluser' => 'guy'));</info>");
}
$cpanel = new Cpanel([
'host' => 'https://' . $config['host'] . ':' . $config['port'],
'username' => $config['username'],
'auth_type' => $config['auth_type'],
'password' => $config['token'],
]);
$cpanel->setTimeout($config['timeout']);
return $cpanel;
}
function getDomainInfo()
{
$domain = vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']);
$cleanDomain = str_replace(['.', ',', ' ', '/', '-'], '', $domain);
$subDomain = get('cpanel')['subdomain_prefix'] . $cleanDomain;
return [
'domain' => $domain,
'subDomain' => $subDomain,
'subDomainWithSuffix' => $subDomain . get('cpanel')['subdomain_suffix'],
];
}
desc('Creates database though CPanel API');
task('cpanel:createdb', function () {
$cpanel = getCPanel();
$config = get('cpanel', []);
if (!askConfirmation(sprintf('This will try to create the database %s on the host though CPanel API, ok?', get('cpanel_createdb')), true)) {
return;
}
$createDbDataResult = $cpanel->cpanel('MysqlFE', 'createdb', $config['user'], ['db' => get('cpanel_createdb')]);
$addPrivilegesDataResult = $cpanel->cpanel('MysqlFE', 'setdbuserprivileges', $config['user'], ['privileges' => $config['db_user_privileges'], 'db' => get('cpanel_createdb'), 'dbuser' => $config['db_user']]);
$createDbData = json_decode($createDbDataResult, true);
$addPrivilegesData = json_decode($addPrivilegesDataResult, true);
if (isset($createDbData['cpanelresult']['error'])) {
writeln($createDbData['cpanelresult']['error']);
} else {
writeln('Successfully created database!');
}
if (isset($addPrivilegesData['cpanelresult']['error'])) {
writeln($addPrivilegesData['cpanelresult']['error']);
} else {
writeln('Successfully added privileges to database!');
}
});
desc('Creates addon domain though CPanel API');
task('cpanel:createaddondomain', function () {
$cpanel = getCPanel();
$config = get('cpanel', []);
$domain = getDomainInfo()['domain'];
$subDomain = getDomainInfo()['subDomain'];
if (!askConfirmation(sprintf('This will try to create the addon domain %s and point it to %s and subdomain %s, ok?', $domain, get('addondir'), $subDomain), true)) {
return;
}
writeln(sprintf('Creates addon domain %s and pointing it to %s', $domain, get('addondir')));
$addAddonDomainResult = $cpanel->cpanel('AddonDomain', 'addaddondomain', $config['user'], ['dir' => get('addondir'), 'newdomain' => $domain, 'subdomain' => $subDomain]);
$addAddonDomainData = json_decode($addAddonDomainResult, true);
if (isset($addAddonDomainResult['cpanelresult']['error'])) {
writeln($addAddonDomainData['cpanelresult']['error']);
} else {
writeln('Successfully created addon domain!');
writeln($addAddonDomainData['cpanelresult']['data'][0]['reason']);
}
});
desc('Deletes addon domain though CPanel API');
task('cpanel:deleteaddondomain', function () {
$cpanel = getCPanel();
$config = get('cpanel', []);
$domain = getDomainInfo()['domain'];
$subDomain = getDomainInfo()['subDomain'];
$subDomainWithSuffix = getDomainInfo()['subDomainWithSuffix'];
if (!askConfirmation(sprintf('This will delete the addon domain %s with corresponding subdomain %s, ok?', $domain, $subDomain), true)) {
return;
}
writeln(sprintf('Deleting addon domain %s', $domain));
$delAddonDomainResult = $cpanel->cpanel('AddonDomain', 'deladdondomain', $config['user'], ['domain' => $domain, 'subdomain' => $subDomainWithSuffix]);
$delAddonDomainResult = json_decode($delAddonDomainResult, true);
if (isset($delAddonDomainResult['cpanelresult']['error'])) {
writeln($delAddonDomainResult['cpanelresult']['error']);
} else {
writeln('Successfully deleted addon domain!');
writeln($delAddonDomainResult['cpanelresult']['data'][0]['reason']);
}
});

View File

@ -0,0 +1,154 @@
<?php
/*
Recipe for adding crontab jobs.
This recipe creates a new section in the crontab file with the configured jobs.
The section is identified by the *crontab:identifier* variable, by default the application name.
## Configuration
- *crontab:jobs* - An array of strings with crontab lines.
## Usage
```php
require 'contrib/crontab.php';
after('deploy:success', 'crontab:sync');
add('crontab:jobs', [
'* * * * * cd {{current_path}} && {{bin/php}} artisan schedule:run >> /dev/null 2>&1',
]);
```
*/
namespace Deployer;
// Get path to bin
set('bin/crontab', function () {
return which('crontab');
});
// Set the identifier used in the crontab, application name by default
set('crontab:identifier', function () {
return get('application', 'application');
});
// Use sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user.
set('crontab:use_sudo', false);
desc('Sync crontab jobs');
task('crontab:sync', function () {
$cronJobsLocal = array_map(
fn($job) => parse($job),
get('crontab:jobs', []),
);
if (count($cronJobsLocal) == 0) {
writeln("Nothing to sync - configure crontab:jobs");
return;
}
$cronJobs = getRemoteCrontab();
$identifier = get('crontab:identifier');
$sectionStart = "###< $identifier";
$sectionEnd = "###> $identifier";
// find our cronjob section
$start = array_search($sectionStart, $cronJobs);
$end = array_search($sectionEnd, $cronJobs);
if ($start === false || $end === false) {
// Move the duplicates over when first generating the section
foreach ($cronJobs as $index => $cronJob) {
if (in_array($cronJob, $cronJobsLocal)) {
unset($cronJobs[$index]);
writeln("Crontab: Found existing job in crontab, moving it to the section");
}
}
// Create the section
$cronJobs[] = $sectionStart;
$cronJobs = [...$cronJobs, ...$cronJobsLocal];
$cronJobs[] = $sectionEnd;
writeln("Crontab: Found no section, created the section with configured jobs");
} else {
// Replace the existing section
array_splice($cronJobs, $start + 1, $end - $start - 1, $cronJobsLocal);
writeln("Crontab: Found existing section, replaced with configured jobs");
}
setRemoteCrontab($cronJobs);
});
desc('Remove crontab jobs');
task('crontab:remove', function () {
$cronJobsLocal = array_map(
fn($job) => parse($job),
get('crontab:jobs', []),
);
$cronJobs = getRemoteCrontab();
$identifier = get('crontab:identifier');
$sectionStart = "###< $identifier";
$sectionEnd = "###> $identifier";
// Find our cronjob section
$start = array_search($sectionStart, $cronJobs);
$end = array_search($sectionEnd, $cronJobs);
if ($start && $end) {
// Remove the existing section
array_splice($cronJobs, $start + 1, $end - $start - 1);
writeln("Crontab: Found existing section, removed jobs");
} elseif (count($cronJobsLocal) > 0) {
$foundJobs = false;
// Remove individual jobs if no section is present
foreach ($cronJobs as $index => $cronJob) {
if (in_array($cronJob, $cronJobsLocal)) {
unset($cronJobs[$index]);
$foundJobs = true;
}
}
if ($foundJobs) {
writeln("Crontab: Found existing jobs in crontab, removed jobs");
} else {
writeln("Crontab: No existing jobs in crontab, skipping");
return;
}
} else {
writeln("Crontab: Found no section and crontab:jobs is not configured, skipping");
return;
}
setRemoteCrontab($cronJobs);
});
function setRemoteCrontab(array $lines): void
{
$sudo = get('crontab:use_sudo') ? 'sudo' : '';
$tmpCrontabPath = sprintf('/tmp/%s', uniqid('crontab_save_'));
if (test("[ -f '$tmpCrontabPath' ]")) {
run("unlink '$tmpCrontabPath'");
}
foreach ($lines as $line) {
run("echo " . quote($line) . " >> $tmpCrontabPath");
}
run("$sudo {{bin/crontab}} " . $tmpCrontabPath);
run('unlink ' . $tmpCrontabPath);
}
function getRemoteCrontab(): array
{
$sudo = get('crontab:use_sudo') ? 'sudo' : '';
if (!test("$sudo {{bin/crontab}} -l >> /dev/null 2>&1")) {
return [];
}
return explode(PHP_EOL, run("$sudo {{bin/crontab}} -l"));
}

View File

@ -0,0 +1,169 @@
<?php
/*
### Configuration
- `directadmin` – array with configuration for DirectAdmin
- `host` – DirectAdmin host
- `port` – DirectAdmin port (default: 2222, not required)
- `scheme` – DirectAdmin scheme (default: http, not required)
- `username` – DirectAdmin username
- `password` – DirectAdmin password (it is recommended to use login keys!)
- `db_user` – Database username (required when using directadmin:createdb or directadmin:deletedb)
- `db_name` – Database namse (required when using directadmin:createdb)
- `db_password` – Database password (required when using directadmin:createdb)
- `domain_name` – Domain to create, delete or edit (required when using directadmin:createdomain, directadmin:deletedomain, directadmin:symlink-private-html or directadmin:php-version)
- `domain_ssl` – Enable SSL, options: ON/OFF, default: ON (optional when using directadmin:createdb)
- `domain_cgi` – Enable CGI, options: ON/OFF, default: ON (optional when using directadmin:createdb)
- `domain_php` – Enable PHP, options: ON/OFF, default: ON (optional when using directadmin:createdb)
- `domain_php_version` – Domain PHP Version, default: 1 (required when using directadmin:php-version)
*/
namespace Deployer;
use Deployer\Task\Context;
use Deployer\Utility\Httpie;
/**
* getDirectAdminConfig
*
* @return array
*/
function getDirectAdminConfig()
{
$config = get('directadmin', []);
if (!is_array($config)
|| !isset($config['host'])
|| !isset($config['username'])
|| !isset($config['password'])) {
throw new \RuntimeException("Please set the following DirectAdmin config:" . PHP_EOL . "set('directadmin', ['host' => '127.0.0.1', 'port' => 2222, 'username' => 'admin', 'password' => 'password']);");
}
return $config;
}
/**
* DirectAdmin
*
* @param string $action
* @param array $data
*
* @return void
*/
function DirectAdmin(string $action, array $data = [])
{
$config = getDirectAdminConfig();
$scheme = $config['scheme'] ?? 'http';
$port = $config['port'] ?? 2222;
$result = Httpie::post(sprintf('%s://%s:%s/%s', $scheme, $config['host'], $port, $action))
->formBody($data)
->setopt(CURLOPT_USERPWD, $config['username'] . ':' . $config['password'])
->send();
parse_str($result, $resultData);
if ($resultData['error'] === '1') {
$resultData['details'] = trim($resultData['details']);
$resultData['details'] = str_replace(['\\n', '\\r'], '', $resultData['details']);
$resultData['details'] = strip_tags($resultData['details']);
writeln('<error>DirectAdmin message: ' . $resultData['details'] . '</error>');
}
}
desc('Creates a database on DirectAdmin');
task('directadmin:createdb', function () {
$config = getDirectAdminConfig();
if (!isset($config['db_name'])
|| !isset($config['db_user'])
|| !isset($config['db_password'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_name' => 'test', 'db_user' => 'test', 'db_password' => '123456']);");
}
DirectAdmin('CMD_API_DATABASES', [
'action' => 'create',
'name' => $config['db_name'],
'user' => $config['db_user'],
'passwd' => $config['db_password'],
'passwd2' => $config['db_password'],
]);
});
desc('Deletes a database on DirectAdmin');
task('directadmin:deletedb', function () {
$config = getDirectAdminConfig();
if (!isset($config['db_user'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_user' => 'test_database']);");
}
DirectAdmin('CMD_API_DATABASES', [
'action' => 'delete',
'select0' => $config['username'] . '_' . $config['db_user'],
]);
});
desc('Creates a domain on DirectAdmin');
task('directadmin:createdomain', function () {
$config = getDirectAdminConfig();
if (!isset($config['domain_name'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);");
}
DirectAdmin('CMD_API_DOMAIN', [
'action' => 'create',
'domain' => $config['domain_name'],
'ssl' => $config['domain_ssl'] ?? 'On',
'cgi' => $config['domain_cgi'] ?? 'ON',
'php' => $config['domain_php'] ?? 'ON',
]);
});
desc('Deletes a domain on DirectAdmin');
task('directadmin:deletedomain', function () {
$config = getDirectAdminConfig();
if (!isset($config['domain_name'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);");
}
DirectAdmin('CMD_API_DOMAIN', [
'delete' => 'anything',
'confirmed' => 'anything',
'select0' => $config['domain_name'],
]);
});
desc('Symlink your private_html to public_html');
task('directadmin:symlink-private-html', function () {
$config = getDirectAdminConfig();
if (!isset($config['domain_name'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);");
}
DirectAdmin('CMD_API_DOMAIN', [
'action' => 'private_html',
'domain' => $config['domain_name'],
'val' => 'symlink',
]);
});
desc('Changes the PHP version from a domain');
task('directadmin:php-version', function () {
$config = getDirectAdminConfig();
if (!isset($config['domain_name'])
|| !isset($config['domain_php_version'])) {
throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com', 'domain_php_version' => 1]);");
}
DirectAdmin('CMD_API_DOMAIN', [
'action' => 'php_selector',
'domain' => $config['domain_name'],
'php1_select' => $config['domain_php_version'],
]);
});

View File

@ -0,0 +1,119 @@
<?php
/*
## Installing
Add hook on deploy:
```php
before('deploy', 'discord:notify');
```
## Configuration
- `discord_channel` – Discord channel ID, **required**
- `discord_token` – Discord channel token, **required**
- `discord_notify_text` – notification message template, markdown supported, default:
```markdown
:information_source: **{{user}}** is deploying branch `{{branch}}` to _{{where}}_
```
- `discord_success_text` – success template, default:
```markdown
:white_check_mark: Branch `{{branch}}` deployed to _{{where}}_ successfully
```
- `discord_failure_text` – failure template, default:
```markdown
:no_entry_sign: Branch `{{branch}}` has failed to deploy to _{{where}}_
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'discord:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'discord:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'discord:notify:failure');
```
*/
namespace Deployer;
use Deployer\Task\Context;
use Deployer\Utility\Httpie;
set('discord_webhook', function () {
return 'https://discordapp.com/api/webhooks/{{discord_channel}}/{{discord_token}}/slack';
});
// Deploy messages
set('discord_notify_text', function () {
return [
'text' => parse(':information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'),
];
});
set('discord_success_text', function () {
return [
'text' => parse(':white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'),
];
});
set('discord_failure_text', function () {
return [
'text' => parse(':no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'),
];
});
// The message
set('discord_message', 'discord_notify_text');
// Helpers
task('discord_send_message', function () {
$message = get(get('discord_message'));
Httpie::post(get('discord_webhook'))->jsonBody($message)->send();
});
// Tasks
desc('Tests messages');
task('discord:test', function () {
set('discord_message', 'discord_notify_text');
invoke('discord_send_message');
set('discord_message', 'discord_success_text');
invoke('discord_send_message');
set('discord_message', 'discord_failure_text');
invoke('discord_send_message');
})
->once();
desc('Notifies Discord');
task('discord:notify', function () {
set('discord_message', 'discord_notify_text');
invoke('discord_send_message');
})
->once()
->isHidden();
desc('Notifies Discord about deploy finish');
task('discord:notify:success', function () {
set('discord_message', 'discord_success_text');
invoke('discord_send_message');
})
->once()
->isHidden();
desc('Notifies Discord about deploy failure');
task('discord:notify:failure', function () {
set('discord_message', 'discord_failure_text');
invoke('discord_send_message');
})
->once()
->isHidden();

View File

@ -0,0 +1,66 @@
<?php
/*
## Configuration options
- **url** *(required)*: the URL to the creates annotation api endpoint.
- **token** *(required)*: authentication token. Can be created at Grafana Console.
- **time** *(optional)* – set deploy time of annotation. specify epoch milliseconds. (Defaults is set to the current time in epoch milliseconds.)
- **tags** *(optional)* – set tag of annotation.
- **text** *(optional)* – set text of annotation. (Defaults is set to "Deployed " + git log -n 1 --format="%h")
```php
// deploy.php
set('grafana', [
'token' => 'eyJrIj...',
'url' => 'http://grafana/api/annotations',
'tags' => ['deploy', 'production'],
]);
```
## Usage
If you want to create annotation about successful end of deployment.
```php
after('deploy:success', 'grafana:annotation');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
desc('Creates Grafana annotation of deployment');
task('grafana:annotation', function () {
$defaultConfig = [
'url' => null,
'token' => null,
'time' => round(microtime(true) * 1000),
'tags' => [],
'text' => null,
];
$config = array_merge($defaultConfig, (array) get('grafana'));
if (!isset($config['url']) || !isset($config['token'])) {
throw new \RuntimeException("Please configure Grafana: set('grafana', ['url' => 'https://localhost/api/annotations', token' => 'eyJrIjo...']);");
}
$params = [
'time' => $config['time'],
'isRegion' => false,
'tags' => $config['tags'],
'text' => $config['text'],
];
if (!isset($params['text'])) {
$params['text'] = 'Deployed ' . trim(runLocally('git log -n 1 --format="%h"'));
}
Httpie::post($config['url'])
->header('Authorization', 'Bearer ' . $config['token'])
->jsonBody($params)
->send();
});

View File

@ -0,0 +1,184 @@
<?php
/*
Add hook on deploy:
```php
before('deploy', 'chat:notify');
```
## Configuration
- `chat_webhook` – chat incoming webhook url, **required**
- `chat_title` – the title of your notification card, default `{{application}}`
- `chat_subtitle` – the subtitle of your card, default `{{hostname}}`
- `chat_favicon` – an image for the header of your card, default `http://{{hostname}}/favicon.png`
- `chat_line1` – first line of the text in your card, default: `{{branch}}`
- `chat_line2` – second line of the text in your card, default: `{{stage}}`
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'chat:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'chat:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'chat:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Title of project
set('chat_title', function () {
return get('application', 'Project');
});
set('chat_subtitle', get('hostname'));
// If 'favicon' is set Google Hangouts Chat will decorate your card with an image.
set('favicon', 'http://{{hostname}}/favicon.png');
// Deploy messages
set('chat_line1', '{{branch}}');
set('chat_line2', '{{stage}}');
desc('Notifies Google Hangouts Chat');
task('chat:notify', function () {
if (!get('chat_webhook', false)) {
return;
}
$card = [
'header' => [
'title' => get('chat_title'),
'subtitle' => get('chat_subtitle'),
'imageUrl' => get('favicon'),
'imageStyle' => 'IMAGE',
],
'sections' => [
'widgets' => [
'keyValue' => [
'topLabel' => get('chat_line1'),
'content' => get('chat_line2'),
'contentMultiline' => false,
'bottomLabel' => 'started',
// Use 'iconUrl' to set a custom icon URL (png)
'icon' => 'CLOCK',
'button' => [
'textButton' => [
'text' => 'Visit site',
'onClick' => [
'openLink' => [
'url' => get('hostname'),
],
],
],
],
],
],
],
];
Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();
})
->once()
->hidden();
desc('Notifies Google Hangouts Chat about deploy finish');
task('chat:notify:success', function () {
if (!get('chat_webhook', false)) {
return;
}
$card = [
'header' => [
'title' => get('chat_title'),
'subtitle' => get('chat_subtitle'),
'imageUrl' => get('favicon'),
'imageStyle' => 'IMAGE',
],
'sections' => [
'widgets' => [
'keyValue' => [
'topLabel' => get('chat_line1'),
'content' => get('chat_line2'),
'contentMultiline' => false,
'bottomLabel' => 'succeeded',
// Use 'iconUrl' to set a custom icon URL (png)
'icon' => 'STAR',
'button' => [
'textButton' => [
'text' => 'Visit site',
'onClick' => [
'openLink' => [
'url' => get('hostname'),
],
],
],
],
],
],
],
];
Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();
})
->once()
->hidden();
desc('Notifies Google Hangouts Chat about deploy failure');
task('chat:notify:failure', function () {
if (!get('chat_webhook', false)) {
return;
}
$card = [
'header' => [
'title' => get('chat_title'),
'subtitle' => get('chat_subtitle'),
'imageUrl' => get('favicon'),
'imageStyle' => 'IMAGE',
],
'sections' => [
'widgets' => [
'keyValue' => [
'topLabel' => get('chat_line1'),
'content' => get('chat_line2'),
'contentMultiline' => false,
'bottomLabel' => 'failed',
// Use 'iconUrl' to set a custom icon URL (png)
// or use 'icon' and pick from this list:
// https://developers.google.com/hangouts/chat/reference/message-formats/cards#customicons
'button' => [
'textButton' => [
'text' => 'Visit site',
'onClick' => [
'openLink' => [
'url' => get('hostname'),
],
],
],
],
],
],
],
];
Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();
})
->once()
->hidden();

View File

@ -0,0 +1,46 @@
<?php
/*
## Configuration
- `hipchat_token` – Hipchat V1 auth token
- `hipchat_room_id` – Room ID or name
- `hipchat_message` – Deploy message, default is `_{{user}}_ deploying `{{what}}` to *{{where}}*`
- `hipchat_from` – Default to target
- `hipchat_color` – Message color, default is **green**
- `hipchat_url` – The URL to the message endpoint, default is https://api.hipchat.com/v1/rooms/message
## Usage
Since you should only notify Hipchat room of a successful deployment, the `hipchat:notify` task should be executed right at the end.
```php
after('deploy', 'hipchat:notify');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('hipchat_color', 'green');
set('hipchat_from', '{{where}}');
set('hipchat_message', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('hipchat_url', 'https://api.hipchat.com/v1/rooms/message');
desc('Notifies Hipchat channel of deployment');
task('hipchat:notify', function () {
$params = [
'room_id' => get('hipchat_room_id'),
'from' => get('target'),
'message' => get('hipchat_message'),
'color' => get('hipchat_color'),
'auth_token' => get('hipchat_token'),
'notify' => 0,
'format' => 'json',
];
Httpie::get(get('hipchat_url'))
->query($params)
->send();
});

View File

@ -0,0 +1,780 @@
<?php
/*
* This recipe for work with ISPManager Lite panel by API.
*/
namespace Deployer;
use Deployer\Exception\Exception;
use Deployer\Utility\Httpie;
set('ispmanager_owner', 'www-root');
set('ispmanager_doc_root', '/var/www/' . get('ispmanager_owner') . '/data/');
// ISPManager default configuration
set('ispmanager', [
'api' => [
'dsn' => 'https://root:password@localhost:1500/ispmgr',
'secure' => true,
],
'createDomain' => null,
'updateDomain' => null,
'deleteDomain' => null,
'createDatabase' => null,
'deleteDatabase' => null,
'phpSelect' => null,
'createAlias' => null,
'deleteAlias' => null,
]);
// Vhost default configuration
set('vhost', [
'name' => '{{domain}}',
'php_enable' => 'on',
'aliases' => 'www.{{domain}}',
'home' => 'www/{{domain}}',
'owner' => get('ispmanager_owner'),
'email' => 'webmaster@{{domain}}',
'charset' => 'off',
'dirindex' => 'index.php uploaded.html',
'ssi' => 'on',
'php' => 'on',
'php_mode' => 'php_mode_mod',
'basedir' => 'on',
'php_apache_version' => 'native',
'cgi' => 'off',
'log_access' => 'on',
'log_error' => 'on',
]);
// Storage
set('ispmanager_session', '');
set('ispmanager_databases', [
'servers' => [],
'hosts' => [],
'dblist' => [],
]);
set('ispmanager_domains', []);
set('ispmanager_phplist', []);
set('ispmanager_aliaslist', []);
desc('Installs ispmanager');
task('ispmanager:init', function () {
$config = get('ispmanager');
if (!is_null($config['createDatabase']) || !is_null($config['deleteDatabase'])) {
invoke('ispmanager:db-server-list');
invoke('ispmanager:db-list');
}
if (!is_null($config['createDomain']) || !is_null($config['deleteDomain'])) {
invoke('ispmanager:domain-list');
}
if (!is_null($config['phpSelect'])) {
invoke('ispmanager:domain-list');
invoke('ispmanager:get-php-list');
}
if (!is_null($config['createAlias']) || !is_null($config['deleteAlias'])) {
invoke('ispmanager:domain-list');
}
});
desc('Takes database servers list');
task('ispmanager:db-server-list', function () {
$response = ispmanagerRequest('get', [
'func' => 'db.server',
]);
$hostList = [];
$serverList = [];
if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {
foreach ($response['doc']['elem'] as $dbServer) {
$serverList[$dbServer['name']['$']] = [
'host' => $dbServer['host']['$'],
'name' => $dbServer['name']['$'],
'version' => $dbServer['savedver']['$'],
];
if (!strpos($dbServer['host']['$'], ':')) {
$dbHost = $dbServer['host']['$'] . ':3306';
} else {
$dbHost = $dbServer['host']['$'];
}
$hostList[$dbHost] = [
'host' => $dbHost,
'name' => $dbServer['name']['$'],
'version' => $dbServer['savedver']['$'],
];
}
}
add('ispmanager_databases', [
'servers' => $serverList,
'hosts' => $hostList,
]);
});
desc('Takes databases list');
task('ispmanager:db-list', function () {
$response = ispmanagerRequest('get', [
'func' => 'db',
]);
$dbList = [];
if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {
foreach ($response['doc']['elem'] as $db) {
$dbList[$db['pair']['$']] = [
'name' => $db['name']['$'],
'server' => $db['server']['$'],
'location' => $db['pair']['$'],
];
}
}
add('ispmanager_databases', [
'dblist' => $dbList,
]);
});
desc('Takes domain list');
task('ispmanager:domain-list', function () {
$response = ispmanagerRequest('get', [
'func' => 'webdomain',
]);
$domainList = [];
if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {
foreach ($response['doc']['elem'] as $domain) {
$domainList[] = $domain['name']['$'];
}
}
add('ispmanager_domains', $domainList);
});
desc('Creates new database');
task('ispmanager:db-create', function () {
$config = get('ispmanager');
if (is_null($config['createDatabase'])) {
warning('Action for database create is not active');
return;
}
$dsnData = parse_url($config['createDatabase']['dsn']);
$dbInfo = get('ispmanager_databases');
$hostInfo = null;
foreach ($dbInfo['hosts'] as $hostData) {
if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) {
$hostInfo = $hostData;
break;
}
}
if (is_null($hostInfo)) {
throw new Exception('Incorrect DB host');
}
$dbName = substr($dsnData['path'], 1);
$dbLocation = $dbName . '->' . $hostInfo['name'];
if (isset($dbInfo['dblist'][$dbLocation])) {
if (!isset($config['createDatabase']['skipIfExist']) || !$config['createDatabase']['skipIfExist']) {
throw new Exception('Database already exists!');
} else {
warning('Database already exists - skipping');
return;
}
}
$dbCreateRequest = [
'func' => 'db.edit',
'name' => $dbName,
'owner' => get('ispmanager_owner'),
'server' => $hostInfo['name'],
'charset' => $config['createDatabase']['charset'],
'sok' => 'ok',
];
if ($dsnData['user'] == '*') {
$dbCreateRequest['user'] = '*';
$dbCreateRequest['username'] = $dbName;
if ($dsnData['pass'] == '*') {
$dbCreateRequest['password'] = generatePassword(8);
} else {
$dbCreateRequest['password'] = $dsnData['pass'];
}
} else {
$dbCreateRequest['user'] = $dsnData['user'];
}
$response = ispmanagerRequest('post', $dbCreateRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Database successfully created');
}
});
desc('Deletes database');
task('ispmanager:db-delete', function () {
$config = get('ispmanager');
if (is_null($config['deleteDatabase'])) {
warning('Action for database delete is not active');
return;
}
$dbInfo = get('ispmanager_databases');
$dsnData = parse_url($config['deleteDatabase']['dsn']);
$hostInfo = null;
foreach ($dbInfo['hosts'] as $hostData) {
if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) {
$hostInfo = $hostData;
break;
}
}
if (is_null($hostInfo)) {
throw new Exception('Incorrect DB host');
}
$dbName = substr($dsnData['path'], 1);
$dbLocation = $dbName . '->' . $hostInfo['name'];
if (!isset($dbInfo['dblist'][$dbLocation])) {
if (!isset($config['deleteDatabase']['skipIfNotExist']) || !$config['deleteDatabase']['skipIfNotExist']) {
throw new Exception('Database not exist!');
} else {
warning('Database not exist - skipping');
return;
}
}
$dbDeleteRequest = [
'func' => 'db.delete',
'elid' => $dbLocation,
];
$response = ispmanagerRequest('post', $dbDeleteRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Database successfully deleted');
}
});
desc('Creates new domain');
task('ispmanager:domain-create', function () {
$config = get('ispmanager');
if (is_null($config['createDomain'])) {
warning('Action for domain create is not active');
return;
}
if (!isset($config['createDomain']['name']) || $config['createDomain']['name'] == '') {
throw new Exception('Invalid domain name!');
}
// Check domain exists
$existDomains = get('ispmanager_domains');
if (in_array($config['createDomain']['name'], $existDomains)) {
if (!isset($config['createDomain']['skipIfExist']) || !$config['createDomain']['skipIfExist']) {
throw new Exception('Domain already exists!');
} else {
warning('Domain already exists - skipping');
return;
}
}
// Build vhost create request
$vhostTemplate = get('vhost');
$domainCreateRequest = [
'func' => 'webdomain.edit',
'sok' => 'ok',
];
foreach ($vhostTemplate as $key => $value) {
$domainCreateRequest[$key] = str_replace('{{domain}}', $config['createDomain']['name'], $vhostTemplate[$key]);
}
$response = ispmanagerRequest('post', $domainCreateRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Domain successfully created');
}
});
desc('Gets allowed PHP modes and versions');
task('ispmanager:get-php-list', function () {
// Get www-root settings for fpm version
$response = ispmanagerRequest('get', [
'func' => 'user.edit',
'elid' => get('ispmanager_owner'),
'elname' => get('ispmanager_owner'),
]);
$userFPMVersion = $response['doc']['limit_php_fpm_version']['$'] ?? null;
$response = ispmanagerRequest('get', [
'func' => 'phpversions',
]);
$versions = [];
foreach ($response['doc']['elem'] as $phpVersion) {
$versions[$phpVersion['key']['$']] = [
'name' => $phpVersion['name']['$'],
'php_mode_mod' => false,
'php_mode_cgi' => false,
'php_mode_fcgi_apache' => false,
'php_mode_fcgi_nginxfpm' => false,
];
if (isset($phpVersion['default_apache']) && $phpVersion['default_apache']['$'] == 'on') {
$versions[$phpVersion['key']['$']]['php_mode_mod'] = true;
}
if (isset($phpVersion['cgi']) && $phpVersion['cgi']['$'] == 'on') {
$versions[$phpVersion['key']['$']]['php_mode_cgi'] = true;
}
if (isset($phpVersion['apache']) && $phpVersion['apache']['$'] == 'on') {
$versions[$phpVersion['key']['$']]['php_mode_fcgi_apache'] = true;
}
if (isset($phpVersion['fpm']) && $phpVersion['fpm']['$'] == 'on' && $phpVersion['key']['$'] == $userFPMVersion) {
$versions[$phpVersion['key']['$']]['php_mode_fcgi_nginxfpm'] = true;
}
}
add('ispmanager_phplist', $versions);
});
desc('Prints allowed PHP modes and versions');
task('ispmanager:print-php-list', function () {
invoke('ispmanager:get-php-list');
$versions = get('ispmanager_phplist');
writeln("PHP versions: ");
writeln(str_repeat('*', 32));
foreach ($versions as $versionKey => $versionData) {
writeln('PHP ' . $versionData['name'] . ' (ID: ' . $versionKey . ')');
writeln(str_repeat('*', 32));
if (!$versionData['php_mode_mod']) {
writeln('Apache module support (php_mode_mod) - <fg=red;options=bold>NO</>');
} else {
writeln('Apache module support (php_mode_mod) - <fg=green;options=bold>YES</>');
}
if (!$versionData['php_mode_cgi']) {
writeln('CGI support (php_mode_cgi) - <fg=red;options=bold>NO</>');
} else {
writeln('CGI support (php_mode_cgi) - <fg=green;options=bold>YES</>');
}
if (!$versionData['php_mode_fcgi_apache']) {
writeln('Apache fast-cgi support (php_mode_fcgi_apache) - <fg=red;options=bold>NO</>');
} else {
writeln('Apache fast-cgi support (php_mode_fcgi_apache) - <fg=green;options=bold>YES</>');
}
if (!$versionData['php_mode_fcgi_nginxfpm']) {
writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - <fg=red;options=bold>NO</>');
} else {
writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - <fg=green;options=bold>YES</>');
}
writeln(str_repeat('*', 32));
}
});
desc('Switches PHP version for domain');
task('ispmanager:domain-php-select', function () {
$config = get('ispmanager');
if (is_null($config['phpSelect'])) {
warning('Action for domain update is not active');
return;
}
if (!isset($config['phpSelect']['name']) || $config['phpSelect']['name'] == '') {
throw new Exception('Invalid domain name!');
}
$existDomains = get('ispmanager_domains');
if (!in_array($config['phpSelect']['name'], $existDomains)) {
throw new Exception('Domain not exist!');
}
if (!isset($config['phpSelect']['mode']) || !isset($config['phpSelect']['version'])) {
throw new Exception('Incorrect settings for select php version');
}
$phpVersions = get('ispmanager_phplist');
$newVersion = $config['phpSelect']['version'];
$newMode = $config['phpSelect']['mode'];
if (!isset($phpVersions[$newVersion])) {
throw new Exception('Incorrect php version');
}
$versionData = $phpVersions[$newVersion];
if (!isset($versionData[$newMode]) || !$versionData[$newMode]) {
throw new Exception('Incorrect php mode');
}
$domainUpdateRequest = [
'func' => 'webdomain.edit',
'elid' => $config['phpSelect']['name'],
'name' => $config['phpSelect']['name'],
'php_mode' => $newMode,
'sok' => 'ok',
];
if ($newMode == 'php_mode_mod') {
$domainUpdateRequest['php_apache_version'] = $newVersion;
} elseif ($newMode == 'php_mode_cgi') {
$domainUpdateRequest['php_cgi_version'] = $newVersion;
} elseif ($newMode == 'php_mode_fcgi_apache') {
$domainUpdateRequest['php_cgi_version'] = $newVersion;
$domainUpdateRequest['php_apache_version'] = $newVersion;
} elseif ($newMode == 'php_mode_fcgi_nginxfpm') {
$domainUpdateRequest['php_cgi_version'] = $newVersion;
$domainUpdateRequest['php_fpm_version'] = $newVersion;
} else {
throw new Exception('Unknown PHP mode!');
}
$response = ispmanagerRequest('post', $domainUpdateRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('PHP successfully selected');
}
});
desc('Creates new domain alias');
task('ispmanager:domain-alias-create', function () {
$config = get('ispmanager');
if (is_null($config['createAlias'])) {
warning('Action for alias create is not active');
return;
}
if (!isset($config['createAlias']['name']) || $config['createAlias']['name'] == '') {
throw new Exception('Invalid domain name!');
}
$existDomains = get('ispmanager_domains');
if (!in_array($config['createAlias']['name'], $existDomains)) {
throw new Exception('Domain not exist!');
}
if (!isset($config['createAlias']['alias']) || $config['createAlias']['alias'] == '') {
throw new Exception('Invalid alias name!');
}
// Get current domain data
$response = ispmanagerRequest('get', [
'func' => 'webdomain.edit',
'elid' => $config['createAlias']['name'],
'elname' => $config['createAlias']['name'],
]);
$existAliases = [];
if (isset($response['doc']['aliases']['$'])) {
$existAliases = explode(' ', $response['doc']['aliases']['$']);
}
$newAliasList = [];
$createAliasList = explode(' ', $config['createAlias']['alias']);
foreach ($createAliasList as $createAlias) {
if (in_array($createAlias, $existAliases)) {
if (!isset($config['createAlias']['skipIfExist']) || !$config['createAlias']['skipIfExist']) {
throw new Exception('Alias already exists!');
} else {
warning('Alias ' . $createAlias . ' already exists - skipping');
continue;
}
}
$newAliasList[] = $createAlias;
}
$saveAliases = array_merge($existAliases, $newAliasList);
$domainUpdateRequest = [
'func' => 'webdomain.edit',
'elid' => $config['createAlias']['name'],
'name' => $config['createAlias']['name'],
'aliases' => implode(' ', $saveAliases),
'sok' => 'ok',
];
$response = ispmanagerRequest('post', $domainUpdateRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Alias successfully created');
}
});
desc('Deletes domain alias');
task('ispmanager:domain-alias-delete', function () {
$config = get('ispmanager');
if (is_null($config['deleteAlias'])) {
warning('Action for alias create is not active');
return;
}
if (!isset($config['deleteAlias']['name']) || $config['deleteAlias']['name'] == '') {
throw new Exception('Invalid domain name!');
}
$existDomains = get('ispmanager_domains');
if (!in_array($config['deleteAlias']['name'], $existDomains)) {
throw new Exception('Domain not exist!');
}
if (!isset($config['deleteAlias']['alias']) || $config['deleteAlias']['alias'] == '') {
throw new Exception('Invalid alias name!');
}
// Get current domain data
$response = ispmanagerRequest('get', [
'func' => 'webdomain.edit',
'elid' => $config['createAlias']['name'],
'elname' => $config['createAlias']['name'],
]);
$existAliases = [];
if (isset($response['doc']['aliases']['$'])) {
$existAliases = explode(' ', $response['doc']['aliases']['$']);
}
$deleteAliasList = explode(' ', $config['deleteAlias']['alias']);
foreach ($deleteAliasList as $deleteAlias) {
if (!in_array($deleteAlias, $existAliases)) {
if (!isset($config['deleteAlias']['skipIfNotExist']) || !$config['deleteAlias']['skipIfNotExist']) {
throw new Exception('Alias not exist!');
} else {
warning('Alias ' . $deleteAlias . ' not exist - skipping');
continue;
}
}
if (($index = array_search($deleteAlias, $existAliases)) !== false) {
unset($existAliases[$index]);
}
}
$domainUpdateRequest = [
'func' => 'webdomain.edit',
'elid' => $config['deleteAlias']['name'],
'name' => $config['deleteAlias']['name'],
'aliases' => implode(' ', $existAliases),
'sok' => 'ok',
];
$response = ispmanagerRequest('post', $domainUpdateRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Alias successfully deleted');
}
});
desc('Deletes domain');
task('ispmanager:domain-delete', function () {
$config = get('ispmanager');
if (is_null($config['deleteDomain'])) {
warning('Action for domain delete is not active');
return;
}
if (!isset($config['deleteDomain']['name']) || $config['deleteDomain']['name'] == '') {
throw new Exception('Invalid domain name!');
}
// Check domain exists
$existDomains = get('ispmanager_domains');
if (!in_array($config['deleteDomain']['name'], $existDomains)) {
if (!isset($config['deleteDomain']['skipIfNotExist']) || !$config['deleteDomain']['skipIfNotExist']) {
throw new Exception('Domain not exist!');
} else {
warning('Domain not exist - skipping');
return;
}
}
// Build request
$domainDeleteRequest = [
'func' => 'webdomain.delete.confirm',
'elid' => $config['deleteDomain']['name'],
'sok' => 'ok',
];
if (!isset($config['deleteDomain']['removeDir']) || !$config['deleteDomain']['removeDir']) {
$domainDeleteRequest['remove_directory'] = 'off';
} else {
$domainDeleteRequest['remove_directory'] = 'on';
}
$response = ispmanagerRequest('post', $domainDeleteRequest);
if (isset($response['doc']['error']['msg']['$'])) {
throw new Exception($response['doc']['error']['msg']['$']);
} else {
info('Domain successfully deleted');
}
});
desc('Auto task processing');
task('ispmanager:process', function () {
$config = get('ispmanager');
if (!is_null($config['createDatabase'])) {
invoke('ispmanager:db-create');
}
if (!is_null($config['deleteDatabase'])) {
invoke('ispmanager:db-delete');
}
if (!is_null($config['createDomain'])) {
invoke('ispmanager:domain-create');
}
if (!is_null($config['deleteDomain'])) {
invoke('ispmanager:domain-delete');
}
if (!is_null($config['phpSelect'])) {
invoke('ispmanager:domain-php-select');
}
if (!is_null($config['createAlias'])) {
invoke('ispmanager:domain-alias-create');
}
if (!is_null($config['deleteAlias'])) {
invoke('ispmanager:domain-alias-delete');
}
});
function ispmanagerRequest($method, $requestData)
{
$config = get('ispmanager');
$dsnData = parse_url($config['api']['dsn']);
$requestUrl = $dsnData['scheme'] . '://' . $dsnData['host'] . ':' . $dsnData['port'] . $dsnData['path'];
if ($config['api']['secure'] && get('ispmanager_session') == '') {
ispmanagerAuthRequest($requestUrl, $dsnData['user'], $dsnData['pass']);
}
if ($method == 'post') {
return Httpie::post($requestUrl)
->formBody(prepareRequest($requestData))
->setopt(CURLOPT_SSL_VERIFYPEER, false)
->setopt(CURLOPT_SSL_VERIFYHOST, false)
->getJson();
} elseif ($method == 'get') {
return Httpie::get($requestUrl)
->query(prepareRequest($requestData))
->setopt(CURLOPT_SSL_VERIFYPEER, false)
->setopt(CURLOPT_SSL_VERIFYHOST, false)
->getJson();
} else {
throw new Exception('Unknown request method');
}
}
function ispmanagerAuthRequest($url, $login, $pass)
{
$authRequestData = [
'func' => 'auth',
'username' => $login,
'password' => $pass,
];
$responseData = Httpie::post($url)
->setopt(CURLOPT_SSL_VERIFYPEER, false)
->setopt(CURLOPT_SSL_VERIFYHOST, false)
->formBody(prepareRequest($authRequestData))
->getJson();
if (isset($responseData['doc']['auth']['$id'])) {
set('ispmanager_session', $responseData['doc']['auth']['$id']);
} else {
throw new Exception('Error while create auth session');
}
}
function prepareRequest($requestData)
{
$config = get('ispmanager');
$dsnData = parse_url($config['api']['dsn']);
if (!isset($requestData['out'])) {
$requestData['out'] = 'json';
}
if (!$config['api']['secure']) {
$requestData['authinfo'] = $dsnData['user'] . ':' . $dsnData['pass'];
} else {
if (get('ispmanager_session') != '') {
$requestData['auth'] = get('ispmanager_session');
}
}
return $requestData;
}
function generatePassword($lenght)
{
return substr(md5(uniqid()), 0, $lenght);
}
// Callbacks before actions under domains
before('ispmanager:domain-alias-create', 'ispmanager:init');
before('ispmanager:domain-alias-delete', 'ispmanager:init');
before('ispmanager:domain-create', 'ispmanager:init');
before('ispmanager:domain-delete', 'ispmanager:init');
before('ispmanager:domain-php-select', 'ispmanager:init');
// Callbacks before actions under databases
before('ispmanager:db-create', 'ispmanager:init');
before('ispmanager:db-delete', 'ispmanager:init');

View File

@ -0,0 +1,154 @@
<?php
/*
## Installing
Create a Mattermost incoming webhook, through the administration panel.
Add hook on deploy:
```
before('deploy', 'mattermost:notify');
```
## Configuration
- `mattermost_webhook` - incoming mattermost webook **required**
```
set('mattermost_webook', 'https://{your-mattermost-site}/hooks/xxx-generatedkey-xxx');
```
- `mattermost_channel` - overrides the channel the message posts in
```
set('mattermost_channel', 'town-square');
```
- `mattermost_username` - overrides the username the message posts as
```
set('mattermost_username', 'deployer');
```
- `mattermost_icon_url` - overrides the profile picture the message posts with
```
set('mattermost_icon_url', 'https://domain.com/your-icon.png');
```
- `mattermost_text` - notification message
```
set('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**');
```
- `mattermost_success_text` – success template, default:
```
set('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}');
```
- `mattermost_failure_text` – failure template, default:
```
set('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}');
```
- `mattermost_success_emoji` – emoji added at the end of success text
- `mattermost_failure_emoji` – emoji added at the end of failure text
For detailed information about Mattermost hooks see: https://developers.mattermost.com/integrate/incoming-webhooks/
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'mattermost:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'mattermost:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'mattermost:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('mattermost_webhook', null);
set('mattermost_channel', null);
set('mattermost_username', 'deployer');
set('mattermost_icon_url', null);
set('mattermost_success_emoji', ':white_check_mark:');
set('mattermost_failure_emoji', ':x:');
set('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**');
set('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}');
set('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}');
desc('Notifies mattermost');
task('mattermost:notify', function () {
if (null === get('mattermost_webhook')) {
return;
}
$body = [
'text' => get('mattermost_text'),
'username' => get('mattermost_username'),
];
if (get('mattermost_channel')) {
$body['channel'] = get('mattermost_channel');
}
if (get('mattermost_icon_url')) {
$body['icon_url'] = get('mattermost_icon_url');
}
Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();
});
desc('Notifies mattermost about deploy finish');
task('mattermost:notify:success', function () {
if (null === get('mattermost_webhook')) {
return;
}
$body = [
'text' => get('mattermost_success_text'),
'username' => get('mattermost_username'),
];
if (get('mattermost_channel')) {
$body['channel'] = get('mattermost_channel');
}
if (get('mattermost_icon_url')) {
$body['icon_url'] = get('mattermost_icon_url');
}
Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();
});
desc('Notifies mattermost about deploy failure');
task('mattermost:notify:failure', function () {
if (null === get('mattermost_webhook')) {
return;
}
$body = [
'text' => get('mattermost_failure_text'),
'username' => get('mattermost_username'),
];
if (get('mattermost_channel')) {
$body['channel'] = get('mattermost_channel');
}
if (get('mattermost_icon_url')) {
$body['icon_url'] = get('mattermost_icon_url');
}
Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();
});

View File

@ -0,0 +1,164 @@
<?php
/*
## Installing
Require ms-teams recipe in your `deploy.php` file:
Setup:
1. Open MS Teams
2. Navigate to Teams section
3. Select existing or create new team
4. Select existing or create new channel
5. Hover over channel to get three dots, click, in menu select "Connectors"
6. Search for and configure "Incoming Webhook"
7. Confirm/create and copy your Webhook URL
8. Setup deploy.php
Add in header:
```php
require 'contrib/ms-teams.php';
set('teams_webhook', 'https://outlook.office.com/webhook/...');
```
Add in content:
```php
before('deploy', 'teams:notify');
after('deploy:success', 'teams:notify:success');
after('deploy:failed', 'teams:notify:failure');
```
9.) Sip your coffee
## Configuration
- `teams_webhook` – teams incoming webhook url, **required**
```
set('teams_webhook', 'https://outlook.office.com/webhook/...');
```
- `teams_title` – the title of application, default `{{application}}`
- `teams_text` – notification message template, markdown supported
```
set('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
```
- `teams_success_text` – success template, default:
```
set('teams_success_text', 'Deploy to *{{where}}* successful');
```
- `teams_failure_text` – failure template, default:
```
set('teams_failure_text', 'Deploy to *{{where}}* failed');
```
- `teams_color` – color's attachment
- `teams_success_color` – success color's attachment
- `teams_failure_color` – failure color's attachment
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'teams:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'teams:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'teams:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Title of project
set('teams_title', function () {
return get('application', 'Project');
});
// Allow Continue on Failure
set('teams_failure_continue', false);
// Deploy message
set('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('teams_success_text', 'Deploy to *{{where}}* successful');
set('teams_failure_text', 'Deploy to *{{where}}* failed');
// Color of attachment
set('teams_color', '#4d91f7');
set('teams_success_color', '#00c100');
set('teams_failure_color', '#ff0909');
desc('Notifies Teams');
task('teams:notify', function () {
if (!get('teams_webhook', false)) {
warning('No MS Teams webhook configured');
return;
}
try {
Httpie::post(get('teams_webhook'))->jsonBody([
"themeColor" => get('teams_color'),
'text' => get('teams_text'),
])->send();
} catch (\Exception $e) {
if (get('teams_failure_continue', false)) {
warning('Error sending Teams Notification: ' . $e->getMessage());
} else {
throw $e;
}
}
})
->once()
->hidden();
desc('Notifies Teams about deploy finish');
task('teams:notify:success', function () {
if (!get('teams_webhook', false)) {
warning('No MS Teams webhook configured');
return;
}
try {
Httpie::post(get('teams_webhook'))->jsonBody([
"themeColor" => get('teams_success_color'),
'text' => get('teams_success_text'),
])->send();
} catch (\Exception $e) {
if (get('teams_failure_continue', false)) {
warning('Error sending Teams Notification: ' . $e->getMessage());
} else {
throw $e;
}
}
})
->once()
->hidden();
desc('Notifies Teams about deploy failure');
task('teams:notify:failure', function () {
if (!get('teams_webhook', false)) {
warning('No MS Teams webhook configured');
return;
}
try {
Httpie::post(get('teams_webhook'))->jsonBody([
"themeColor" => get('teams_failure_color'),
'text' => get('teams_failure_text'),
])->send();
} catch (\Exception $e) {
if (get('teams_failure_continue', false)) {
warning('Error sending Teams Notification: ' . $e->getMessage());
} else {
throw $e;
}
}
})
->once()
->hidden();

View File

@ -0,0 +1,54 @@
<?php
/*
## Configuration
- `newrelic_app_id` – newrelic's app id
- `newrelic_api_key` – newrelic's api key
- `newrelic_description` – message to send
- `newrelic_endpoint` – newrelic's REST API endpoint
## Usage
Since you should only notify New Relic of a successful deployment, the `newrelic:notify` task should be executed right at the end.
```php
after('deploy', 'newrelic:notify');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('newrelic_app_id', function () {
throw new \Exception('Please, configure "newrelic_app_id" parameter.');
});
set('newrelic_description', function () {
return runLocally('git log -n 1 --format="%an: %s" | tr \'"\' "\'"');
});
set('newrelic_revision', function () {
return runLocally('git log -n 1 --format="%h"');
});
set('newrelic_endpoint', 'api.newrelic.com');
desc('Notifies New Relic of deployment');
task('newrelic:notify', function () {
if (($appId = get('newrelic_app_id')) && ($apiKey = get('newrelic_api_key')) && ($endpoint = get('newrelic_endpoint'))) {
$data = [
'user' => get('user'),
'revision' => get('newrelic_revision'),
'description' => get('newrelic_description'),
];
Httpie::post("https://$endpoint/v2/applications/$appId/deployments.json")
->header("X-Api-Key", $apiKey)
->query(['deployment' => $data])
->send();
}
})
->once()
->hidden();

View File

@ -0,0 +1,35 @@
<?php
/*
## Configuration
- `bin/npm` *(optional)*: set npm binary, automatically detected otherwise.
## Usage
```php
after('deploy:update_code', 'npm:install');
after('npm:install', 'npm:build');
```
*/
namespace Deployer;
set('bin/npm', function () {
return which('npm');
});
// Uses `npm ci` command. This command is similar to npm install,
// except it's meant to be used in automated environments such as
// test platforms, continuous integration, and deployment -- or
// any situation where you want to make sure you're doing a clean
// install of your dependencies.
desc('Installs npm packages');
task('npm:install', function () {
run('cd {{release_path}} && {{bin/npm}} ci');
});
desc('Runs npm build');
task('npm:build', function () {
run('cd {{release_path}} && {{bin/npm}} run build');
});

View File

@ -0,0 +1,159 @@
<?php
/*
## Installing
Require ntfy.sh recipe in your `deploy.php` file:
Setup:
1. Setup deploy.php
Add in header:
```php
require 'contrib/ntfy.php';
set('ntfy_topic', 'ntfy.sh/mytopic');
```
Add in content:
```php
before('deploy', 'ntfy:notify');
after('deploy:success', 'ntfy:notify:success');
after('deploy:failed', 'ntfy:notify:failure');
```
9.) Sip your coffee
## Configuration
- `ntfy_server` – ntfy server url, default `ntfy.sh`
```
set('ntfy_server', 'ntfy.sh');
```
- `ntfy_topic` – ntfy topic, **required**
```
set('ntfy_topic', 'mysecrettopic');
```
- `ntfy_title` – the title of the message, default `{{application}}`
- `ntfy_text` – notification message template
```
set('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
```
- `ntfy_tags` – notification message tags / emojis (comma separated)
```
set('ntfy_tags', `information_source`);
```
- `ntfy_priority` – notification message priority (integer)
```
set('ntfy_priority', 5);
```
- `ntfy_success_text` – success template, default:
```
set('ntfy_success_text', 'Deploy to *{{where}}* successful');
```
- `ntfy_success_tags` – success tags / emojis (comma separated)
```
set('ntfy_success_tags', `white_check_mark,champagne`);
```
- `ntfy_success_priority` – success notification message priority
- `ntfy_failure_text` – failure template, default:
```
set('ntfy_failure_text', 'Deploy to *{{where}}* failed');
```
- `ntfy_failure_tags` – failure tags / emojis (comma separated)
```
set('ntfy_failure_tags', `warning,skull`);
```
- `ntfy_failure_priority` – failure notification message priority
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'ntfy:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'ntfy:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'ntfy:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('ntfy_server', 'ntfy.sh');
// Title of project
set('ntfy_title', function () {
return get('application', 'Project');
});
// Deploy message
set('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('ntfy_success_text', 'Deploy to *{{where}}* successful');
set('ntfy_failure_text', 'Deploy to *{{where}}* failed');
// Message tags
set('ntfy_tags', '');
set('ntfy_success_tags', '');
set('ntfy_failure_tags', '');
desc('Notifies ntfy server');
task('ntfy:notify', function () {
if (!get('ntfy_topic', false)) {
warning('No ntfy topic configured');
return;
}
Httpie::post(get('ntfy_server'))->jsonBody([
"topic" => get('ntfy_topic'),
"title" => get('ntfy_title'),
"message" => get('ntfy_text'),
"tags" => explode(",", get('ntfy_tags')),
"priority" => get('ntfy_priority'),
])->send();
})
->once()
->hidden();
desc('Notifies ntfy server about deploy finish');
task('ntfy:notify:success', function () {
if (!get('ntfy_topic', false)) {
warning('No ntfy topic configured');
return;
}
Httpie::post(get('ntfy_server'))->jsonBody([
"topic" => get('ntfy_topic'),
"title" => get('ntfy_title'),
"message" => get('ntfy_success_text'),
"tags" => explode(",", get('ntfy_success_tags')),
"priority" => get('ntfy_success_priority'),
])->send();
})
->once()
->hidden();
desc('Notifies ntfy server about deploy failure');
task('ntfy:notify:failure', function () {
if (!get('ntfy_topic', false)) {
warning('No ntfy topic configured');
return;
}
Httpie::post(get('ntfy_server'))->jsonBody([
"topic" => get('ntfy_topic'),
"title" => get('ntfy_title'),
"message" => get('ntfy_failure_text'),
"tags" => explode(",", get('ntfy_failure_tags')),
"priority" => get('ntfy_failure_priority'),
])->send();
})
->once()
->hidden();

View File

@ -0,0 +1,228 @@
<?php
/*
## Configuration options
All options are in the config parameter `phinx` specified as an array (instead of the `phinx_path` variable).
All parameters are *optional*, but you can specify them with a dictionary (to change all parameters)
or by deployer dot notation (to change one option).
### Phinx params
- `phinx.environment`
- `phinx.date`
- `phinx.configuration` N.B. current directory is the project directory
- `phinx.target`
- `phinx.seed`
- `phinx.parser`
- `phinx.remove-all` (pass empty string as value)
### Phinx path params
- `phinx_path` Specify phinx path (by default phinx is searched for in $PATH, ./vendor/bin and ~/.composer/vendor/bin)
### Example of usage
```php
$phinx_env_vars = [
'environment' => 'development',
'configuration' => './migration/.phinx.yml',
'target' => '20120103083322',
'remove-all' => '',
];
set('phinx_path', '/usr/local/phinx/bin/phinx');
set('phinx', $phinx_env_vars);
after('cleanup', 'phinx:migrate');
// or set it for a specific server
host('dev')
->user('user')
->set('deploy_path', '/var/www')
->set('phinx', $phinx_env_vars)
->set('phinx_path', '');
```
## Suggested Usage
You can run all tasks before or after any
tasks (but you need to specify external configs for phinx).
If you use internal configs (which are in your project) you need
to run it after the `deploy:update_code` task is completed.
## Read more
For further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html).
*/
namespace Deployer;
use Deployer\Exception\RunException;
/*
* Phinx recipe for Deployer
*
* @author Alexey Boyko <ket4yiit@gmail.com>
* @contributor Security-Database <info@security-database.com>
* @copyright 2016 Alexey Boyko
* @license MIT https://github.com/deployphp/recipes/blob/master/LICENSE
*
* @link https://github.com/deployphp/recipes
*
* @see http://deployer.org
* @see https://phinx.org
*/
/**
* Path to Phinx
*/
set('bin/phinx', function () {
try {
$phinxPath = which('phinx');
} catch (RunException $e) {
$phinxPath = null;
}
if ($phinxPath !== null) {
return "phinx";
} elseif (test('[ -f {{release_path}}/vendor/bin/phinx ]')) {
return "{{release_path}}/vendor/bin/phinx";
} elseif (test('[ -f ~/.composer/vendor/bin/phinx ]')) {
return '~/.composer/vendor/bin/phinx';
} else {
throw new \RuntimeException('Cannot find phinx. Please specify path to phinx manually');
}
});
/**
* Make Phinx command
*
* @param string $cmdName Name of command
* @param array $conf Command options(config)
*
* @return string Phinx command to execute
*/
function phinx_get_cmd($cmdName, $conf)
{
$phinx = get('phinx_path') ?: get('bin/phinx');
$phinxCmd = "$phinx $cmdName";
$options = '';
foreach ($conf as $name => $value) {
$options .= " --$name $value";
}
$phinxCmd .= $options;
return $phinxCmd;
}
/**
* Returns options array that allowed for command
*
* @param array $allowedOptions List of allowed options
*
* @return array Array of options
*/
function phinx_get_allowed_config($allowedOptions)
{
$opts = [];
try {
foreach (get('phinx') as $key => $val) {
if (in_array($key, $allowedOptions)) {
$opts[$key] = $val;
}
}
} catch (\RuntimeException $e) {
}
return $opts;
}
desc('Migrats database with phinx');
task('phinx:migrate', function () {
$ALLOWED_OPTIONS = [
'configuration',
'date',
'environment',
'target',
'parser',
];
$conf = phinx_get_allowed_config($ALLOWED_OPTIONS);
cd('{{release_path}}');
$phinxCmd = phinx_get_cmd('migrate', $conf);
run($phinxCmd);
cd('{{deploy_path}}');
});
desc('Rollbacks database migrations with phinx');
task('phinx:rollback', function () {
$ALLOWED_OPTIONS = [
'configuration',
'date',
'environment',
'target',
'parser',
];
$conf = phinx_get_allowed_config($ALLOWED_OPTIONS);
cd('{{release_path}}');
$phinxCmd = phinx_get_cmd('rollback', $conf);
run($phinxCmd);
cd('{{deploy_path}}');
});
desc('Seeds database with phinx');
task('phinx:seed', function () {
$ALLOWED_OPTIONS = [
'configuration',
'environment',
'parser',
'seed',
];
$conf = phinx_get_allowed_config($ALLOWED_OPTIONS);
cd('{{release_path}}');
$phinxCmd = phinx_get_cmd('seed:run', $conf);
run($phinxCmd);
cd('{{deploy_path}}');
});
desc('Sets a migrations breakpoint with phinx');
task('phinx:breakpoint', function () {
$ALLOWED_OPTIONS = [
'configuration',
'environment',
'remove-all',
'target',
];
$conf = phinx_get_allowed_config($ALLOWED_OPTIONS);
cd('{{release_path}}');
$phinxCmd = phinx_get_cmd('breakpoint', $conf);
run($phinxCmd);
cd('{{deploy_path}}');
});

View File

@ -0,0 +1,45 @@
<?php
/*
:::caution
Do **not** reload php-fpm. Some user requests could fail or not complete in the
process of reloading.
Instead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision
recipe, it's already configured the right way and no php-fpm reload is needed.
:::
## Configuration
- `php_fpm_version` – The PHP-fpm version. For example: `8.0`.
- `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php{{php_fpm_version}}-fpm`.
- `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload {{php_fpm_service}}`.
## Usage
Start by explicitely providing the current version of PHP-version using the `php_fpm_version`.
Alternatively, you may use any of the options above to configure how PHP-fpm should reload.
Then, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so.
```php
set('php_fpm_version', '8.0');
after('deploy', 'php-fpm:reload');
```
*/
namespace Deployer;
// Automatically detects by using {{bin/php}}.
set('php_fpm_version', function () {
return run('{{bin/php}} -r "printf(\'%d.%d\', PHP_MAJOR_VERSION, PHP_MINOR_VERSION);"');
});
set('php_fpm_service', 'php{{php_fpm_version}}-fpm');
desc('Reloads the php-fpm service');
task('php-fpm:reload', function () {
warning('Avoid reloading php-fpm [deployer.org/docs/8.x/avoid-php-fpm-reloading]');
run('sudo systemctl reload {{php_fpm_service}}');
});

View File

@ -0,0 +1,110 @@
<?php
/*
### Installing
```php
// deploy.php
require 'recipe/rabbit.php';
```
### Configuration options
- **rabbit** *(required)*: accepts an *array* with the connection information to [rabbitmq](http://www.rabbitmq.com) server token and team name.
You can provide also other configuration options:
- *host* - default is localhost
- *port* - default is 5672
- *username* - default is *guest*
- *password* - default is *guest*
- *channel* - no default value, need to be specified via config
- *message* - default is **Deployment to '$host' on *$prod* was successful\n$releasePath**
- *vhost* - default is
```php
// deploy.php
set('rabbit', [
'host' => 'localhost',
'port' => '5672',
'username' => 'guest',
'password' => 'guest',
'channel' => 'notify-channel',
'vhost' => '/my-app'
]);
```
### Suggested Usage
Since you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end.
```php
// deploy.php
before('deploy:end', 'deploy:rabbit');
```
*/
namespace Deployer;
use Deployer\Task\Context;
use PhpAmqpLib\Connection\AMQPConnection;
use PhpAmqpLib\Message\AMQPMessage;
desc('Notifies RabbitMQ channel about deployment');
task('deploy:rabbit', function () {
if (!class_exists('PhpAmqpLib\Connection\AMQPConnection')) {
throw new \RuntimeException("<comment>Please install php package</comment> <info>videlalvaro/php-amqplib</info> <comment>to use rabbitmq</comment>");
}
$config = get('rabbit', []);
if (!isset($config['message'])) {
$releasePath = get('release_path');
$host = Context::get()->getHost();
$stage = get('stage', false);
$stageInfo = ($stage) ? sprintf(' on *%s*', $stage) : '';
$message = "Deployment to '%s'%s was successful\n(%s)";
$config['message'] = sprintf(
$message,
$host->getHostname(),
$stageInfo,
$releasePath,
);
}
$defaultConfig = [
'host' => 'localhost',
'port' => 5672,
'username' => 'guest',
'password' => 'guest',
'vhost' => '/',
];
$config = array_merge($defaultConfig, $config);
if (!isset($config['channel'])
|| !isset($config['host'])
|| !isset($config['port'])
|| !isset($config['username'])
|| !isset($config['password'])
|| !isset($config['vhost'])) {
throw new \RuntimeException("<comment>Please configure rabbit config:</comment> <info>set('rabbit', array('channel' => 'channel', 'host' => 'host', 'port' => 'port', 'username' => 'username', 'password' => 'password'));</info>");
}
$connection = new AMQPConnection($config['host'], $config['port'], $config['username'], $config['password'], $config['vhost']);
$channel = $connection->channel();
$msg = new AMQPMessage($config['message']);
$channel->basic_publish($msg, $config['channel'], $config['channel']);
$channel->close();
$connection->close();
});

View File

@ -0,0 +1,42 @@
<?php
/*
## Configuration
- `raygun_api_key` – the API key of your Raygun application
- `raygun_version` – the version of your application that this deployment is releasing
- `raygun_owner_name` – the name of the person creating this deployment
- `raygun_email` – the email of the person creating this deployment
- `raygun_comment` – the deployment notes
- `raygun_scm_identifier` – the commit that this deployment was built off
- `raygun_scm_type` - the source control system you use
## Usage
To notify Raygun of a successful deployment, you can use the 'raygun:notify' task after a deployment.
```php
after('deploy', 'raygun:notify');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
desc('Notifies Raygun of deployment');
task('raygun:notify', function () {
$data = [
'apiKey' => get('raygun_api_key'),
'version' => get('raygun_version'),
'ownerName' => get('raygun_owner_name'),
'emailAddress' => get('raygun_email'),
'comment' => get('raygun_comment'),
'scmIdentifier' => get('raygun_scm_identifier'),
'scmType' => get('raygun_scm_type'),
];
Httpie::post('https://app.raygun.io/deployments')
->jsonBody($data)
->send();
});

View File

@ -0,0 +1,173 @@
<?php
/*
## Installing
Create a RocketChat incoming webhook, through the administration panel.
Add hook on deploy:
```
before('deploy', 'rocketchat:notify');
```
## Configuration
- `rocketchat_webhook` - incoming rocketchat webook **required**
```
set('rocketchat_webhook', 'https://rocketchat.yourcompany.com/hooks/XXXXX');
```
- `rocketchat_title` - the title of the application, defaults to `{{application}}`
- `rocketchat_text` - notification message
```
set('rocketchat_text', '_{{user}}_ deploying {{what}} to {{where}}');
```
- `rocketchat_success_text` – success template, default:
```
set('rocketchat_success_text', 'Deploy to *{{where}}* successful');
```
- `rocketchat_failure_text` – failure template, default:
```
set('rocketchat_failure_text', 'Deploy to *{{where}}* failed');
```
- `rocketchat_color` – color's attachment
- `rocketchat_success_color` – success color's attachment
- `rocketchat_failure_color` – failure color's attachment
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'rocketchat:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'rocketchat:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'rocketchat:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('rockchat_title', function () {
return get('application', 'Project');
});
set('rocketchat_icon_emoji', ':robot:');
set('rocketchat_icon_url', null);
set('rocketchat_channel', null);
set('rocketchat_room_id', null);
set('rocketchat_username', null);
set('rocketchat_webhook', null);
set('rocketchat_color', '#000000');
set('rocketchat_success_color', '#00c100');
set('rocketchat_failure_color', '#ff0909');
set('rocketchat_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('rocketchat_success_text', 'Deploy to *{{where}}* successful');
set('rocketchat_failure_text', 'Deploy to *{{where}}* failed');
desc('Notifies RocketChat');
task('rocketchat:notify', function () {
if (null === get('rocketchat_webhook')) {
return;
}
$body = [
'text' => get('rockchat_title'),
'username' => get('rocketchat_username'),
'attachments' => [[
'text' => get('rocketchat_text'),
'color' => get('rocketchat_color'),
]],
];
if (get('rocketchat_channel')) {
$body['channel'] = get('rocketchat_channel');
}
if (get('rocketchat_room_id')) {
$body['roomId'] = get('rocketchat_room_id');
}
if (get('rocketchat_icon_url')) {
$body['avatar'] = get('rocketchat_icon_url');
} elseif (get('rocketchat_icon_emoji')) {
$body['emoji'] = get('rocketchat_icon_emoji');
}
Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();
});
desc('Notifies RocketChat about deploy finish');
task('rocketchat:notify:success', function () {
if (null === get('rocketchat_webhook')) {
return;
}
$body = [
'text' => get('rockchat_title'),
'username' => get('rocketchat_username'),
'attachments' => [[
'text' => get('rocketchat_success_text'),
'color' => get('rocketchat_success_color'),
]],
];
if (get('rocketchat_channel')) {
$body['channel'] = get('rocketchat_channel');
}
if (get('rocketchat_room_id')) {
$body['roomId'] = get('rocketchat_room_id');
}
if (get('rocketchat_icon_url')) {
$body['avatar'] = get('rocketchat_icon_url');
} elseif (get('rocketchat_icon_emoji')) {
$body['emoji'] = get('rocketchat_icon_emoji');
}
Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();
});
desc('Notifies RocketChat about deploy failure');
task('rocketchat:notify:failure', function () {
if (null === get('rocketchat_webhook')) {
return;
}
$body = [
'text' => get('rockchat_title'),
'username' => get('rocketchat_username'),
'attachments' => [[
'color' => get('rocketchat_failure_color'),
'text' => get('rocketchat_failure_text'),
]],
];
if (get('rocketchat_channel')) {
$body['channel'] = get('rocketchat_channel');
}
if (get('rocketchat_room_id')) {
$body['roomId'] = get('rocketchat_room_id');
}
if (get('rocketchat_icon_url')) {
$body['avatar'] = get('rocketchat_icon_url');
} elseif (get('rocketchat_icon_emoji')) {
$body['emoji'] = get('rocketchat_icon_emoji');
}
Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();
});

View File

@ -0,0 +1,48 @@
<?php
/*
## Configuration
- `rollbar_token` – access token to rollbar api
- `rollbar_comment` – comment about deploy, default to
```php
set('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
```
- `rollbar_username` – rollbar user name
## Usage
Since you should only notify Rollbar channel of a successful deployment, the `rollbar:notify` task should be executed right at the end.
```php
after('deploy', 'rollbar:notify');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
desc('Notifies Rollbar of deployment');
task('rollbar:notify', function () {
if (!get('rollbar_token', false)) {
return;
}
$params = [
'access_token' => get('rollbar_token'),
'environment' => get('where'),
'revision' => runLocally('git log -n 1 --format="%h"'),
'local_username' => get('user'),
'rollbar_username' => get('rollbar_username'),
'comment' => get('rollbar_comment'),
];
Httpie::post('https://api.rollbar.com/api/1/deploy/')
->formBody($params)
->send();
})
->once();

View File

@ -0,0 +1,252 @@
<?php
/*
:::warning
This must not be confused with `/src/Utility/Rsync.php`, deployer's built-in rsync. Their configuration options are also very different, read carefully below.
:::
## Configuration options
- **rsync**: Accepts an array with following rsync options (all are optional and defaults are ok):
- *exclude*: accepts an *array* with patterns to be excluded from sending to server
- *exclude-file*: accepts a *string* containing absolute path to file, which contains exclude patterns
- *include*: accepts an *array* with patterns to be included in sending to server
- *include-file*: accepts a *string* containing absolute path to file, which contains include patterns
- *filter*: accepts an *array* of rsync filter rules
- *filter-file*: accepts a *string* containing merge-file filename.
- *filter-perdir*: accepts a *string* containing merge-file filename to be scanned and merger per each directory in rsync list on files to send
- *flags*: accepts a *string* of flags to set when calling rsync command. Please **avoid** flags that accept params, and use *options* instead.
- *options*: accepts an *array* of options to set when calling rsync command. **DO NOT** prefix options with `--` as it's automatically added.
- *timeout*: accepts an *int* defining timeout for rsync command to run locally.
### Sample Configuration:
Following is default configuration. By default rsync ignores only git dir and `deploy.php` file.
```php
// deploy.php
set('rsync',[
'exclude' => [
'.git',
'deploy.php',
],
'exclude-file' => false,
'include' => [],
'include-file' => false,
'filter' => [],
'filter-file' => false,
'filter-perdir'=> false,
'flags' => 'rz', // Recursive, with compress
'options' => ['delete'],
'timeout' => 60,
]);
```
If You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example:
```php
// deploy.php
set('rsync',[
'exclude' => ['excludes_file'],
'exclude-file' => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems
'include' => [],
'include-file' => false,
'filter' => [],
'filter-file' => false,
'filter-perdir' => false,
'flags' => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag
'options' => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty
'timeout' => 3600, //for those huge repos or crappy connection
]);
```
### Parameter
- **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory
- **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination.
### Sample configurations:
This is default configuration:
```php
set('rsync_src', __DIR__);
set('rsync_dest','{{release_path}}');
```
If You use local deploy recipe You can set src to local release:
```php
host('hostname')
->hostname('10.10.10.10')
->port(22)
->set('deploy_path','/your/remote/path/app')
->set('rsync_src', '/your/local/path/app')
->set('rsync_dest','{{release_path}}');
```
## Usage
- `rsync` task
Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git.
- `rsync:warmup` task
If Your deploy task looks like:
```php
task('deploy', [
'deploy:prepare',
'deploy:release',
'rsync',
'deploy:vendors',
'deploy:symlink',
])->desc('Deploy your project');
```
And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient.
*/
namespace Deployer;
use Deployer\Host\Localhost;
use Deployer\Task\Context;
use function Deployer\Support\rsync_rsh;
set('rsync', [
'exclude' => [
'.git',
'deploy.php',
],
'exclude-file' => false,
'include' => [],
'include-file' => false,
'filter' => [],
'filter-file' => false,
'filter-perdir' => false,
'flags' => 'rz',
'options' => ['delete'],
'timeout' => 300,
]);
set('rsync_src', __DIR__);
set('rsync_dest', '{{release_path}}');
set('rsync_excludes', function () {
$config = get('rsync');
$excludes = $config['exclude'];
$excludeFile = $config['exclude-file'];
$excludesRsync = '';
foreach ($excludes as $exclude) {
$excludesRsync .= ' --exclude=' . quote($exclude);
}
if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) {
$excludesRsync .= ' --exclude-from=' . quote($excludeFile);
}
return $excludesRsync;
});
set('rsync_includes', function () {
$config = get('rsync');
$includes = $config['include'];
$includeFile = $config['include-file'];
$includesRsync = '';
foreach ($includes as $include) {
$includesRsync .= ' --include=' . quote($include);
}
if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) {
$includesRsync .= ' --include-from=' . quote($includeFile);
}
return $includesRsync;
});
set('rsync_filter', function () {
$config = get('rsync');
$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;
});
set('rsync_options', function () {
$config = get('rsync');
$options = $config['options'];
$optionsRsync = [];
foreach ($options as $option) {
$optionsRsync[] = "--$option";
}
return implode(' ', $optionsRsync);
});
desc('Warmups remote Rsync target');
task('rsync:warmup', function () {
$config = get('rsync');
$source = "{{current_path}}";
$destination = "{{deploy_path}}/release";
if (test("[ -d $(echo $source) ]")) {
run("rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} $source/ $destination/");
} else {
writeln("<comment>No way to warmup rsync.</comment>");
}
});
desc('Rsync local->remote');
task('rsync', function () {
$config = get('rsync');
$src = get('rsync_src');
while (is_callable($src)) {
$src = $src();
}
if (!trim($src)) {
// 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.');
}
$dst = get('rsync_dest');
while (is_callable($dst)) {
$dst = $dst();
}
if (!trim($dst)) {
// 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.');
}
$rsyncFlags = (is_string($config['flags']) && trim($config['flags']) !== '') ? "-{$config['flags']}" : '';
$host = Context::get()->getHost();
if ($host instanceof Localhost) {
runLocally("rsync {$rsyncFlags} {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '$dst/'", timeout: $config['timeout']);
return;
}
$rsh = quote(rsync_rsh($host->connectionOptions()));
runLocally("rsync {$rsyncFlags} -e $rsh {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '{$host->connectionString()}:$dst/'", timeout: $config['timeout']);
});

View File

@ -0,0 +1,296 @@
<?php
/*
### Configuration options
- **organization** *(required)*: the slug of the organization the release belongs to.
- **projects** *(required)*: array of slugs of the projects to create a release for.
- **token** *(required)*: authentication token. Can be created at [https://sentry.io/settings/account/api/auth-tokens/]
- **version** *(required)* – a version identifier for this release.
Can be a version number, a commit hash etc. (Defaults is set to git log -n 1 --format="%h".)
- **version_prefix** *(optional)* - a string prefixed to version.
Releases are global per organization so indipentent projects needs to prefix version number with unique string to avoid conflicts
- **environment** *(optional)* - the environment you’re deploying to. By default framework's environment is used.
For example for symfony, *symfony_env* configuration is read otherwise defaults to 'prod'.
- **ref** *(optional)* – an optional commit reference. This is useful if a tagged version has been provided.
- **refs** *(optional)* - array to indicate the start and end commits for each repository included in a release.
Head commits must include parameters *repository* and *commit*) (the HEAD sha).
They can optionally include *previousCommit* (the sha of the HEAD of the previous release),
which should be specified if this is the first time you’ve sent commit data.
- **commits** *(optional)* - array commits data to be associated with the release.
Commits must include parameters *id* (the sha of the commit), and can optionally include *repository*,
*message*, *author_name*, *author_email* and *timestamp*. By default will send all new commits,
unless it's a first release, then only first 200 will be sent.
- **url** *(optional)* – a URL that points to the release. This can be the path to an online interface to the sourcecode for instance.
- **date_released** *(optional)* – date that indicates when the release went live. If not provided the current time is assumed.
- **sentry_server** *(optional)* – sentry server (if you host it yourself). defaults to hosted sentry service.
- **date_deploy_started** *(optional)* - date that indicates when the deploy started. Defaults to current time.
- **date_deploy_finished** *(optional)* - date that indicates when the deploy ended. If not provided, the current time is used.
- **deploy_name** *(optional)* - name of the deploy
- **git_version_command** *(optional)* - the command that retrieves the git version information (Defaults is set to git log -n 1 --format="%h", other options are git describe --tags --abbrev=0)
```php
// deploy.php
set('sentry', [
'organization' => 'exampleorg',
'projects' => [
'exampleproj'
],
'token' => 'd47828...',
'version' => '0.0.1',
]);
```
### Suggested Usage
Since you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end.
```php
// deploy.php
after('deploy', 'deploy:sentry');
```
*/
namespace Deployer;
use Closure;
use DateTime;
use Deployer\Exception\ConfigurationException;
use Deployer\Utility\Httpie;
desc('Notifies Sentry of deployment');
task(
'deploy:sentry',
static function () {
$now = date('c');
$defaultConfig = [
'version' => getReleaseGitRef(),
'version_prefix' => null,
'refs' => [],
'ref' => null,
'commits' => getGitCommitsRefs(),
'url' => null,
'date_released' => $now,
'date_deploy_started' => $now,
'date_deploy_finished' => $now,
'sentry_server' => 'https://sentry.io',
'previous_commit' => null,
'environment' => get('symfony_env', 'prod'),
'deploy_name' => null,
];
$config = array_merge($defaultConfig, (array) get('sentry'));
array_walk(
$config,
static function (&$value) use ($config) {
if (is_callable($value)) {
$value = $value($config);
}
},
);
if (
!isset($config['organization'], $config['token'], $config['version'])
|| (empty($config['projects']) || !is_array($config['projects']))
) {
throw new \RuntimeException(
<<<EXAMPLE
Required data missing. Please configure sentry:
set(
'sentry',
[
'organization' => 'exampleorg',
'projects' => [
'exampleproj',
'exampleproje2'
],
'token' => 'd47828...',
]
);"
EXAMPLE,
);
}
$releaseData = array_filter(
[
'version' => ($config['version_prefix'] ?? '') . $config['version'],
'refs' => $config['refs'],
'ref' => $config['ref'],
'url' => $config['url'],
'commits' => array_slice($config['commits'] ?? [], 0), // reset keys to serialize as array in json
'dateReleased' => $config['date_released'],
'projects' => $config['projects'],
'previousCommit' => $config['previous_commit'],
],
);
$releasesApiUrl = $config['sentry_server'] . '/api/0/organizations/' . $config['organization'] . '/releases/';
$response = Httpie::post(
$releasesApiUrl,
)
->setopt(CURLOPT_TIMEOUT, 10)
->header('Authorization', sprintf('Bearer %s', $config['token']))
->jsonBody($releaseData)
->getJson();
if (!isset($response['version'], $response['projects'])) {
throw new \RuntimeException(sprintf('Unable to create a release: %s', print_r($response, true)));
}
writeln(
sprintf(
'<info>Sentry:</info> Release of version <comment>%s</comment> '
. 'for projects: <comment>%s</comment> created successfully.',
$response['version'],
implode(', ', array_column($response['projects'], 'slug')),
),
);
$deployData = array_filter(
[
'environment' => $config['environment'],
'name' => $config['deploy_name'],
'url' => $config['url'],
'dateStarted' => $config['date_deploy_started'],
'dateFinished' => $config['date_deploy_finished'],
],
);
$response = Httpie::post(
$releasesApiUrl . $response['version'] . '/deploys/',
)
->setopt(CURLOPT_TIMEOUT, 10)
->header('Authorization', sprintf('Bearer %s', $config['token']))
->jsonBody($deployData)
->getJson();
if (!isset($response['id'], $response['environment'])) {
throw new \RuntimeException(sprintf('Unable to create a deployment: %s', print_r($response, true)));
}
writeln(
sprintf(
'<info>Sentry:</info> Deployment <comment>%s</comment> '
. 'for environment <comment>%s</comment> created successfully',
$response['id'],
$response['environment'],
),
);
},
);
function getPreviousReleaseRevision()
{
switch (get('update_code_strategy')) {
case 'local_archive':
case 'archive':
if (has('previous_release')) {
return run('cat {{previous_release}}/REVISION');
}
return null;
case 'clone':
if (has('previous_release')) {
cd('{{previous_release}}');
return trim(run('git rev-parse HEAD'));
}
return null;
default:
throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}."));
}
}
function getCurrentReleaseRevision()
{
switch (get('update_code_strategy')) {
case 'local_archive':
case 'archive':
return run('cat {{release_path}}/REVISION');
case 'clone':
cd('{{release_path}}');
return trim(run('git rev-parse HEAD'));
default:
throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}."));
}
}
function getReleaseGitRef(): Closure
{
return static function ($config = []): string {
$strategy = get('update_code_strategy');
if ($strategy === 'archive') {
cd('{{deploy_path}}/.dep/repo');
} else {
cd('{{release_path}}');
}
if (isset($config['git_version_command'])) {
return trim(run($config['git_version_command']));
}
if ($strategy !== 'clone') {
return run('cat {{current_path}}/REVISION');
}
return trim(run('git log -n 1 --format="%h"'));
};
}
function getGitCommitsRefs(): Closure
{
return static function ($config = []): array {
$previousReleaseRevision = getPreviousReleaseRevision();
$currentReleaseRevision = getCurrentReleaseRevision() ?: 'HEAD';
if ($previousReleaseRevision === null) {
$commitRange = $currentReleaseRevision;
} else {
$commitRange = $previousReleaseRevision . '..' . $currentReleaseRevision;
}
try {
if (get('update_code_strategy') === 'archive') {
cd('{{deploy_path}}/.dep/repo');
} else {
cd('{{release_path}}');
}
$result = run(sprintf('git rev-list --pretty="%s" %s', 'format:%H#%an#%ae#%at#%s', $commitRange));
$lines = array_filter(
// limit number of commits for first release with many commits
array_map('trim', array_slice(explode("\n", $result), 0, 200)),
static function (string $line): bool {
return !empty($line) && strpos($line, 'commit') !== 0;
},
);
return array_map(
static function (string $line): array {
[$ref, $authorName, $authorEmail, $timestamp, $message] = explode('#', $line, 5);
return [
'id' => $ref,
'author_name' => $authorName,
'author_email' => $authorEmail,
'message' => $message,
'timestamp' => date(\DateTime::ATOM, (int) $timestamp),
];
},
$lines,
);
} catch (\Deployer\Exception\RunException $e) {
writeln($e->getMessage());
return [];
}
};
}

View File

@ -0,0 +1,178 @@
<?php
/*
## Installing
<a href="https://slack.com/oauth/authorize?&client_id=113734341365.225973502034&scope=incoming-webhook"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
Add hook on deploy:
```php
before('deploy', 'slack:notify');
```
## Configuration
- `slack_webhook` – slack incoming webhook url, **required**
```
set('slack_webhook', 'https://hooks.slack.com/...');
```
- `slack_channel` - channel to send notification to. The default is the channel configured in the webhook
- `slack_title` – the title of application, default `{{application}}`
- `slack_text` – notification message template, markdown supported
```
set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
```
- `slack_success_text` – success template, default:
```
set('slack_success_text', 'Deploy to *{{where}}* successful');
```
- `slack_failure_text` – failure template, default:
```
set('slack_failure_text', 'Deploy to *{{where}}* failed');
```
- `slack_color` – color's attachment
- `slack_success_color` – success color's attachment
- `slack_failure_color` – failure color's attachment
- `slack_fields` - set attachments fields for pretty output in Slack, default:
```
set('slack_fields', []);
```
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'slack:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'slack:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'slack:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Channel to publish to, when false the default channel the webhook will be used
set('slack_channel', false);
// Title of project
set('slack_title', function () {
return get('application', 'Project');
});
// Deploy message
set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('slack_success_text', 'Deploy to *{{where}}* successful');
set('slack_failure_text', 'Deploy to *{{where}}* failed');
set('slack_rollback_text', '_{{user}}_ rolled back changes on *{{where}}*');
set('slack_fields', []);
// Color of attachment
set('slack_color', '#4d91f7');
set('slack_success_color', '#00c100');
set('slack_failure_color', '#ff0909');
set('slack_rollback_color', '#eba211');
function checkSlackAnswer($result)
{
if ('invalid_token' === $result) {
warning('Invalid Slack token');
return false;
}
return true;
}
desc('Notifies Slack');
task('slack:notify', function () {
if (!get('slack_webhook', false)) {
warning('No Slack webhook configured');
return;
}
$attachment = [
'title' => get('slack_title'),
'text' => get('slack_text'),
'color' => get('slack_color'),
'mrkdwn_in' => ['text'],
];
$result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();
checkSlackAnswer($result);
})
->once()
->hidden();
desc('Notifies Slack about deploy finish');
task('slack:notify:success', function () {
if (!get('slack_webhook', false)) {
warning('No Slack webhook configured');
return;
}
$attachment = [
'title' => get('slack_title'),
'text' => get('slack_success_text'),
'color' => get('slack_success_color'),
'fields' => get('slack_fields'),
'mrkdwn_in' => ['text'],
];
$result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();
checkSlackAnswer($result);
})
->once()
->hidden();
desc('Notifies Slack about deploy failure');
task('slack:notify:failure', function () {
if (!get('slack_webhook', false)) {
warning('No Slack webhook configured');
return;
}
$attachment = [
'title' => get('slack_title'),
'text' => get('slack_failure_text'),
'color' => get('slack_failure_color'),
'mrkdwn_in' => ['text'],
];
$result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();
checkSlackAnswer($result);
})
->once()
->hidden();
desc('Notifies Slack about rollback');
task('slack:notify:rollback', function () {
if (!get('slack_webhook', false)) {
warning('No Slack webhook configured');
return;
}
$attachment = [
'title' => get('slack_title'),
'text' => get('slack_rollback_text'),
'color' => get('slack_rollback_color'),
'mrkdwn_in' => ['text'],
];
$result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();
checkSlackAnswer($result);
})
->once()
->hidden();

View File

@ -0,0 +1,177 @@
<?php
/*
### Description
This is a recipe that uses the [Supervisord server monitoring project](https://github.com/mlazarov/supervisord-monitor).
With this recipe the possibility is created to restart a supervisord process through the Supervisor Monitor webtool, by using cURL. This workaround is particular usefull when the deployment user has unsuficient rights to restart a daemon process from the cli.
### Configuration
```
set('supervisord', [
'uri' => 'https://youruri.xyz/supervisor',
'basic_auth_user' => 'username',
'basic_auth_password' => 'password',
'process_name' => 'process01',
]);
```
or
```
set('supervisord_uri', 'https://youruri.xyz/supervisor');
set('supervisord_basic_auth_user', 'username');
set('supervisord_basic_auth_password', 'password');
set('supervisord_process_name', 'process01');
```
- `supervisord` – array with configuration for Supervisord
- `uri` – URI to the Supervisord monitor page
- `basic_auth_user` – Basic auth username to access the URI
- `basic_auth_password` – Basic auth password to access the URI
- `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated
### Task
- `supervisord-monitor:restart` Restarts given processes
- `supervisord-monitor:stop` Stops given processes
- `supervisord-monitor:start` Starts given processes
### Usage
A complete example with configs, staging and deployment
```
<?php
namespace Deployer;
use Dotenv\Dotenv;
require 'vendor/autoload.php';
require 'supervisord_monitor.php';
// Project name
set('application', 'myproject.com');
// Project repository
set('repository', 'git@github.com:myorg/myproject.com');
set('supervisord', [
'uri' => 'https://youruri.xyz/supervisor',
'basic_auth_user' => 'username',
'basic_auth_password' => 'password',
'process_name' => 'process01',
]);
host('staging.myproject.com')
->set('branch', 'develop')
->set('labels', ['stage' => 'staging']);
host('myproject.com')
->set('branch', 'main')
->set('labels', ['stage' => 'production']);
// Tasks
task('build', function () {
run('cd {{release_path}} && build');
});
task('deploy', [
'build',
'supervisord',
]);
task('supervisord', ['supervisord-monitor:restart'])
->select('stage=production');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
function supervisordCheckConfig()
{
$config = get('supervisord', []);
foreach ($config as $key => $value) {
if ($value) {
set('supervisord_' . $key, $value);
}
}
if (!get('supervisord_uri')
|| !get('supervisord_basic_auth_user')
|| !get('supervisord_basic_auth_password')
|| !get('supervisord_process_name')) {
throw new \RuntimeException("<comment>Please configure Supervisord config:</comment> <info>set('supervisord', array('uri' => 'yourdomain.xyz/supervisor', 'basic_auth_user' => 'abc' , 'basic_auth_password' => 'xyz', 'process_name' => 'process01,process02'));</info> or <info>set('supervisord_uri', 'yourdomain.xyz/supervisor'); set('supervisord_basic_auth_user', 'abc'); etc</info>");
}
}
function supervisordGetBasicAuthToken()
{
return 'Basic ' . base64_encode(get('supervisord_basic_auth_user') . ':' . get('supervisord_basic_auth_password'));
}
function supervisordIsAuthenticated()
{
supervisordCheckConfig();
$authResponseInfo = [];
Httpie::post(get('supervisord_uri'))->header('Authorization', supervisordGetBasicAuthToken())->send($authResponseInfo);
return $authResponseInfo['http_code'] === 200;
}
function supervisordControlAction($name, $action = 'stop')
{
$stopResponseInfo = [];
Httpie::post(get('supervisord_uri') . '/control/' . $action . '/localhost/' . $name)->header('Authorization', supervisordGetBasicAuthToken())->send($stopResponseInfo);
return $stopResponseInfo['http_code'] === 200;
}
task('supervisord-monitor:restart', function () {
if (supervisordIsAuthenticated()) {
$names = explode(',', get('supervisord_process_name'));
foreach ($names as $name) {
$name = trim($name);
if (supervisordControlAction($name, 'stop')) {
writeln('Daemon [' . $name . '] stopped');
if (supervisordControlAction($name, 'start')) {
writeln('Daemon [' . $name . '] started');
}
}
}
} else {
writeln('Authentication failed');
}
});
task('supervisord-monitor:stop', function () {
if (supervisordIsAuthenticated()) {
$names = explode(',', get('supervisord_process_name'));
foreach ($names as $name) {
$name = trim($name);
if (supervisordControlAction($name, 'stop')) {
writeln('Daemon [' . $name . '] stopped');
}
}
} else {
writeln('Authentication failed');
}
});
task('supervisord-monitor:start', function () {
if (supervisordIsAuthenticated()) {
$names = explode(',', get('supervisord_process_name'));
foreach ($names as $name) {
$name = trim($name);
if (supervisordControlAction($name, 'start')) {
writeln('Daemon [' . $name . '] started');
}
}
} else {
writeln('Authentication failed');
}
});

View File

@ -0,0 +1,177 @@
<?php
/*
## Installing
1. Create telegram bot with [BotFather](https://t.me/BotFather) and grab the token provided
2. Send `/start` to your bot and open https://api.telegram.org/bot{$TELEGRAM_TOKEN_HERE}/getUpdates
3. Take chat_id from response
Add hook on deploy:
```php
before('deploy', 'telegram:notify');
```
## Configuration
- `telegram_token` – telegram bot token, **required**
- `telegram_chat_id` — chat ID to push messages to
- `telegram_proxy` - proxy connection string in [CURLOPT_PROXY](https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html) form like:
```
http://proxy:80
socks5://user:password@host:3128
```
- `telegram_title` – the title of application, default `{{application}}`
- `telegram_text` – notification message template
```
_{{user}}_ deploying `{{what}}` to *{{where}}*
```
- `telegram_success_text` – success template, default:
```
Deploy to *{{where}}* successful
```
- `telegram_failure_text` – failure template, default:
```
Deploy to *{{where}}* failed
```
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'telegram:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'telegram:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'telegram:notify:failure');
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Title of project
set('telegram_title', function () {
return get('application', 'Project');
});
// Telegram settings
set('telegram_token', function () {
throw new \Exception('Please, configure "telegram_token" parameter.');
});
set('telegram_chat_id', function () {
throw new \Exception('Please, configure "telegram_chat_id" parameter.');
});
set('telegram_url', function () {
return 'https://api.telegram.org/bot' . get('telegram_token') . '/sendmessage';
});
// Deploy message
set('telegram_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('telegram_success_text', 'Deploy to *{{where}}* successful');
set('telegram_failure_text', 'Deploy to *{{where}}* failed');
desc('Notifies Telegram');
task('telegram:notify', function () {
if (!get('telegram_token', false)) {
warning('No Telegram token configured');
return;
}
if (!get('telegram_chat_id', false)) {
warning('No Telegram chat id configured');
return;
}
$telegramUrl = get('telegram_url') . '?' . http_build_query(
[
'chat_id' => get('telegram_chat_id'),
'text' => get('telegram_text'),
'parse_mode' => 'Markdown',
],
);
$httpie = Httpie::get($telegramUrl);
if (get('telegram_proxy', '') !== '') {
$httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));
}
$httpie->send();
})
->once()
->hidden();
desc('Notifies Telegram about deploy finish');
task('telegram:notify:success', function () {
if (!get('telegram_token', false)) {
warning('No Telegram token configured');
return;
}
if (!get('telegram_chat_id', false)) {
warning('No Telegram chat id configured');
return;
}
$telegramUrl = get('telegram_url') . '?' . http_build_query(
[
'chat_id' => get('telegram_chat_id'),
'text' => get('telegram_success_text'),
'parse_mode' => 'Markdown',
],
);
$httpie = Httpie::get($telegramUrl);
if (get('telegram_proxy', '') !== '') {
$httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));
}
$httpie->send();
})
->once()
->hidden();
desc('Notifies Telegram about deploy failure');
task('telegram:notify:failure', function () {
if (!get('telegram_token', false)) {
warning('No Telegram token configured');
return;
}
if (!get('telegram_chat_id', false)) {
warning('No Telegram chat id configured');
return;
}
$telegramUrl = get('telegram_url') . '?' . http_build_query(
[
'chat_id' => get('telegram_chat_id'),
'text' => get('telegram_failure_text'),
'parse_mode' => 'Markdown',
],
);
$httpie = Httpie::get($telegramUrl);
if (get('telegram_proxy', '') !== '') {
$httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));
}
$httpie->send();
})
->once()
->hidden();

View File

@ -0,0 +1,44 @@
<?php
/*
## Configuration
- **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used.
## Usage
```php
// For Yarn
after('deploy:update_code', 'yarn:install');
// For npm
after('deploy:update_code', 'npm:install');
after('deploy:update_code', 'webpack_encore:build');
```
*/
namespace Deployer;
require_once __DIR__ . '/npm.php';
require_once __DIR__ . '/yarn.php';
set('webpack_encore/package_manager', function () {
if (test('[ -f {{release_path}}/yarn.lock ]')) {
return 'yarn';
}
return 'npm';
});
set('webpack_encore/env', 'production');
desc('Runs webpack encore build');
task('webpack_encore:build', function () {
$packageManager = get('webpack_encore/package_manager');
if (!in_array($packageManager, ['npm', 'yarn'], true)) {
throw new \Exception(sprintf('Package Manager "%s" is not supported', $packageManager));
}
run("cd {{release_path}} && {{bin/$packageManager}} run encore {{webpack_encore/env}}");
});

View File

@ -0,0 +1,122 @@
<?php
/*
This recipes works with Custom Integrations and Publishing Bots.
Add hook on deploy:
```
before('deploy', 'workplace:notify');
```
## Configuration
- `workplace_webhook` - incoming workplace webhook **required**
```
// With custom integration
set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>');
// With publishing bot
set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token=<ACCESS_TOKEN>');
// Use markdown on message
set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>&formatting=MARKDOWN');
```
- `workplace_text` - notification message
```
set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
```
- `workplace_success_text` – success template, default:
```
set('workplace_success_text', 'Deploy to *{{where}}* successful');
```
- `workplace_failure_text` – failure template, default:
```
set('workplace_failure_text', 'Deploy to *{{where}}* failed');
```
- `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post:
```
set('workplace_edit_post', false);
```
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'workplace:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'workplace:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'workplace:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
// Deploy message
set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');
set('workplace_success_text', 'Deploy to *{{where}}* successful');
set('workplace_failure_text', 'Deploy to *{{where}}* failed');
// By default, create a new post for every message
set('workplace_edit_post', false);
desc('Notifies Workplace');
task('workplace:notify', function () {
if (!get('workplace_webhook', false)) {
return;
}
$url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_text'));
$response = Httpie::post($url)->getJson();
if (get('workplace_edit_post', false)) {
// Endpoint will be something like: https//graph.facebook.com/<POST_ID>?<QUERY_PARAMS>
$url = sprintf(
'%s://%s/%s?%s',
parse_url(get('workplace_webhook'), PHP_URL_SCHEME),
parse_url(get('workplace_webhook'), PHP_URL_HOST),
$response['id'],
parse_url(get('workplace_webhook'), PHP_URL_QUERY),
);
// Replace the webhook with a url that points to the created post
set('workplace_webhook', $url);
}
})
->once()
->hidden();
desc('Notifies Workplace about deploy finish');
task('workplace:notify:success', function () {
if (!get('workplace_webhook', false)) {
return;
}
$url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_success_text'));
Httpie::post($url)->send();
})
->once()
->hidden();
desc('Notifies Workplace about deploy failure');
task('workplace:notify:failure', function () {
if (!get('workplace_webhook', false)) {
return;
}
$url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_failure_text'));
Httpie::post($url)->send();
})
->once()
->hidden();

View File

@ -0,0 +1,122 @@
<?php
/*
Add hook on deploy:
```php
before('deploy', 'yammer:notify');
```
## Configuration
- `yammer_url` – The URL to the message endpoint, default is https://www.yammer.com/api/v1/messages.json
- `yammer_token` *(required)* – Yammer auth token
- `yammer_group_id` *(required)* - Group ID
- `yammer_title` – the title of application, default `{{application}}`
- `yammer_body` – notification message template, default:
```
<em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>
```
- `yammer_success_body` – success template, default:
```
Deploy to <strong>{{where}}</strong> successful
```
- `yammer_failure_body` – failure template, default:
```
Deploy to <strong>{{where}}</strong> failed
```
## Usage
If you want to notify only about beginning of deployment add this line only:
```php
before('deploy', 'yammer:notify');
```
If you want to notify about successful end of deployment add this too:
```php
after('deploy:success', 'yammer:notify:success');
```
If you want to notify about failed deployment add this too:
```php
after('deploy:failed', 'yammer:notify:failure');
```
*/
namespace Deployer;
use Deployer\Utility\Httpie;
set('yammer_url', 'https://www.yammer.com/api/v1/messages.json');
// Title of project
set('yammer_title', function () {
return get('application', 'Project');
});
// Deploy message
set('yammer_body', '<em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>');
set('yammer_success_body', 'Deploy to <strong>{{where}}</strong> successful');
set('yammer_failure_body', 'Deploy to <strong>{{where}}</strong> failed');
desc('Notifies Yammer');
task('yammer:notify', function () {
$params = [
'is_rich_text' => 'true',
'message_type' => 'announcement',
'group_id' => get('yammer_group_id'),
'title' => get('yammer_title'),
'body' => get('yammer_body'),
];
Httpie::post(get('yammer_url'))
->header('Authorization', 'Bearer ' . get('yammer_token'))
->header('Content-type', 'application/json')
->jsonBody($params)
->send();
})
->once()
->hidden();
desc('Notifies Yammer about deploy finish');
task('yammer:notify:success', function () {
$params = [
'is_rich_text' => 'true',
'message_type' => 'announcement',
'group_id' => get('yammer_group_id'),
'title' => get('yammer_title'),
'body' => get('yammer_success_body'),
];
Httpie::post(get('yammer_url'))
->header('Authorization', 'Bearer ' . get('yammer_token'))
->header('Content-type', 'application/json')
->jsonBody($params)
->send();
})
->once()
->hidden();
desc('Notifies Yammer about deploy failure');
task('yammer:notify:failure', function () {
$params = [
'is_rich_text' => 'true',
'message_type' => 'announcement',
'group_id' => get('yammer_group_id'),
'title' => get('yammer_title'),
'body' => get('yammer_failure_body'),
];
Httpie::post(get('yammer_url'))
->header('Authorization', 'Bearer ' . get('yammer_token'))
->header('Content-type', 'application/json')
->jsonBody($params)
->send();
})
->once()
->hidden();

View File

@ -0,0 +1,29 @@
<?php
/*
## Configuration
- **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise.
## Usage
```php
after('deploy:update_code', 'yarn:install');
after('yarn:install', 'yarn:build');
```
*/
namespace Deployer;
set('bin/yarn', function () {
return which('yarn');
});
desc('Installs Yarn packages');
task('yarn:install', function () {
run('cd {{release_path}} && {{bin/yarn}}');
});
desc('Runs Yarn build');
task('yarn:build', function () {
run('cd {{release_path}} && {{bin/yarn}} build');
});

View File

@ -0,0 +1,49 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['cakephp']);
/**
* CakePHP 4 Project Template configuration
*/
// CakePHP 4 Project Template shared dirs
set('shared_dirs', [
'logs',
'tmp',
]);
// CakePHP 4 Project Template shared files
set('shared_files', [
'config/.env',
'config/app.php',
]);
/**
* Create plugins' symlinks
*/
task('deploy:init', function () {
run('{{bin/php}} {{release_or_current_path}}/bin/cake.php plugin assets symlink');
})->desc('Initialization');
/**
* Run migrations
*/
task('deploy:run_migrations', function () {
run('{{bin/php}} {{release_or_current_path}}/bin/cake.php migrations migrate --no-lock');
run('{{bin/php}} {{release_or_current_path}}/bin/cake.php schema_cache build');
})->desc('Run migrations');
/**
* Main task
*/
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:init',
'deploy:run_migrations',
'deploy:publish',
])->desc('Deploy your project');

View File

@ -0,0 +1,23 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['codeigniter']);
// CodeIgniter shared dirs
set('shared_dirs', ['application/cache', 'application/logs']);
// CodeIgniter writable dirs
set('writable_dirs', ['application/cache', 'application/logs']);
/**
* Main task
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:publish',
]);

View File

@ -0,0 +1,190 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['codeigniter4']);
// Default Configurations
set('public_path', 'public');
set('shared_dirs', ['writable']);
set('shared_files', ['.env']);
set('writable_dirs', [
'writable/cache',
'writable/debugbar',
'writable/logs',
'writable/session',
'writable/uploads',
]);
set('log_files', 'writable/logs/*.log');
set('codeigniter4_version', function () {
$result = run('{{bin/php}} {{release_or_current_path}}/spark');
preg_match_all('/(\d+\.?)+/', $result, $matches);
return $matches[0][0] ?? 5.5;
});
/**
* Run an spark command.
*
* Supported options:
* - 'min' => #.#: The minimum Codeigniter4 version required (included).
* - 'max' => #.#: The maximum Codeigniter4 version required (included).
* - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistent or empty.
* - 'failIfNoEnv': Fail the command if `.env` file is inexistent or empty.
* - 'showOutput': Show the output of the command if given.
*
* @param string $command The spark command (with cli options if any).
* @param array $options The options that define the behavior of the command.
* @return callable A function that can be used as a task.
*/
function spark($command, $options = [])
{
return function () use ($command, $options) {
// Ensure the spark command is available on the current version.
$versionTooEarly = array_key_exists('min', $options)
&& codeigniter4_version_compare($options['min'], '<');
$versionTooLate = array_key_exists('max', $options)
&& codeigniter4_version_compare($options['max'], '>');
if ($versionTooEarly || $versionTooLate) {
return;
}
// Ensure we warn or fail when a command relies on the ".env" file.
if (in_array('failIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) {
throw new \Exception('Your .env file is empty! Cannot proceed.');
}
if (in_array('skipIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) {
warning("Your .env file is empty! Skipping...</>");
return;
}
$spark = '{{release_or_current_path}}/spark';
// Run the spark command.
$output = run("{{bin/php}} $spark $command");
// Output the results when appropriate.
if (in_array('showOutput', $options)) {
writeln("<info>$output</info>");
}
};
}
function codeigniter4_version_compare($version, $comparator)
{
return version_compare(get('codeigniter4_version'), $version, $comparator);
}
/**
* Discover & Checks
*/
desc('Shows file cache information in the current system.');
task('spark:cache:info', spark('cache:info', ['showOutput']));
desc('Check your Config values.');
task('spark:config:check', spark('config:check', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0']));
desc('Retrieves the current environment, or set a new one.');
task('spark:env', spark('env', ['skipIfNoEnv', 'showOutput']));
desc('Check filters for a route.');
task('spark:filter:check', spark('filter:check', ['showOutput', 'min' => '4.3.0']));
desc('Find and save available phrases to translate.');
task('spark:lang:find', spark('lang:find', ['showOutput', 'min' => '4.5.0']));
desc('Verifies your namespaces are setup correctly.');
task('spark:namespaces', spark('namespaces', ['showOutput']));
desc('Check your php.ini values.');
task('spark:phpini:check', spark('phpini:check', ['showOutput', 'min' => '4.5.0']));
desc('Displays all routes.');
task('spark:routes', spark('routes', ['showOutput', 'min' => '4.3.0']));
/**
* Actions
*/
desc('Generates a new encryption key and writes it in an `.env` file.');
task('spark:key:generate', spark('key:generate', ['skipIfNoEnv']));
desc('Optimize for production.');
task('spark:optimize', spark('optimize', ['min' => '4.5.0']));
desc('Discovers and executes all predefined Publisher classes.');
task('spark:publish', spark('publish', ['skipIfNoEnv', 'showOutput']));
/*
* Database and migrations.
*/
desc('Create a new database schema.');
task('spark:db:create', spark('db:create', ['showOutput']));
desc('Runs the specified seeder to populate known data into the database.');
task('spark:db:seed', spark('db:seed', ['skipIfNoEnv']));
desc('Retrieves information on the selected table.');
task('spark:db:table', spark('db:table', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0']));
desc('Locates and runs all new migrations against the database.');
task('spark:migrate', spark('migrate --all', ['skipIfNoEnv']));
desc('Does a rollback followed by a latest to refresh the current state of the database.');
task('spark:migrate:refresh', spark('migrate:refresh -f --all', ['skipIfNoEnv']));
desc('Runs the "down" method for all migrations in the last batch.');
task('spark:migrate:rollback', spark('migrate:rollback -f', ['skipIfNoEnv', 'showOutput']));
desc('Displays a list of all migrations and whether they\'ve been run or not.');
task('spark:migrate:status', spark('migrate:status', ['skipIfNoEnv', 'showOutput']));
/**
* Housekeeping
*/
desc('Clears the current system caches.');
task('spark:cache:clear', spark('cache:clear'));
desc('Clears all debugbar JSON files.');
task('spark:debugbar:clear', spark('debugbar:clear'));
desc('Clears all log files.');
task('spark:logs:clear', spark('logs:clear'));
/**
* Custom Spark Command for shield or setting packages
*/
desc('Run a custom spark command.');
task('spark:custom', spark('', ['showOutput']));
/**
* Main deploy task.
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'spark:optimize',
'spark:migrate',
'deploy:publish',
]);

View File

@ -0,0 +1,199 @@
<?php
namespace Deployer;
require __DIR__ . '/provision.php';
require __DIR__ . '/deploy/check_remote.php';
require __DIR__ . '/deploy/cleanup.php';
require __DIR__ . '/deploy/clear_paths.php';
require __DIR__ . '/deploy/copy_dirs.php';
require __DIR__ . '/deploy/env.php';
require __DIR__ . '/deploy/info.php';
require __DIR__ . '/deploy/lock.php';
require __DIR__ . '/deploy/push.php';
require __DIR__ . '/deploy/release.php';
require __DIR__ . '/deploy/rollback.php';
require __DIR__ . '/deploy/setup.php';
require __DIR__ . '/deploy/shared.php';
require __DIR__ . '/deploy/symlink.php';
require __DIR__ . '/deploy/update_code.php';
require __DIR__ . '/deploy/vendors.php';
require __DIR__ . '/deploy/writable.php';
use Deployer\Exception\ConfigurationException;
use Deployer\Exception\RunException;
add('recipes', ['common']);
// Name of current user who is running deploy.
// If not set will try automatically get git user name,
// otherwise output of `whoami` command.
set('user', function () {
if (getenv('CI') !== false) {
$ciUserVars = ['GITLAB_USER_NAME', 'GITHUB_ACTOR', 'CIRCLE_USERNAME', 'DRONE_BUILD_TRIGGER'];
foreach ($ciUserVars as $var) {
if (($ciUser = getenv($var)) !== false) {
return $ciUser;
}
}
return 'ci';
}
try {
return runLocally('git config --get user.name');
} catch (RunException $exception) {
try {
return runLocally('whoami');
} catch (RunException $exception) {
return 'no_user';
}
}
});
// Number of releases to preserve in releases folder.
set('keep_releases', 10);
// Repository to deploy.
set('repository', '');
// Default timeout for `run()` and `runLocally()` functions.
//
// Set to `null` to disable timeout.
set('default_timeout', 300);
/**
* Remote environment variables.
* ```php
* set('env', [
* 'KEY' => 'something',
* ]);
* ```
*
* It is possible to override it per `run()` call.
*
* ```php
* run('echo $KEY', env: ['KEY' => 'over']);
* ```
*/
set('env', []);
/**
* Path to `.env` file which will be used as environment variables for each command per `run()`.
*
* ```php
* set('dotenv', '{{release_or_current_path}}/.env');
* ```
*/
set('dotenv', false);
/**
* The deploy path.
*
* For example can be set for a bunch of host once as:
* ```php
* set('deploy_path', '~/{{alias}}');
* ```
*/
set('deploy_path', function () {
throw new ConfigurationException('Please, specify `deploy_path`.');
});
/**
* Return current release path. Default to {{deploy_path}}/`current`.
* ```php
* set('current_path', '/var/public_html');
* ```
*/
set('current_path', '{{deploy_path}}/current');
// Path to the `php` bin.
set('bin/php', function () {
if (currentHost()->hasOwn('php_version')) {
return '/usr/bin/php{{php_version}}';
}
return which('php');
});
// Path to the `git` bin.
set('bin/git', function () {
return which('git');
});
// Should {{bin/symlink}} use `--relative` option or not. Will detect
// automatically.
set('use_relative_symlink', function () {
return commandSupportsOption('ln', '--relative');
});
// Path to the `ln` bin. With predefined options `-nfs`.
set('bin/symlink', function () {
return get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs';
});
// Path to a file which will store temp script with sudo password.
// Defaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after
// sudo command executed.
set('sudo_askpass', function () {
if (test('[ -d {{deploy_path}}/.dep ]')) {
return '{{deploy_path}}/.dep/sudo_pass';
} else {
return '/tmp/dep_sudo_pass';
}
});
desc('Prepares a new release');
task('deploy:prepare', [
'deploy:info',
'deploy:setup',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'deploy:env',
'deploy:shared',
'deploy:writable',
]);
desc('Publishes the release');
task('deploy:publish', [
'deploy:symlink',
'deploy:unlock',
'deploy:cleanup',
'deploy:success',
]);
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:publish',
]);
/**
* Prints success message
*/
task('deploy:success', function () {
info('successfully deployed!');
})
->hidden();
/**
* Hook on deploy failure.
*/
task('deploy:failed', function () {})
->hidden();
fail('deploy', 'deploy:failed');
/**
* Follows latest application logs.
*/
desc('Shows application logs');
task('logs:app', function () {
if (!has('log_files')) {
warning("Please, specify \"log_files\" option.");
return;
}
cd('{{current_path}}');
run('tail -f {{log_files}}');
})->verbose();

View File

@ -0,0 +1,14 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['composer']);
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:publish',
]);

View File

@ -0,0 +1,114 @@
<?php
namespace Deployer;
require_once __DIR__ . '/symfony.php';
add('recipes', ['contao']);
// The public path is the path to be set as DocumentRoot and is defined in the `composer.json` of the project
// but defaults to `public` from Contao 5.0 on.
// This path is relative from the {{current_path}}, see [`recipe/provision/website.php`](/docs/recipe/provision/website.php#public_path).
set('public_path', function () {
$composerConfig = json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR);
return $composerConfig['extra']['public-dir'] ?? 'public';
});
add('shared_files', ['config/parameters.yml']);
add('shared_dirs', [
'assets/images',
'contao-manager',
'files',
'{{public_path}}/share',
'system/config',
'var/backups',
'var/logs',
]);
set('bin/console', function () {
return '{{bin/php}} {{release_or_current_path}}/vendor/bin/contao-console';
});
set('contao_version', function () {
$result = run('{{bin/console}} --version');
preg_match_all('/(\d+\.?)+/', $result, $matches);
return $matches[0][0] ?? 'n/a';
});
set('symfony_version', function () {
$result = run('{{bin/console}} about');
preg_match_all('/(\d+\.?)+/', $result, $matches);
return $matches[0][0] ?? 5.0;
});
// This task updates the database. A database backup is saved automatically as a default.
//
// To automatically drop the obsolete database structures, you can override the task as follows:
//
// ```php
// task('contao:migrate', function () {
// run('{{bin/php}} {{bin/console}} contao:migrate --with-deletes {{console_options}}');
// });
// ```
desc('Run Contao migrations');
task('contao:migrate', function () {
run('{{bin/console}} contao:migrate {{console_options}}');
});
// Downloads the `contao-manager.phar.php` into the public path.
desc('Download the Contao Manager');
task('contao:manager:download', function () {
run('curl -LsO https://download.contao.org/contao-manager/stable/contao-manager.phar && mv contao-manager.phar {{release_or_current_path}}/{{public_path}}/contao-manager.phar.php');
});
// Locks the Contao install tool which is useful if you don't use it.
desc('Lock the Contao Install Tool');
task('contao:install:lock', function () {
run('{{bin/console}} contao:install:lock {{console_options}}');
});
// Locks the Contao Manager which is useful if you only need the API of the Manager rather than the UI.
desc('Lock the Contao Manager');
task('contao:manager:lock', function () {
cd('{{release_or_current_path}}');
run('echo "99" > contao-manager/login.lock');
});
desc('Enable maintenance mode');
task('contao:maintenance:enable', function () {
// Enable maintenance mode in both the current and release build, so that the maintenance mode will be enabled
// for the current installation before the symlink changes and the new installation after the symlink changed.
foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) {
// The current path might not be present during first deploy.
if (!test("[ -d $path ]")) {
continue;
}
cd($path);
run('{{bin/console}} contao:maintenance-mode enable {{console_options}}');
}
});
desc('Disable maintenance mode');
task('contao:maintenance:disable', function () {
foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) {
if (!test("[ -d $path ]")) {
continue;
}
cd($path);
run('{{bin/console}} contao:maintenance-mode disable {{console_options}}');
}
});
desc('Deploy the project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'contao:maintenance:enable',
'contao:migrate',
'contao:maintenance:disable',
'deploy:publish',
]);

View File

@ -0,0 +1,136 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['craftcms']);
set('log_files', 'storage/logs/*.log');
set('shared_dirs', [
'storage',
'web/assets',
]);
set('shared_files', ['.env']);
set('writable_dirs', [
'config/project',
'storage',
'web/assets',
'web/cpresources',
]);
/**
* Run a craft command.
*
* Supported options:
* - 'showOutput': Show the output of the command if given.
* - 'interactive': Don't append the --interactive=0 flag to the command.
*
* @param string $command The craft command (with cli options if any).
* @param array $options The options that define the behaviour of the command.
*
* @return callable A function that can be used as a task.
*/
function craft($command, $options = [])
{
return function () use ($command, $options) {
if (! test('[ -s {{release_path}}/.env ]')) {
throw new \Exception('Your .env file is empty! Cannot proceed.');
}
// By default we don't want any command to be interactive
if (! in_array('interactive', $options)) {
$command .= ' --interactive=0';
}
$output = run("{{bin/php}} {{release_path}}/craft $command");
if (in_array('showOutput', $options)) {
writeln("<info>$output</info>");
}
};
}
/*
* Migrations
*/
desc('Runs all pending Craft, plugin, and content migrations');
task('craft:migrate/all', craft('migrate/all'));
desc('Upgrades Craft by applying new migrations');
task('craft:migrate/up', craft('migrate/up'));
/*
* Generate keys
*/
desc('Generates a new application ID and saves it in the `.env` file');
task('craft:setup/app-id', craft('setup/app-id'));
desc('Generates a new security key and saves it in the `.env` file');
task('craft:setup/security-key', craft('setup/security-key'));
/*
* Project config
*/
desc('Applies project config file changes.');
task('craft:project-config/apply', craft('project-config/apply'));
/*
* Caches
*/
desc('Flushes all caches registered in the system');
task('craft:cache/flush-all', craft('cache/flush-all'));
desc('Clear all caches');
task('craft:clear-caches/all', craft('clear-caches/all'));
desc('Clear all Asset caches');
task('craft:clear-caches/asset', craft('clear-caches/asset'));
desc('Clear all Asset indexing data');
task('craft:clear-caches/asset-indexing-data', craft('clear-caches/asset-indexing-data'));
desc('Clear all compiled classes');
task('craft:clear-caches/compiled-classes', craft('clear-caches/compiled-classes'));
desc('Clear all compiled templates');
task('craft:clear-caches/compiled-templates', craft('clear-caches/compiled-templates'));
desc('Clear all control panel resources');
task('craft:clear-caches/cp-resources', craft('clear-caches/cp-resources'));
desc('Clear all data caches');
task('craft:clear-caches/data', craft('clear-caches/data'));
desc('Clear all temp files');
task('craft:clear-caches/temp-files', craft('clear-caches/temp-files'));
/*
* Garbage collection
*/
desc('Runs garbage collection');
task('craft:gc', craft('gc --delete-all-trashed=1 --silent-exit-on-exception=1', ['showOutput']));
/*
* Main deploy
*/
desc('Deploys Craft CMS');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'craft:clear-caches/compiled-classes',
'craft:migrate/all',
'craft:project-config/apply',
'craft:gc',
'craft:clear-caches/all',
'deploy:publish',
]);

View File

@ -0,0 +1,50 @@
<?php
namespace Deployer;
use Deployer\Exception\Exception;
use Deployer\Exception\GracefulShutdownException;
// Cancel deployment if there would be no change to the codebase.
// This avoids unnecessary releases if the latest commit has already been deployed.
desc('Checks remote head');
task('deploy:check_remote', function () {
$repository = get('repository');
// Skip if there is no current deployment to compare
if (!test('[ -d {{current_path}}/.git ]')) {
return;
}
// Determine the hash of the remote revision about to be deployed
$targetRevision = input()->getOption('revision');
if (!$targetRevision) {
$ref = 'HEAD';
$opt = '';
if ($tag = input()->getOption('tag')) {
$ref = $tag;
$opt = '--tags';
} elseif ($branch = get('branch')) {
$ref = $branch;
$opt = '--heads';
}
$remoteLs = runLocally("git ls-remote $opt $repository $ref");
if (strstr($remoteLs, "\n")) {
throw new Exception("Could not determine target revision. '$ref' matched multiple commits.");
}
if (!$remoteLs) {
throw new Exception("Could not resolve a revision from '$ref'.");
}
$targetRevision = substr($remoteLs, 0, strpos($remoteLs, "\t"));
}
// Compare commit hashes. We use strpos to support short versions.
$targetRevision = trim($targetRevision);
$lastDeployedRevision = run('cat {{current_path}}/REVISION');
if ($targetRevision && strpos($lastDeployedRevision, $targetRevision) === 0) {
throw new GracefulShutdownException("Already up-to-date.");
}
info("deployed different version");
});

View File

@ -0,0 +1,21 @@
<?php
namespace Deployer;
// Use sudo in deploy:cleanup task for rm command.
set('cleanup_use_sudo', false);
desc('Cleanup old releases');
task('deploy:cleanup', function () {
$releases = get('releases_list');
$keep = get('keep_releases');
$sudo = get('cleanup_use_sudo') ? 'sudo' : '';
run("cd {{deploy_path}} && if [ -e release ]; then rm release; fi");
if ($keep > 0) {
foreach (array_slice($releases, $keep) as $release) {
run("$sudo rm -rf {{deploy_path}}/releases/$release");
}
}
});

View File

@ -0,0 +1,25 @@
<?php
namespace Deployer;
// List of paths to remove from {{release_path}}.
set('clear_paths', []);
// Use sudo for deploy:clear_path task?
set('clear_use_sudo', false);
desc('Cleanup files and/or directories');
task('deploy:clear_paths', function () {
$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) {
run(implode('; ', $chunk));
}
});

View File

@ -0,0 +1,32 @@
<?php
namespace Deployer;
// List of dirs to copy between releases.
// For example you can copy `node_modules` to speedup npm install.
set('copy_dirs', []);
desc('Copies directories');
task('deploy:copy_dirs', function () {
if (has('previous_release')) {
foreach (get('copy_dirs') as $dir) {
// Make sure all path without tailing slash.
$dir = trim($dir, '/');
if (test("[ -d {{previous_release}}/$dir ]")) {
$destinationDir = '';
if (strpos($dir, '/') !== false) {
$destinationDir = substr($dir, 0, strrpos($dir, '/') + 1);
}
run("mkdir -p {{release_path}}/$dir");
// -a, --archive
// copy directories recursively, preserve all attributes,
// never follow symbolic links in SOURCE
// -f, --force
// if an existing destination file cannot be opened, remove it and try again (this option is ignored when the -n
// option is also used)
run("cp -af {{previous_release}}/$dir {{release_path}}/$destinationDir");
}
}
}
});

View File

@ -0,0 +1,13 @@
<?php
namespace Deployer;
set('dotenv_example', '.env.example');
desc('Configure .env file');
task('deploy:env', function () {
cd('{{release_or_current_path}}');
if (test('[ ! -e .env ] && [ -f {{dotenv_example}} ]')) {
run('cp {{dotenv_example}} .env');
}
});

View File

@ -0,0 +1,38 @@
<?php
namespace Deployer;
// Defines "what" text for the 'deploy:info' task.
// Uses one of the following sources:
// 1. Repository name
// 2. Application name
set('what', function () {
$repo = get('repository');
if (!empty($repo)) {
return preg_replace('/\.git$/', '', basename($repo));
}
$application = get('application');
if (!empty($application)) {
return $application;
}
return 'something';
});
// Defines "where" text for the 'deploy:info' task.
// Uses one of the following sources:
// 1. Host's stage label
// 2. Host's alias
set('where', function () {
$labels = get('labels');
if (isset($labels['stage'])) {
return $labels['stage'];
}
return currentHost()->getAlias();
});
desc('Displays info about deployment');
task('deploy:info', function () {
$releaseName = test('[ -d {{deploy_path}}/.dep ]') ? get('release_name') : 1;
info("deploying <fg=green;options=bold>{{what}}</> to <fg=magenta;options=bold>{{where}}</> (release <fg=magenta;options=bold>{$releaseName}</>)");
});

View File

@ -0,0 +1,33 @@
<?php
namespace Deployer;
use Deployer\Exception\GracefulShutdownException;
desc('Locks deploy');
task('deploy:lock', function () {
$user = quote(get('user'));
$locked = run("[ -f {{deploy_path}}/.dep/deploy.lock ] && echo +locked || echo $user > {{deploy_path}}/.dep/deploy.lock");
if ($locked === '+locked') {
$lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock");
throw new GracefulShutdownException(
"Deploy locked by $lockedUser.\n"
. "Execute \"deploy:unlock\" task to unlock.",
);
}
});
desc('Unlocks deploy');
task('deploy:unlock', function () {
run("rm -f {{deploy_path}}/.dep/deploy.lock");//always success
});
desc('Checks if deploy is locked');
task('deploy:is_locked', function () {
$locked = test("[ -f {{deploy_path}}/.dep/deploy.lock ]");
if ($locked) {
$lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock");
throw new GracefulShutdownException("Deploy is locked by $lockedUser.");
}
info('Deploy is unlocked.');
});

View File

@ -0,0 +1,25 @@
<?php
namespace Deployer;
// Creates patch of local changes and pushes them on host.
// And applies to current_path. Push can be done many times.
// The task purpose to be used only for development.
desc('Pushes local changes to remote host');
task('push', function () {
$files = explode("\n", runLocally("git diff --name-only HEAD"));
info('uploading:');
foreach ($files as $file) {
writeln(" - $file");
}
upload(
$files,
'{{current_path}}',
['progress_bar' => false, 'options' => ['--relative']],
);
// Mark this release as dirty.
run("echo '{{user}}' > {{current_path}}/DIRTY_RELEASE");
});

View File

@ -0,0 +1,205 @@
<?php
namespace Deployer;
use Deployer\Exception\Exception;
use Symfony\Component\Console\Helper\Table;
// The name of the release.
set('release_name', function () {
return within('{{deploy_path}}', function () {
$latest = run('cat .dep/latest_release || echo 0');
return strval(intval($latest) + 1);
});
});
// Holds releases log from `.dep/releases_log` file.
set('releases_log', function () {
cd('{{deploy_path}}');
if (!test('[ -f .dep/releases_log ]')) {
return [];
}
$releaseLogs = array_map(function ($line) {
return json_decode($line, true);
}, explode("\n", run('tail -n 300 .dep/releases_log')));
return array_filter($releaseLogs); // Return all non-empty lines.
});
// Return list of release names on host.
set('releases_list', function () {
cd('{{deploy_path}}');
// If there is no releases return empty list.
if (!test('[ -d releases ] && [ "$(ls -A releases)" ]')) {
return [];
}
// Will list only dirs in releases.
$ll = explode("\n", run('cd releases && ls -t -1 -d */'));
$ll = array_map(function ($release) {
return basename(rtrim(trim($release), '/'));
}, $ll);
// Return releases from newest to oldest.
$releasesLog = array_reverse(get('releases_log'));
$releases = [];
foreach ($releasesLog as $release) {
if (in_array($release['release_name'], $ll, true)) {
$releases[] = $release['release_name'];
}
}
return $releases;
});
// Return release path.
set('release_path', function () {
$releaseExists = test('[ -h {{deploy_path}}/release ]');
if ($releaseExists) {
$link = run("readlink {{deploy_path}}/release");
return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link;
} else {
throw new Exception(parse('The "release_path" ({{deploy_path}}/release) does not exist.'));
}
});
// Current release revision. Usually a git hash.
set('release_revision', function () {
return run('cat {{release_path}}/REVISION');
});
// Return the release path during a deployment
// but fallback to the current path otherwise.
set('release_or_current_path', function () {
$releaseExists = test('[ -h {{deploy_path}}/release ]');
return $releaseExists ? get('release_path') : get('current_path');
});
// Clean up unfinished releases and prepare next release
desc('Prepares release');
task('deploy:release', function () {
cd('{{deploy_path}}');
// Clean up if there is unfinished release.
if (test('[ -h release ]')) {
run('rm 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 = "releases/$releaseName";
// Check what there is no such release path.
if (test("[ -d $releasePath ]")) {
$freeReleaseName = '...';
// Check what $releaseName is integer.
if (ctype_digit($releaseName)) {
$freeReleaseName = intval($releaseName);
// Find free release name.
while (test("[ -d 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)) {
run("echo $releaseName > .dep/latest_release");
}
// Metainfo.
$timestamp = timestamp();
$metainfo = [
'created_at' => $timestamp,
'release_name' => $releaseName,
'user' => get('user'),
'target' => get('target'),
];
// Save metainfo about release.
$json = quote(json_encode($metainfo));
run("echo $json >> .dep/releases_log");
// Make new release.
run("mkdir -p $releasePath");
run("{{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]}");
}
});
desc('Shows releases list');
/*
* Example output:
* ```
* +---------------------+------example.org ------------+--------+-----------+
* | Date (UTC) | Release | Author | Target | Commit |
* +---------------------+-------------+----------------+--------+-----------+
* | 2021-11-06 20:51:45 | 1 | Anton Medvedev | HEAD | 34d24192e |
* | 2021-11-06 21:00:50 | 2 (bad) | Anton Medvedev | HEAD | 392948a40 |
* | 2021-11-06 23:19:20 | 3 | Anton Medvedev | HEAD | a4057a36c |
* | 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD | s3wa45ca6 |
* +---------------------+-------------+----------------+--------+-----------+
* ```
*/
task('releases', function () {
cd('{{deploy_path}}');
$releasesLog = get('releases_log');
$currentRelease = basename(run('readlink {{current_path}}'));
$releasesList = get('releases_list');
$table = [];
$tz = !empty(getenv('TIMEZONE')) ? getenv('TIMEZONE') : date_default_timezone_get();
foreach ($releasesLog as &$metainfo) {
$date = \DateTime::createFromFormat(\DateTimeInterface::ISO8601, $metainfo['created_at']);
$date->setTimezone(new \DateTimeZone($tz));
$status = $release = $metainfo['release_name'];
if (in_array($release, $releasesList, true)) {
if (test("[ -f releases/$release/BAD_RELEASE ]")) {
$status = "<error>$release</error> (bad)";
} elseif (test("[ -f releases/$release/DIRTY_RELEASE ]")) {
$status = "<error>$release</error> (dirty)";
} else {
$status = "<info>$release</info>";
}
try {
$revision = run("cat releases/$release/REVISION");
} catch (\Throwable $e) {
$revision = 'unknown';
}
} else {
$revision = 'unknown';
}
if ($release === $currentRelease) {
$status .= ' (current)';
}
$table[] = [
$date->format("Y-m-d H:i:s"),
$status,
$metainfo['user'],
$metainfo['target'],
$revision,
];
}
(new Table(output()))
->setHeaderTitle(currentHost()->getAlias())
->setHeaders(["Date ($tz)", 'Release', 'Author', 'Target', 'Commit'])
->setRows($table)
->render();
});

View File

@ -0,0 +1,91 @@
<?php
namespace Deployer;
use Deployer\Exception\Exception;
/*
* Rollback candidate will be automatically chosen by looking
* at output of `ls` command and content of `.dep/releases_log`.
*
* If rollback candidate is marked as **BAD_RELEASE**, it will be skipped.
*
* :::tip
* You can override rollback candidate via:
* ```
* dep rollback -o rollback_candidate=123
* ```
* :::
*/
set('rollback_candidate', function () {
$currentRelease = basename(run('readlink {{current_path}}'));
$releases = get('releases_list');
$releasesBeforeCurrent = [];
$foundCurrent = false;
foreach ($releases as $r) {
if ($r === $currentRelease) {
$foundCurrent = true;
continue;
}
if ($foundCurrent) {
$releasesBeforeCurrent[] = $r;
}
}
while (isset($releasesBeforeCurrent[0])) {
$candidate = $releasesBeforeCurrent[0];
// Skip all bad releases.
if (test("[ -f {{deploy_path}}/releases/$candidate/BAD_RELEASE ]")) {
array_shift($releasesBeforeCurrent);
continue;
}
return $candidate;
}
throw new Exception("No more releases you can revert to.");
});
desc('Rollbacks to the previous release');
/*
* Uses {{rollback_candidate}} for symlinking. Current release will be marked as
* bad by creating file **BAD_RELEASE** with timestamp and {{user}}.
*
* :::warning
* You can always manually symlink {{current_path}} to proper release.
* ```
* dep run '{{bin/symlink}} releases/123 {{current_path}}'
* ```
* :::
*/
task('rollback', function () {
cd('{{deploy_path}}');
$currentRelease = basename(run('readlink {{current_path}}'));
$candidate = get('rollback_candidate');
writeln("Current release is <fg=red>$currentRelease</fg=red>.");
if (!test("[ -d releases/$candidate ]")) {
throw new \RuntimeException(parse("Release \"$candidate\" not found in \"{{deploy_path}}/releases\"."));
}
if (test("[ -f releases/$candidate/BAD_RELEASE ]")) {
writeln("Candidate <fg=yellow>$candidate</> marked as <error>bad release</error>.");
if (!askConfirmation("Continue rollback to $candidate?")) {
writeln('Rollback aborted.');
return;
}
}
writeln("Rolling back to <info>$candidate</info> release.");
// Symlink to old release.
run("{{bin/symlink}} releases/$candidate {{current_path}}");
// Mark release as bad.
$timestamp = timestamp();
run("echo '$timestamp,{{user}}' > releases/$currentRelease/BAD_RELEASE");
writeln("<info>rollback</info> to release <info>$candidate</info> was <success>successful</success>");
});

View File

@ -0,0 +1,22 @@
<?php
namespace Deployer;
desc('Prepares host for deploy');
task('deploy:setup', function () {
run(
<<<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 (test('[ ! -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.");
}
});

View File

@ -0,0 +1,89 @@
<?php
namespace Deployer;
use Deployer\Exception\Exception;
use Symfony\Component\Console\Output\OutputInterface;
// List of dirs what will be shared between releases.
// Each release will have symlink to those dirs stored in {{deploy_path}}/shared dir.
// ```php
// set('shared_dirs', ['storage']);
// ```
set('shared_dirs', []);
// List of files what will be shared between releases.
// Each release will have symlink to those files stored in {{deploy_path}}/shared dir.
// ```php
// set('shared_files', ['.env']);
// ```
set('shared_files', []);
desc('Creates symlinks for shared files and dirs');
task('deploy:shared', function () {
$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 (!test("[ -d $sharedPath/$dir ]")) {
// Create shared dir if it does not exist.
run("mkdir -p $sharedPath/$dir");
// If release contains shared dir, copy that dir from release to shared.
if (test("[ -d $(echo {{release_path}}/$dir) ]")) {
run("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir));
}
}
// Remove from source.
run("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.
run("mkdir -p `dirname {{release_path}}/$dir`");
// Symlink shared dir to release dir
run("{{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 (!test("[ -d $sharedPath/$dirname ]")) {
run("mkdir -p $sharedPath/$dirname");
}
// Check if shared file does not exist in shared.
// and file exist in release
if (!test("[ -f $sharedPath/$file ]") && test("[ -f {{release_path}}/$file ]")) {
// Copy file in shared dir if not present
run("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file");
}
// Remove from source.
run("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi");
// Ensure dir is available in release
run("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi");
// Touch shared
run("[ -f $sharedPath/$file ] || touch $sharedPath/$file");
// Symlink shared dir to release dir
run("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file");
}
});

View File

@ -0,0 +1,21 @@
<?php
namespace Deployer;
// Use mv -T if available. Will check automatically.
set('use_atomic_symlink', function () {
return commandSupportsOption('mv', '--no-target-directory');
});
desc('Creates symlink to release');
task('deploy:symlink', function () {
if (get('use_atomic_symlink')) {
run("mv -T {{deploy_path}}/release {{current_path}}");
} else {
// Atomic symlink does not supported.
// Will use simple two steps switch.
run("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}"); // Atomic override symlink.
run("cd {{deploy_path}} && rm release"); // Remove release link.
}
});

View File

@ -0,0 +1,138 @@
<?php
namespace Deployer;
use Deployer\Exception\ConfigurationException;
use Symfony\Component\Console\Input\InputOption;
/**
* Determines which branch to deploy. Can be overridden with CLI option `--branch`.
* If not specified, will get current git HEAD branch as default branch to deploy.
*/
set('branch', 'HEAD');
option('tag', null, InputOption::VALUE_REQUIRED, 'Tag to deploy');
option('revision', null, InputOption::VALUE_REQUIRED, 'Revision to deploy');
option('branch', null, InputOption::VALUE_REQUIRED, 'Branch to deploy');
// The deploy target: a branch, a tag or a revision.
set('target', function () {
$target = '';
$branch = get('branch');
if (!empty($branch)) {
$target = $branch;
}
// Override target from CLI options.
if (input()->hasOption('branch') && !empty(input()->getOption('branch'))) {
$target = input()->getOption('branch');
}
if (input()->hasOption('tag') && !empty(input()->getOption('tag'))) {
$target = input()->getOption('tag');
}
if (input()->hasOption('revision') && !empty(input()->getOption('revision'))) {
$target = input()->getOption('revision');
}
if (empty($target)) {
$target = "HEAD";
}
return $target;
});
// Sets deploy:update_code strategy.
// Can be one of:
// - local_archive (copies the repository from local machine)
// - archive (default, fetches the code from the remote repository)
// - clone (if you need the origin repository `.git` dir in your {{release_path}}, clones from remote repository)
set('update_code_strategy', 'archive');
// Sets environment variable _GIT_SSH_COMMAND_ for `git clone` command.
// If `StrictHostKeyChecking` flag is set to `accept-new` then ssh will
// automatically add new host keys to the user known hosts files, but
// will not permit connections to hosts with changed host keys.
set('git_ssh_command', 'ssh -o StrictHostKeyChecking=accept-new');
/**
* Specifies a sub directory within the repository to deploy.
* Works only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`.
*
* Example:
* - set value to `src` if you want to deploy the folder that lives at `/src`.
* - set value to `src/api` if you want to deploy the folder that lives at `/src/api`.
*
* Note: do not use a leading `/`!
*/
set('sub_directory', false);
/**
* Update code at {{release_path}} on host.
*/
desc('Updates code');
task('deploy:update_code', function () {
$strategy = get('update_code_strategy');
$target = get('target');
$targetWithDir = $target;
if (!empty(get('sub_directory'))) {
$targetWithDir .= ':{{sub_directory}}';
}
if ($strategy === 'local_archive') {
$gitRoot = runLocally("git rev-parse --show-toplevel");
runLocally("git -C " . quote($gitRoot) . " archive $targetWithDir -o archive.tar");
upload("$gitRoot/archive.tar", '{{release_path}}/archive.tar');
run("tar -xf {{release_path}}/archive.tar -C {{release_path}}");
run("rm {{release_path}}/archive.tar");
unlink("$gitRoot/archive.tar");
$rev = quote(runLocally("git rev-list $target -1"));
} else {
$git = get('bin/git');
$repository = get('repository');
if (empty($repository)) {
throw new ConfigurationException("Missing 'repository' configuration.");
}
$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.
run("[ -d $bare ] || mkdir -p $bare");
run("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", env: $env);
cd($bare);
// If remote url changed, drop `.dep/repo` and reinstall.
if (run("$git config --get remote.origin.url") !== $repository) {
cd('{{deploy_path}}');
run("rm -rf $bare");
goto start;
}
run("$git remote update 2>&1", env: $env);
// Copy to release_path.
if ($strategy === 'archive') {
run("$git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1");
} elseif ($strategy === 'clone') {
cd('{{release_path}}');
run("$git clone -l $bare .");
run("$git remote set-url origin $repository", env: $env);
run("$git checkout --force $target");
} else {
throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}."));
}
$rev = quote(run("$git rev-list $target -1"));
}
// Save git revision in REVISION file.
run("echo $rev > {{release_path}}/REVISION");
});

View File

@ -0,0 +1,37 @@
<?php
namespace Deployer;
set('composer_action', 'install');
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');
set('composer_version', null);
// Returns Composer binary path if found. Otherwise, tries to install composer to `.dep/composer.phar`.
set('bin/composer', function () {
if (test('[ -f {{deploy_path}}/.dep/composer.phar ]')) {
if (empty(get('composer_version')) || preg_match(parse('/Composer.*{{composer_version}}/'), run('{{bin/php}} {{deploy_path}}/.dep/composer.phar --version'))) {
return '{{bin/php}} {{deploy_path}}/.dep/composer.phar';
}
}
if (commandExist('composer')) {
if (empty(get('composer_version')) || preg_match(parse('/Composer.*{{composer_version}}/'), run('{{bin/php}} ' . which('composer') . ' --version'))) {
return '{{bin/php}} ' . which('composer');
}
}
$versionAsName = get('composer_version') ? ' {{composer_version}}' : '';
$versionAsOption = get('composer_version') ? ' -- --version={{composer_version}}' : '';
warning("Composer{$versionAsName} wasn't found. Installing to \"{{deploy_path}}/.dep/composer.phar\".");
run("cd {{deploy_path}} && curl -sS https://getcomposer.org/installer | {{bin/php}}{$versionAsOption}");
run('mv {{deploy_path}}/composer.phar {{deploy_path}}/.dep/composer.phar');
return '{{bin/php}} {{deploy_path}}/.dep/composer.phar';
});
desc('Installs vendors');
task('deploy:vendors', function () {
if (!commandExist('unzip')) {
warning('To speed up composer installation setup "unzip" command with PHP zip extension.');
}
run('cd {{release_or_current_path}} && {{bin/composer}} {{composer_action}} {{composer_options}} 2>&1');
});

View File

@ -0,0 +1,164 @@
<?php
namespace Deployer;
// Used to make a writable directory by a server.
// Used in `chown` and `acl` modes of {{writable_mode}}.
// Attempts automatically to detect http user in process list.
set('http_user', function () {
$candidates = explode("\n", run("ps axo comm,user | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \$NF}' | uniq"));
$httpUser = array_shift($candidates);
if (empty($httpUser)) {
throw new \RuntimeException(
"Can't detect http user name.\n"
. "Please setup `http_user` config parameter.",
);
}
return $httpUser;
});
// Used to make a writable directory by a server.
// Used in `chgrp` mode of {{writable_mode}} only.
// Attempts automatically to detect http user in process list.
set('http_group', function () {
$candidates = explode("\n", run("ps axo comm,group | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \$NF}' | uniq"));
$httpGroup = array_shift($candidates);
if (empty($httpGroup)) {
throw new \RuntimeException(
"Can't detect http user name.\n"
. "Please setup `http_group` config parameter.",
);
}
return $httpGroup;
});
// List of writable dirs.
set('writable_dirs', []);
// One of:
// - chown
// - chgrp
// - chmod
// - acl
// - sticky
// - skip
set('writable_mode', 'acl');
// Using sudo in writable commands?
set('writable_use_sudo', false);
// Use recursive mode (-R)?
set('writable_recursive', false);
// The chmod mode.
set('writable_chmod_mode', '0755');
// List of additional groups to give write permission to.
set('writable_acl_groups', []);
// Force ACLs to be reapplied even if they already exist. Useful when recursive ACLs need to reach new nested paths but sudo isn't available. Slower, so enable only to fix writable dir permissions.
set('writable_acl_force', false);
desc('Makes writable dirs');
task('deploy:writable', function () {
$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`.');
}
cd('{{release_or_current_path}}');
// Create directories if they don't exist
run("mkdir -p $dirs");
if ($mode === 'chown') {
$httpUser = get('http_user');
// Change owner.
// -L traverse every symbolic link to a directory encountered
run("$sudo chown -L $recursive $httpUser $dirs");
} elseif ($mode === 'chgrp') {
// Change group ownership.
// -L traverse every symbolic link to a directory encountered
run("$sudo chgrp -L $recursive {{http_group}} $dirs");
run("$sudo chmod $recursive g+rwx $dirs");
} elseif ($mode === 'chmod') {
run("$sudo chmod $recursive {{writable_chmod_mode}} $dirs");
} elseif ($mode === 'acl') {
$remoteUser = get('remote_user', false);
if (empty($remoteUser)) {
$remoteUser = run('whoami');
}
$httpUser = get('http_user');
if (run("uname -s") === 'Darwin') {
// macOS supports chmod +a for ACL management
run("$sudo chmod +a \"$httpUser allow delete,write,append,file_inherit,directory_inherit\" $dirs");
run("$sudo chmod +a \"$remoteUser allow delete,write,append,file_inherit,directory_inherit\" $dirs");
} elseif (commandExist('setfacl')) {
$setFaclUsers = "-m u:\"$httpUser\":rwX";
$setFaclGroups = "";
foreach (get("writable_acl_groups") as $index => $group) {
if ($index > 0) {
$setFaclGroups .= " ";
}
$setFaclGroups .= "-m g:\"$group\":rwX";
}
// Check if remote user exists, before adding it to setfacl
$remoteUserExists = test("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 unless forcing ACL reset.
// Now, we will check each directory for ACL and only setfacl for which has not been set before,
// unless writable_acl_force is enabled.
$writeableDirs = get('writable_dirs');
$forceAcl = get('writable_acl_force');
foreach ($writeableDirs as $dir) {
// Check if ACL has been set or not
$hasfacl = run("getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l");
// Set ACL for directory if it has not been set before or if forcing ACL reset
if ($forceAcl || !$hasfacl) {
run("setfacl -L $recursive $setFaclUsers $setFaclGroups $dir");
run("setfacl -dL $recursive $setFaclUsers $setFaclGroups $dir");
}
}
} else {
run("$sudo setfacl -L $recursive $setFaclUsers $setFaclGroups $dirs");
run("$sudo setfacl -dL $recursive $setFaclUsers $setFaclGroups $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
run("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`.");
}
});

View File

@ -0,0 +1,80 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['drupal7']);
task('deploy', [
'deploy:prepare',
'deploy:publish',
]);
//Set Drupal 7 site. Change if you use different site
set('drupal_site', 'default');
//Drupal 7 shared dirs
set('shared_dirs', [
'sites/{{drupal_site}}/files',
]);
//Drupal 7 shared files
set('shared_files', [
'sites/{{drupal_site}}/settings.php',
]);
//Drupal 7 writable dirs
set('writable_dirs', [
'sites/{{drupal_site}}/files',
]);
//Create and upload Drupal 7 settings.php using values from secrets
task('drupal:settings', function () {
if (askConfirmation('Are you sure to generate and upload settings.php file?')) {
//Get template
$template = get('settings_template');
//Import secrets
$secrets = get('settings');
//Prepare replacement variables
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($secrets),
);
$replacements = [];
foreach ($iterator as $key => $value) {
$keys = [];
for ($i = $iterator->getDepth(); $i > 0; $i--) {
$keys[] = $iterator->getSubIterator($i - 1)->key();
}
$keys[] = $key;
$replacements['{{' . implode('.', $keys) . '}}'] = $value;
}
//Create settings from template
$settings = file_get_contents($template);
$settings = strtr($settings, $replacements);
writeln('settings.php created successfully');
$tmpFilename = tempnam(sys_get_temp_dir(), 'tmp_settings_');
file_put_contents($tmpFilename, $settings);
upload($tmpFilename, '{{deploy_path}}/shared/sites/{{drupal_site}}/settings.php');
unlink($tmpFilename);
}
});
//Upload Drupal 7 files folder
task('drupal:upload_files', function () {
if (askConfirmation('Are you sure?')) {
upload('sites/{{drupal_site}}/files', '{{deploy_path}}/shared/sites/{{drupal_site}}/files');
}
});

View File

@ -0,0 +1,32 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['drupal8']);
task('deploy', [
'deploy:prepare',
'deploy:publish',
]);
//Set drupal site. Change if you use different site
set('drupal_site', 'default');
//Drupal 8 shared dirs
set('shared_dirs', [
'sites/{{drupal_site}}/files',
]);
//Drupal 8 shared files
set('shared_files', [
'sites/{{drupal_site}}/settings.php',
'sites/{{drupal_site}}/services.yml',
]);
//Drupal 8 Writable dirs
set('writable_dirs', [
'sites/{{drupal_site}}/files',
]);

View File

@ -0,0 +1,48 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['flow_framework']);
// Flow-Framework application-context
set('flow_context', 'Production');
// Flow-Framework cli-command
set('flow_command', 'flow');
// Flow-Framework shared directories
set('shared_dirs', [
'Data/Persistent',
'Data/Logs',
'Configuration/{{flow_context}}',
]);
/**
* Apply database migrations
*/
desc('Applies database migrations');
task('deploy:run_migrations', function () {
run('FLOW_CONTEXT={{flow_context}} {{bin/php}} {{release_or_current_path}}/{{flow_command}} doctrine:migrate');
});
/**
* Publish resources
*/
desc('Publishes resources');
task('deploy:publish_resources', function () {
run('FLOW_CONTEXT={{flow_context}} {{bin/php}} {{release_or_current_path}}/{{flow_command}} resource:publish');
});
/**
* Main task
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:run_migrations',
'deploy:publish_resources',
'deploy:publish',
]);

View File

@ -0,0 +1,22 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['fuelphp']);
// FuelPHP 1.x shared dirs
set('shared_dirs', [
'fuel/app/cache', 'fuel/app/logs',
]);
/**
* Main task
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:publish',
]);

View File

@ -0,0 +1,17 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['joomla']);
set('shared_files', ['configuration.php']);
set('shared_dirs', ['images']);
set('writable_dirs', ['images']);
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:publish',
]);

View File

@ -0,0 +1,297 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['laravel']);
set('shared_dirs', ['storage']);
set('shared_files', ['.env']);
set('writable_dirs', [
'bootstrap/cache',
'storage',
]);
set('writable_recursive', true);
set('log_files', 'storage/logs/*.log');
set('bin/artisan', '{{release_or_current_path}}/artisan');
set('laravel_version', function () {
$result = run("{{bin/php}} {{bin/artisan}} --version");
preg_match_all('/(\d+\.?)+/', $result, $matches);
return $matches[0][0] ?? 5.5;
});
set('public_path', 'public');
/**
* Run an artisan command.
*
* Supported options:
* - 'min' => #.#: The minimum Laravel version required (included).
* - 'max' => #.#: The maximum Laravel version required (included).
* - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistant or empty.
* - 'failIfNoEnv': Fail the command if `.env` file is inexistant or empty.
* - 'showOutput': Show the output of the command if given.
*
* @param string $command The artisan command (with cli options if any).
* @param array $options The options that define the behaviour of the command.
* @return callable A function that can be used as a task.
*/
function artisan($command, $options = [])
{
return function () use ($command, $options) {
// Ensure the artisan command is available on the current version.
$versionTooEarly = array_key_exists('min', $options)
&& laravel_version_compare($options['min'], '<');
$versionTooLate = array_key_exists('max', $options)
&& laravel_version_compare($options['max'], '>');
if ($versionTooEarly || $versionTooLate) {
return;
}
// Get the dotenv path or use default.
$dotenv = get('dotenv', '{{release_or_current_path}}/.env');
// Ensure we warn or fail when a command relies on the ".env" file.
if (in_array('failIfNoEnv', $options) && !test("[ -s $dotenv ]")) {
throw new \Exception('Your .env file is empty! Cannot proceed.');
}
if (in_array('skipIfNoEnv', $options) && !test("[ -s $dotenv ]")) {
warning("Your .env file is empty! Skipping...</>");
return;
}
// Run the artisan command.
$output = run("{{bin/php}} {{bin/artisan}} $command");
// Output the results when appropriate.
if (in_array('showOutput', $options)) {
writeln("<info>$output</info>");
}
};
}
function laravel_version_compare($version, $comparator)
{
return version_compare(get('laravel_version'), $version, $comparator);
}
/*
* Maintenance mode.
*/
desc('Puts the application into maintenance / demo mode');
task('artisan:down', artisan('down', ['showOutput']));
desc('Brings the application out of maintenance mode');
task('artisan:up', artisan('up', ['showOutput']));
/*
* Generate keys.
*/
desc('Sets the application key');
task('artisan:key:generate', artisan('key:generate'));
desc('Creates the encryption keys for API authentication');
task('artisan:passport:keys', artisan('passport:keys'));
/*
* Database and migrations.
*/
desc('Seeds the database with records');
task('artisan:db:seed', artisan('db:seed --force', ['skipIfNoEnv', 'showOutput']));
desc('Runs the database migrations');
task('artisan:migrate', artisan('migrate --force', ['skipIfNoEnv']));
desc('Drops all tables and re-run all migrations');
task('artisan:migrate:fresh', artisan('migrate:fresh --force', ['skipIfNoEnv']));
desc('Rollbacks the last database migration');
task('artisan:migrate:rollback', artisan('migrate:rollback --force', ['skipIfNoEnv', 'showOutput']));
desc('Shows the status of each migration');
task('artisan:migrate:status', artisan('migrate:status', ['skipIfNoEnv', 'showOutput']));
/*
* Cache and optimizations.
*/
desc('Flushes the application cache');
task('artisan:cache:clear', artisan('cache:clear'));
desc('Creates a cache file for faster configuration loading');
task('artisan:config:cache', artisan('config:cache'));
desc('Removes the configuration cache file');
task('artisan:config:clear', artisan('config:clear'));
desc('Discovers and cache the application\'s events and listeners');
task('artisan:event:cache', artisan('event:cache', ['min' => '5.8.9']));
desc('Clears all cached events and listeners');
task('artisan:event:clear', artisan('event:clear', ['min' => '5.8.9']));
desc('Lists the application\'s events and listeners');
task('artisan:event:list', artisan('event:list', ['showOutput', 'min' => '5.8.9']));
desc('Cache the framework bootstrap files');
task('artisan:optimize', artisan('optimize'));
desc('Removes the cached bootstrap files');
task('artisan:optimize:clear', artisan('optimize:clear'));
desc('Reload running services');
task('artisan:reload', artisan('reload', ['min' => '12.41']));
desc('Creates a route cache file for faster route registration');
task('artisan:route:cache', artisan('route:cache'));
desc('Removes the route cache file');
task('artisan:route:clear', artisan('route:clear'));
desc('Lists all registered routes');
task('artisan:route:list', artisan('route:list', ['showOutput']));
desc('Creates the symbolic links configured for the application');
task('artisan:storage:link', artisan('storage:link', ['min' => 5.3]));
desc('Compiles all of the application\'s Blade templates');
task('artisan:view:cache', artisan('view:cache', ['min' => 5.6]));
desc('Clears all compiled view files');
task('artisan:view:clear', artisan('view:clear'));
/**
* Queue and Horizon.
*/
desc('Lists all of the failed queue jobs');
task('artisan:queue:failed', artisan('queue:failed', ['showOutput']));
desc('Flushes all of the failed queue jobs');
task('artisan:queue:flush', artisan('queue:flush'));
desc('Restarts queue worker daemons after their current job');
task('artisan:queue:restart', artisan('queue:restart'));
desc('Starts a master supervisor in the foreground');
task('artisan:horizon', artisan('horizon'));
desc('Deletes all of the jobs from the specified queue');
task('artisan:horizon:clear', artisan('horizon:clear --force'));
desc('Instructs the master supervisor to continue processing jobs');
task('artisan:horizon:continue', artisan('horizon:continue'));
desc('Lists all of the deployed machines');
task('artisan:horizon:list', artisan('horizon:list', ['showOutput']));
desc('Pauses the master supervisor');
task('artisan:horizon:pause', artisan('horizon:pause'));
desc('Terminates any rogue Horizon processes');
task('artisan:horizon:purge', artisan('horizon:purge'));
desc('Gets the current status of Horizon');
task('artisan:horizon:status', artisan('horizon:status', ['showOutput']));
desc('Terminates the master supervisor so it can be restarted');
task('artisan:horizon:terminate', artisan('horizon:terminate'));
desc('Publish all of the Horizon resources');
task('artisan:horizon:publish', artisan('horizon:publish'));
desc('Lists all of the supervisors');
task('artisan:horizon:supervisors', artisan('horizon:supervisors', ['showOutput']));
desc('Deletes metrics for all jobs and queues');
task('artisan:horizon:clear-metrics', artisan('horizon:clear-metrics'));
desc('Stores a snapshot of the queue metrics');
task('artisan:horizon:snapshot', artisan('horizon:snapshot'));
/*
* Scheduler.
*/
desc('Interrupt in-progress schedule:run invocations');
task('artisan:schedule:interrupt', artisan('schedule:interrupt'));
/*
* Telescope.
*/
desc('Clears all entries from Telescope');
task('artisan:telescope:clear', artisan('telescope:clear'));
desc('Prunes stale entries from the Telescope database');
task('artisan:telescope:prune', artisan('telescope:prune'));
/*
* Octane.
*/
desc('Starts the octane server');
task('artisan:octane', artisan('octane:start'));
desc('Reloads the octane server');
task('artisan:octane:reload', artisan('octane:reload'));
desc('Stops the octane server');
task('artisan:octane:stop', artisan('octane:stop'));
desc('Check the status of the octane server');
task('artisan:octane:status', artisan('octane:status'));
/*
* Nova.
*/
desc('Publish all of the Laravel Nova resources');
task('artisan:nova:publish', artisan('nova:publish'));
/*
* Reverb.
*/
desc('Starts the Reverb server');
task('artisan:reverb:start', artisan('reverb:start'));
desc('Restarts the Reverb server');
task('artisan:reverb:restart', artisan('reverb:restart'));
/*
* Pulse.
*/
desc('Starts the Pulse server');
task('artisan:pulse:check', artisan('pulse:check'));
desc('Restarts the Pulse server');
task('artisan:pulse:restart', artisan('pulse:restart'));
desc('Purges all Pulse data from storage');
task('artisan:pulse:purge', artisan('pulse:purge'));
desc('Process incoming Pulse data from the ingest stream');
task('artisan:pulse:work', artisan('pulse:work'));
/**
* Main deploy task.
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'artisan:storage:link',
'artisan:optimize',
'artisan:migrate',
'deploy:publish',
'artisan:reload',
]);

View File

@ -0,0 +1,51 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['magento']);
/**
* Magento Configuration
*/
// Magento shared dirs
set('shared_dirs', ['var', 'media']);
// Magento shared files
set('shared_files', ['app/etc/local.xml']);
// Magento writable dirs
set('writable_dirs', ['var', 'media']);
/**
* Clear cache
*/
desc('Clears cache');
task('deploy:cache:clear', function () {
run("cd {{release_or_current_path}} && php -r \"require_once 'app/Mage.php'; umask(0); Mage::app()->cleanCache();\"");
});
/**
* Remove files that can be used to compromise Magento
*/
task('deploy:clear_version', function () {
run("rm -f {{release_or_current_path}}/LICENSE.html");
run("rm -f {{release_or_current_path}}/LICENSE.txt");
run("rm -f {{release_or_current_path}}/LICENSE_AFL.txt");
run("rm -f {{release_or_current_path}}/RELEASE_NOTES.txt");
})->hidden();
after('deploy:update_code', 'deploy:clear_version');
/**
* Main task
*/
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:cache:clear',
'deploy:publish',
]);

View File

@ -0,0 +1,651 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
require_once __DIR__ . '/../contrib/cachetool.php';
use Deployer\Exception\ConfigurationException;
use Deployer\Exception\GracefulShutdownException;
use Deployer\Exception\RunException;
use Deployer\Exception\TimeoutException;
use Deployer\Host\Host;
const CONFIG_IMPORT_NEEDED_EXIT_CODE = 2;
const DB_UPDATE_NEEDED_EXIT_CODE = 2;
const MAINTENANCE_MODE_ACTIVE_OUTPUT_MSG = 'maintenance mode is active';
const ENV_CONFIG_FILE_PATH = 'app/etc/env.php';
const TMP_ENV_CONFIG_FILE_PATH = 'app/etc/env_tmp.php';
add('recipes', ['magento2']);
// Configuration
// By default setup:static-content:deploy uses `en_US`.
// To change that, simply put `set('static_content_locales', 'en_US de_DE');`
// in you deployer script.
set('static_content_locales', 'en_US');
// Configuration
// You can also set the themes to run against. By default it'll deploy
// all themes - `add('magento_themes', ['Magento/luma', 'Magento/backend']);`
// If the themes are set as a simple list of strings, then all languages defined in {{static_content_locales}} are
// compiled for the given themes.
// Alternatively The themes can be defined as an associative array, where the key represents the theme name and
// the key contains the languages for the compilation (for this specific theme)
// Example:
// set('magento_themes', ['Magento/luma']); - Will compile this theme with every language from {{static_content_locales}}
// set('magento_themes', [
// 'Magento/luma' => null, - Will compile all languages from {{static_content_locales}} for Magento/luma
// 'Custom/theme' => 'en_US fr_FR' - Will compile only en_US and fr_FR for Custom/theme
// 'Custom/another' => '{{static_content_locales}} it_IT' - Will compile all languages from {{static_content_locales}} + it_IT for Custom/another
// ]); - Will compile this theme with every language
set('magento_themes', [
]);
// Static content deployment options, e.g. '--no-parent'
set('static_deploy_options', '');
// Deploy frontend and adminhtml together as default
set('split_static_deployment', false);
// Use the default languages for the backend as default
set('static_content_locales_backend', '{{static_content_locales}}');
// backend themes to deploy. Only used if split_static_deployment=true
// This setting supports the same options/structure as {{magento_themes}}
set('magento_themes_backend', ['Magento/backend' => null]);
// Configuration
// Also set the number of concurrent jobs to run. The default is 1
// Update using: `set('static_content_jobs', '1');`
set('static_content_jobs', '1');
set('content_version', function () {
return time();
});
// Magento directory relative to repository root. Use "." (default) if it is not located in a subdirectory
set('magento_dir', '.');
set('shared_files', [
'{{magento_dir}}/app/etc/env.php',
'{{magento_dir}}/var/.maintenance.ip',
]);
set('shared_dirs', [
'{{magento_dir}}/var/composer_home',
'{{magento_dir}}/var/log',
'{{magento_dir}}/var/export',
'{{magento_dir}}/var/report',
'{{magento_dir}}/var/import',
'{{magento_dir}}/var/import_history',
'{{magento_dir}}/var/session',
'{{magento_dir}}/var/importexport',
'{{magento_dir}}/var/backups',
'{{magento_dir}}/var/tmp',
'{{magento_dir}}/pub/sitemap',
'{{magento_dir}}/pub/media',
'{{magento_dir}}/pub/static/_cache',
]);
set('writable_dirs', [
'{{magento_dir}}/var',
'{{magento_dir}}/pub/static',
'{{magento_dir}}/pub/media',
'{{magento_dir}}/generated',
'{{magento_dir}}/var/page_cache',
]);
set('clear_paths', [
'{{magento_dir}}/generated/*',
'{{magento_dir}}/pub/static/_cache/*',
'{{magento_dir}}/var/generation/*',
'{{magento_dir}}/var/cache/*',
'{{magento_dir}}/var/page_cache/*',
'{{magento_dir}}/var/view_preprocessed/*',
]);
// WARNING: Do not use {{bin/magento}} in deploy:failed handlers - release_or_current_path points to the failed
// release during failure, not the live one. Use {{current_path}} explicitly instead.
// See: config_import_needed_on_current, magento:config:import:on-current, magento:maintenance:enable/disable.
set('bin/magento', '{{release_or_current_path}}/{{magento_dir}}/bin/magento');
set('magento_version', function () {
// detect version
$versionOutput = run('{{bin/php}} {{bin/magento}} --version');
preg_match('/(\d+\.?)+(-p\d+)?$/', $versionOutput, $matches);
return $matches[0] ?? '2.0';
});
set('config_import_needed', function () {
// detect if app:config:import is needed
try {
run('{{bin/php}} {{bin/magento}} app:config:status');
} catch (RunException $e) {
if ($e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) {
return true;
}
throw $e;
}
return false;
});
set('config_import_needed_on_current', function () {
if (!test('[ -d {{current_path}}/{{magento_dir}} ] && [ -f {{current_path}}/{{magento_dir}}/bin/magento ]')) {
writeln('Current Magento installation is unavailable => import skipped');
return false;
}
try {
// detect if app:config:import is needed on the current (live) release
// do not use {{bin/magento}} as it resolves via release_or_current_path which is unreliable in failure scenarios
run('{{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento app:config:status');
} catch (RunException|TimeoutException $e) {
if ($e instanceof RunException && $e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) {
return true;
}
// In failure scenarios, non-status errors should not break deploy:failed handlers.
writeln('Unable to determine app config status on current release => import skipped');
return false;
}
return false;
});
set('database_upgrade_needed', function () {
// detect if db upgrade is needed
try {
run('{{bin/php}} {{bin/magento}} setup:db:status');
} catch (RunException $e) {
if ($e->getExitCode() == DB_UPDATE_NEEDED_EXIT_CODE) {
return true;
}
throw $e;
}
return false;
});
set('full_upgrade_needed', function () {
//Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status
//TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698
return false;
});
set('upgrade_needed', function () {
// Detect necessary upgrade, partial db or full upgrade
try {
return get('database_upgrade_needed') || get('full_upgrade_needed');
} catch (RunException $e) {
throw $e;
}
});
// Deploy without setting maintenance mode if possible
set('enable_zerodowntime', true);
// Tasks
// To work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php`
// e.g.
// ```php
// 'MAGE_MODE' => 'production'
// ```
desc('Compiles magento di');
task('magento:compile', function () {
run("{{bin/php}} {{bin/magento}} setup:di:compile");
run('cd {{release_or_current_path}}/{{magento_dir}} && {{bin/composer}} dump-autoload -o');
});
// To work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template`
// in `app/etc/config.php`, e.g.:
// ```php
// 'system' => [
// 'default' => [
// 'dev' => [
// 'js' => [
// 'merge_files' => '1',
// 'minify_files' => '1'
// ],
// 'css' => [
// 'merge_files' => '1',
// 'minify_files' => '1'
// ],
// 'template' => [
// 'minify_html' => '1'
// ]
// ]
// ]
// ```
desc('Deploys assets');
task('magento:deploy:assets', function () {
$themesToCompile = '';
if (get('split_static_deployment')) {
invoke('magento:deploy:assets:adminhtml');
invoke('magento:deploy:assets:frontend');
} else {
if (count(get('magento_themes')) > 0) {
$themes = array_is_list(get('magento_themes')) ? get('magento_themes') : array_keys(get('magento_themes'));
foreach ($themes as $theme) {
$themesToCompile .= ' -t ' . $theme;
}
}
run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --content-version={{content_version}} {{static_deploy_options}} {{static_content_locales}} $themesToCompile -j {{static_content_jobs}}");
}
});
desc('Deploys assets for backend only');
task('magento:deploy:assets:adminhtml', function () {
magentoDeployAssetsSplit('backend');
});
desc('Deploys assets for frontend only');
task('magento:deploy:assets:frontend', function () {
magentoDeployAssetsSplit('frontend');
});
/**
* @phpstan-param 'frontend'|'backend' $area
*
* @throws ConfigurationException
*/
function magentoDeployAssetsSplit(string $area)
{
if (!in_array($area, ['frontend', 'backend'], true)) {
throw new ConfigurationException("\$area must be either 'frontend' or 'backend', '$area' given");
}
$maxProcesses = get('static_content_jobs');
$isFrontend = $area === 'frontend';
$suffix = $isFrontend
? ''
: '_backend';
$themesConfig = get("magento_themes$suffix");
$defaultLanguages = get("static_content_locales$suffix");
$useDefaultLanguages = array_is_list($themesConfig);
/** @var list<string> $themes */
$themes = $useDefaultLanguages
? $themesConfig
: array_keys($themesConfig);
$staticContentArea = $isFrontend
? 'frontend'
: 'adminhtml';
// group themes by their languages to minimize number of static content deploy commands, and allow parallel jobs
$themeGroups = [];
if ($useDefaultLanguages) {
$themeGroups[$defaultLanguages] = $themes;
} else {
foreach ($themes as $theme) {
$locales = array_unique(array_filter(explode(' ', parse($themesConfig[$theme] ?? $defaultLanguages))));
sort($locales); // sort locales to ensure consistent grouping
$localesSorted = implode(' ', $locales);
$themeGroups[$localesSorted][] = $theme;
}
}
foreach ($themeGroups as $locales => $themes) {
$localeCount = substr_count($locales, ' ') + 1;
// how many themes can we process in parallel?
$maxConcurrentThemes = max(1, floor($maxProcesses / $localeCount));
// WARNING: when static_content_jobs>1, and it's doing more than 1 theme-locale per process, it can get stuck - so we do 1batch at a time
do {
$chunk = array_splice($themes, 0, $maxConcurrentThemes);
$themeArgs = '-t ' . implode(' -t ', $chunk);
run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --area=$staticContentArea --content-version={{content_version}} {{static_deploy_options}} $locales $themeArgs -j {{static_content_jobs}}");
} while (!empty($themes));
}
}
desc('Syncs content version');
task('magento:sync:content_version', function () {
$timestamp = time();
on(select('all'), function (Host $host) use ($timestamp) {
$host->set('content_version', $timestamp);
});
})->once();
before('magento:deploy:assets', 'magento:sync:content_version');
desc('Enables maintenance mode');
task('magento:maintenance:enable', function () {
// do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current"
run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:enable; fi");
});
desc('Disables maintenance mode');
task('magento:maintenance:disable', function () {
// do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current"
run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:disable; fi");
});
desc('Set maintenance mode if needed');
task('magento:maintenance:enable-if-needed', function () {
! get('enable_zerodowntime') || get('upgrade_needed') || get('config_import_needed')
? invoke('magento:maintenance:enable')
: writeln('Config and database up to date => no maintenance mode');
});
desc('Config Import');
task('magento:config:import', function () {
if (get('config_import_needed')) {
run('{{bin/php}} {{bin/magento}} app:config:import --no-interaction');
} else {
writeln('App config is up to date => import skipped');
}
});
desc('Config Import on current release');
task('magento:config:import:on-current', function () {
try {
if (get('config_import_needed_on_current')) {
// do not use {{bin/magento}} as it must run on the current (last successful) release in failure scenarios
run('{{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento app:config:import --no-interaction');
} else {
writeln('App config import skipped');
}
} catch (RunException|TimeoutException $e) {
writeln('Unable to import app config on current release => import skipped');
}
});
desc('Upgrades magento database');
task('magento:upgrade:db', function () {
if (get('database_upgrade_needed')) {
// clear config cache, so there is no error when a new MQ topic is introduced
run("{{bin/php}} {{bin/magento}} cache:clean config");
run("{{bin/php}} {{bin/magento}} setup:db-schema:upgrade --no-interaction");
run("{{bin/php}} {{bin/magento}} setup:db-data:upgrade --no-interaction");
} else {
writeln('Database schema is up to date => upgrade skipped');
}
});
desc('Run upgrades if needed');
task('magento:upgrade', function () {
if (get('full_upgrade_needed')) {
run("{{bin/php}} {{bin/magento}} setup:upgrade --no-interaction --keep-generated");
} elseif (get('database_upgrade_needed')) {
invoke('magento:upgrade:db');
}
})->once();
desc('Flushes Magento Cache');
task('magento:cache:flush', function () {
run("{{bin/php}} {{bin/magento}} cache:flush");
});
desc('Magento2 deployment operations');
task('deploy:magento', [
'magento:build',
'magento:maintenance:enable-if-needed',
'magento:config:import',
'magento:upgrade',
'magento:maintenance:disable',
]);
desc('Magento2 build operations');
task('magento:build', [
'magento:compile',
'magento:deploy:assets',
]);
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:clear_paths',
'deploy:magento',
'deploy:publish',
]);
after('deploy:symlink', 'magento:cache:flush');
after('deploy:failed', 'deploy:magento:failed');
desc('Run Magento post-deployment failure tasks');
task('deploy:magento:failed', [
'magento:config:import:on-current',
'magento:maintenance:disable',
]);
// Artifact deployment section
// The file the artifact is saved to
set('artifact_file', 'artifact.tar.gz');
// The directory the artifact is saved in
set('artifact_dir', 'artifacts');
// Points to a file with a list of files to exclude from packaging.
// The format is as with the `tar --exclude-from=[file]` option
set('artifact_excludes_file', 'artifacts/excludes');
// If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory
set('build_from_repo', false);
// Set this value if "build_from_repo" is set to true. The target to deploy must also be set with "--branch", "--tag" or "--revision"
set('repository', null);
// The relative path to the artifact file. If the directory does not exist, it will be created
set('artifact_path', function () {
if (!testLocally('[ -d {{artifact_dir}} ]')) {
runLocally('mkdir -p {{artifact_dir}}');
}
return get('artifact_dir') . '/' . get('artifact_file');
});
// The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings
set('bin/tar', function () {
if (commandExist('gtar')) {
return which('gtar');
} else {
return which('tar');
}
});
// tasks section
desc('Packages all relevant files in an artifact.');
task('artifact:package', function () {
if (!test('[ -f {{artifact_excludes_file}} ]')) {
throw new GracefulShutdownException(
"No artifact excludes file provided, provide one at artifacts/excludes or change location",
);
}
run('{{bin/tar}} --exclude-from={{artifact_excludes_file}} -czf {{artifact_path}} -C {{release_or_current_path}} .');
});
desc('Uploads artifact in release folder for extraction.');
task('artifact:upload', function () {
upload(get('artifact_path'), '{{release_path}}');
});
desc('Extracts artifact in release path.');
task('artifact:extract', function () {
run('{{bin/tar}} -xzpf {{release_path}}/{{artifact_file}} -C {{release_path}}');
run('rm -rf {{release_path}}/{{artifact_file}}');
});
desc('Clears generated files prior to building.');
task('build:remove-generated', function () {
run('rm -rf generated/*');
});
desc('Prepare local artifact build');
task('build:prepare', function () {
if (!currentHost()->get('local')) {
throw new GracefulShutdownException('Artifact can only be built locally, you provided a non local host');
}
$buildDir = get('build_from_repo') ? get('artifact_dir') . '/repo' : '.';
set('deploy_path', $buildDir);
set('release_path', $buildDir);
set('current_path', $buildDir);
if (!get('build_from_repo')) {
return;
}
$repository = (string) get('repository');
if ($repository === '') {
throw new GracefulShutdownException('You must specify the "repository" option.');
}
run('rm -rf {{release_or_current_path}}');
run('git clone {{repository}} {{release_or_current_path}}');
run('git -C {{release_or_current_path}} checkout --force {{target}}');
});
desc('Builds an artifact.');
task('artifact:build', [
'build:prepare',
'build:remove-generated',
'deploy:vendors',
'magento:compile',
'magento:deploy:assets',
'artifact:package',
]);
// Array of shared files that will be added to the default shared_files without overriding
set('additional_shared_files', []);
// Array of shared directories that will be added to the default shared_dirs without overriding
set('additional_shared_dirs', []);
desc('Adds additional files and dirs to the list of shared files and dirs');
task('deploy:additional-shared', function () {
add('shared_files', get('additional_shared_files'));
add('shared_dirs', get('additional_shared_dirs'));
});
/**
* Update cache id_prefix on deploy so that you are compiling against a fresh cache
* Reference Issue: https://github.com/davidalger/capistrano-magento2/issues/151
* To use this feature, add the following to your deployer scripts:
* ```php
* after('deploy:shared', 'magento:set_cache_prefix');
* after('deploy:magento', 'magento:cleanup_cache_prefix');
* ```
**/
desc('Update cache id_prefix');
task('magento:set_cache_prefix', function () {
//download current env config
$tmpConfigFile = tempnam(sys_get_temp_dir(), 'deployer_config');
download('{{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH, $tmpConfigFile);
$envConfigArray = include($tmpConfigFile);
//set prefix to `alias_releasename_`
$prefixUpdate = get('alias') . '_' . get('release_name') . '_';
//check for preload keys and update
if (isset($envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'])) {
$oldPrefix = $envConfigArray['cache']['frontend']['default']['id_prefix'];
$preloadKeys = $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'];
$newPreloadKeys = [];
foreach ($preloadKeys as $preloadKey) {
$newPreloadKeys[] = preg_replace('/^' . $oldPrefix . '/', $prefixUpdate, $preloadKey);
}
$envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'] = $newPreloadKeys;
}
//update id_prefix to include release name
$envConfigArray['cache']['frontend']['default']['id_prefix'] = $prefixUpdate;
$envConfigArray['cache']['frontend']['page_cache']['id_prefix'] = $prefixUpdate;
//Generate configuration array as string
$envConfigStr = '<?php return ' . var_export($envConfigArray, true) . ';';
file_put_contents($tmpConfigFile, $envConfigStr);
//upload updated config to server
upload($tmpConfigFile, '{{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH);
//cleanup tmp file
unlink($tmpConfigFile);
//delete the symlink for env.php
run('rm {{release_or_current_path}}/' . ENV_CONFIG_FILE_PATH);
//link the env to the tmp version
run('{{bin/symlink}} {{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH . ' {{release_path}}/' . ENV_CONFIG_FILE_PATH);
});
/**
* After successful deployment, move the tmp_env.php file to env.php ready for next deployment
*/
desc('Cleanup cache id_prefix env files');
task('magento:cleanup_cache_prefix', function () {
run('rm {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH);
run('rm {{release_or_current_path}}/' . ENV_CONFIG_FILE_PATH);
run('mv {{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH . ' {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH);
// Symlink shared dir to release dir
run('{{bin/symlink}} {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH . ' {{release_path}}/' . ENV_CONFIG_FILE_PATH);
});
/**
* Remove cron from crontab and kill running cron jobs
* To use this feature, add the following to your deployer scripts:
* ```php
* after('magento:maintenance:enable-if-needed', 'magento:cron:stop');
* ```
*/
desc('Remove cron from crontab and kill running cron jobs');
task('magento:cron:stop', function () {
if (has('previous_release')) {
run('{{bin/php}} {{previous_release}}/{{magento_dir}}/bin/magento cron:remove');
}
run('pgrep -U "$(id -u)" -f "bin/magento +(cron:run|queue:consumers:start)" | xargs -r kill');
});
/**
* Install cron in crontab
* To use this feature, add the following to your deployer scripts:
* ```php
* after('magento:upgrade', 'magento:cron:install');
* ```
*/
desc('Install cron in crontab');
task('magento:cron:install', function () {
run('cd {{release_or_current_path}}');
run('{{bin/php}} {{bin/magento}} cron:install');
});
desc('Prepares an artifact on the target server');
task('artifact:prepare', [
'deploy:info',
'deploy:setup',
'deploy:lock',
'deploy:release',
'artifact:upload',
'artifact:extract',
'deploy:additional-shared',
'deploy:shared',
'deploy:writable',
]);
desc('Executes the tasks after artifact is released');
task('artifact:finish', [
'magento:cache:flush',
'cachetool:clear:opcache',
'deploy:cleanup',
'deploy:unlock',
'deploy:success',
]);
desc('Actually releases the artifact deployment');
task('artifact:deploy', [
'artifact:prepare',
'magento:maintenance:enable-if-needed',
'magento:config:import',
'magento:upgrade',
'magento:maintenance:disable',
'deploy:symlink',
'artifact:finish',
]);
fail('artifact:deploy', 'deploy:failed');

View File

@ -0,0 +1,31 @@
<?php
namespace Deployer;
require_once __DIR__ . '/symfony.php';
add('recipes', ['pimcore']);
add('shared_dirs', ['public/var', 'var/email', 'var/recyclebin', 'var/versions']);
add('shared_files', ['config/local/database.yaml']);
add('writable_dirs', ['public/var', 'var/cache/dev']);
desc('Rebuilds Pimcore Classes');
task('pimcore:rebuild-classes', function () {
run('{{bin/console}} pimcore:build:classes');
run('{{bin/console}} pimcore:deployment:classes-rebuild --create-classes --delete-classes --no-interaction');
});
desc('Removes cache');
task('pimcore:cache_clear', function () {
run('rm -rf {{release_or_current_path}}/var/cache/dev/*');
});
task('pimcore:deploy', [
'pimcore:rebuild-classes',
'pimcore:cache_clear',
]);
after('deploy:vendors', 'pimcore:deploy');

View File

@ -0,0 +1,46 @@
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['prestashop']);
set('shared_files', [
'config/settings.inc.php',
'.htaccess',
]);
set('shared_dirs', [
'img',
'log',
'download',
'upload',
'translations',
'mails',
'themes/default-bootstrap/lang',
'themes/default-bootstrap/mails',
'themes/default-bootstrap/pdf/lang',
]);
set('writable_dirs', [
'img',
'log',
'cache',
'download',
'upload',
'translations',
'mails',
'themes/default-bootstrap/lang',
'themes/default-bootstrap/mails',
'themes/default-bootstrap/pdf/lang',
'themes/default-bootstrap/cache',
]);
desc('Deploys your project');
task(
'deploy',
[
'deploy:prepare',
'deploy:vendors',
'deploy:publish',
],
);

View File

@ -0,0 +1,218 @@
<?php
namespace Deployer;
require __DIR__ . '/provision/databases.php';
require __DIR__ . '/provision/nodejs.php';
require __DIR__ . '/provision/php.php';
require __DIR__ . '/provision/user.php';
require __DIR__ . '/provision/website.php';
use Deployer\Task\Context;
use function Deployer\Support\parse_home_dir;
add('recipes', ['provision']);
// Name of lsb_release like: focal, bionic, etc.
// As only Ubuntu 20.04 LTS is supported for provision should be the `focal`.
set('lsb_release', function () {
return run("lsb_release -s -c");
});
desc('Provision the server');
task('provision', [
'provision:check',
'provision:configure',
'provision:update',
'provision:upgrade',
'provision:install',
'provision:ssh',
'provision:firewall',
'provision:user',
'provision:php',
'provision:node',
'provision:databases',
'provision:composer',
'provision:server',
'provision:website',
'provision:verify',
]);
// Default user to use for provisioning.
set('provision_user', 'root');
desc('Checks pre-required state');
task('provision:check', function () {
set('remote_user', get('provision_user'));
$release = run('cat /etc/os-release');
['NAME' => $name, 'VERSION_ID' => $version] = parse_ini_string($release);
if ($name !== 'Ubuntu') {
warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
warning('!! !!');
warning('!! Only Ubuntu is supported! !!');
warning('!! !!');
warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {
throw new \RuntimeException('Provision aborted due to incompatible OS.');
}
}
// Also only version 20 and older are supported.
if (version_compare($version, '20', '<')) {
warning("Ubuntu $version is not supported. Use Ubuntu 20 or newer.");
if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {
throw new \RuntimeException('Provision aborted due to incompatible OS.');
}
}
})->oncePerNode();
desc('Collects required params');
task('provision:configure', function () {
set('remote_user', get('provision_user'));
$params = [
'sudo_password',
'domain',
'public_path',
'php_version',
'db_type',
];
$dbparams = [
'db_user',
'db_name',
'db_password',
];
$showCode = false;
foreach ($params as $name) {
if (!Context::get()->getConfig()->hasOwn($name)) {
$showCode = true;
}
get($name);
}
if (get('db_type') !== 'none') {
foreach ($dbparams as $name) {
if (!Context::get()->getConfig()->hasOwn($name)) {
$showCode = true;
}
get($name);
}
}
if ($showCode) {
$code = "\n\n<comment>====== Configuration Start ======</comment>";
$code .= "\nhost(<info>'{{alias}}'</info>)";
$codeParams = $params;
if (get('db_type') !== 'none') {
$codeParams = array_merge($codeParams, $dbparams);
}
foreach ($codeParams as $name) {
$code .= "\n ->set(<info>'$name'</info>, <info>'" . get($name) . "'</info>)";
}
$code .= ";\n";
$code .= "<comment>====== Configuration End ======</comment>\n\n";
writeln($code);
}
});
desc('Adds repositories and update');
task('provision:update', function () {
set('remote_user', get('provision_user'));
// Update before installing anything
run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']);
// Pre-requisites
run('apt install -y curl gpg software-properties-common', env: ['DEBIAN_FRONTEND' => 'noninteractive']);
// PHP
run('apt-add-repository ppa:ondrej/php -y', env: [
'DEBIAN_FRONTEND' => 'noninteractive',
'LC_ALL' => 'C.UTF-8',
]);
// Caddy
run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg");
run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list");
// Update
run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']);
})
->oncePerNode()
->verbose();
desc('Upgrades all packages');
task('provision:upgrade', function () {
set('remote_user', get('provision_user'));
run('apt-get upgrade -y', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);
})
->oncePerNode()
->verbose();
desc('Installs packages');
task('provision:install', function () {
set('remote_user', get('provision_user'));
$packages = [
'acl',
'apt-transport-https',
'build-essential',
'caddy',
'curl',
'debian-archive-keyring',
'debian-keyring',
'fail2ban',
'gcc',
'git',
'libmcrypt4',
'libpcre3-dev',
'libsqlite3-dev',
'make',
'ncdu',
'nodejs',
'pkg-config',
'python-is-python3',
'redis',
'sendmail',
'sqlite3',
'ufw',
'unzip',
'uuid-runtime',
'whois',
];
run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);
})
->verbose()
->oncePerNode();
desc('Configures the ssh');
task('provision:ssh', function () {
set('remote_user', get('provision_user'));
run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config");
run('ssh-keygen -A');
run('service ssh restart');
if (test('[ ! -d /root/.ssh ]')) {
run('mkdir -p /root/.ssh');
run('touch /root/.ssh/authorized_keys');
}
})->oncePerNode();
desc('Setups a firewall');
task('provision:firewall', function () {
set('remote_user', get('provision_user'));
run('ufw allow 22');
run('ufw allow 80');
run('ufw allow 443');
run('ufw --force enable');
})->oncePerNode();
desc('Verifies what provision was successful');
task('provision:verify', function () {
fetch('{{domain}}', 'get', [], null, $info, true);
if ($info['http_code'] === 404) {
info("provisioned successfully!");
}
});

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 Not Found</title>
<style>
body {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
align-content: center;
background: #343434;
color: #fff;
display: grid;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 20px;
justify-content: center;
margin: 0;
min-height: 100vh;
}
main {
padding: 0 30px;
}
svg {
animation: 2s ease-in-out infinite hover;
}
@keyframes hover {
0%, 100% {
transform: translateY(0)
}
50% {
transform: translateY(-8px)
}
}
</style>
</head>
<body>
<main>
<svg width="68" viewBox="0 0 48 40" xmlns="http://www.w3.org/2000/svg">
<path
d="M41.8253 0.269788C44.8519 -0.884712 47.9239 1.84927 47.1284 4.98944L39.3219 35.8043C38.615 38.5948 35.2805 39.7475 33.0012 37.9892L19.9291 27.905C17.8765 26.3215 17.8492 23.2338 19.8735 21.6144L35.3856 9.20469C36.2481 8.51467 37.5067 8.65451 38.1967 9.51704C38.8868 10.3796 38.7469 11.6381 37.8844 12.3282L22.8693 24.3402C22.6163 24.5427 22.6197 24.9286 22.8762 25.1266L34.8414 34.3568C35.1263 34.5766 35.5431 34.4325 35.6315 34.0837L43.0145 4.94009C43.1139 4.54757 42.7299 4.20583 42.3516 4.35014L5.41328 18.44C4.96316 18.6117 4.99234 19.2581 5.4561 19.3885L12.954 21.4973C14.4463 21.917 15.5617 23.1612 15.8166 24.6903L17.2908 33.5357C17.3714 34.0193 18.0276 34.11 18.2364 33.6664L18.8254 32.4148C19.2957 31.4154 20.4872 30.9865 21.4866 31.4568C22.486 31.9271 22.915 33.1186 22.4446 34.118L21.1735 36.8193C19.5032 40.3685 14.2535 39.6429 13.6086 35.7737L11.9232 25.6611C11.8913 25.4699 11.7519 25.3144 11.5654 25.2619L2.9172 22.8296C-0.792895 21.7862 -1.02636 16.6153 2.5746 15.2417L41.8253 0.269788Z"
fill="white"/>
</svg>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
</main>
</body>
</html>

View File

@ -0,0 +1,25 @@
{{domain}} {
root * {{deploy_path}}/current/{{public_path}}
encode zstd gzip
file_server
php_fastcgi * unix//run/php/php{{php_version}}-fpm.sock {
resolve_root_symlink
}
log {
output file {{deploy_path}}/log/access.log {
mode 0644
}
}
handle_errors {
@404 {
expression {http.error.status_code} == 404
}
rewrite @404 /404.html
encode zstd gzip
file_server {
root /var/deployer
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Deployer;
set('db_type', function () {
$supportedDbTypes = [
'none',
'mysql',
'mariadb',
'postgresql',
];
return askChoice(' What DB to install? ', $supportedDbTypes, 0);
});
set('db_name', function () {
return ask(' DB name: ', 'prod');
});
set('db_user', function () {
return ask(' DB user: ', 'deployer');
});
set('db_password', function () {
return askHiddenResponse(' DB password: ');
});
desc('Provision databases');
task('provision:databases', function () {
set('remote_user', get('provision_user'));
$dbType = get('db_type');
if ($dbType === 'none') {
return;
}
invoke('provision:' . $dbType);
})
->limit(1);
desc('Provision MySQL');
task('provision:mysql', function () {
run('apt-get install -y mysql-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);
run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\"");
run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\"");
run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\"");
run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\"");
});
desc('Provision MariaDB');
task('provision:mariadb', function () {
run('apt-get install -y mariadb-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);
run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\"");
run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\"");
run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\"");
run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\"");
});
desc('Provision PostgreSQL');
task('provision:postgresql', function () {
run('apt-get install -y postgresql postgresql-contrib', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);
run("sudo -u postgres psql <<< $'CREATE DATABASE {{db_name}};'");
run("sudo -u postgres psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%db_password%\';'", secrets: ['db_password' => get('db_password')]);
run("sudo -u postgres psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'");
});

View File

@ -0,0 +1,36 @@
<?php
namespace Deployer;
set('node_version', '--lts');
desc('Installs npm packages');
task('provision:node', function () {
set('remote_user', get('provision_user'));
if (has('nodejs_version')) {
throw new \RuntimeException('nodejs_version is deprecated, use node_version instead.');
}
$arch = run('uname -m');
if ($arch === 'arm' || str_starts_with($arch, 'armv7')) {
$filename = 'fnm-arm32';
} elseif (str_starts_with($arch, 'aarch') || str_starts_with($arch, 'armv8')) {
$filename = 'fnm-arm64';
} else {
$filename = 'fnm-linux';
}
$url = "https://github.com/Schniz/fnm/releases/latest/download/$filename.zip";
run("rm -rf /tmp/$filename.zip");
run("curl -sSL $url --output /tmp/$filename.zip");
run("unzip /tmp/$filename.zip -d /tmp");
run("mv /tmp/fnm /usr/local/bin/fnm");
run('chmod +x /usr/local/bin/fnm');
run('fnm install {{node_version}}');
run("echo " . quote('eval "`fnm env`"') . " >> /etc/profile.d/fnm.sh");
})
->oncePerNode();

View File

@ -0,0 +1,85 @@
<?php
namespace Deployer;
set('php_version', function () {
$defaultPhpVersion = file_exists('composer.json')
? explode('|', preg_replace('/[^0-9.|]+/', '', json_decode(file_get_contents('composer.json'), true)['require']['php'] ?? '8.4'))[0]
: '8.4';
if (count(($parts = explode('.', $defaultPhpVersion))) > 2) {
$defaultPhpVersion = "$parts[0].$parts[1]";
}
return ask(' What PHP version to install? ', $defaultPhpVersion, ['5.6', '7.4', '8.2', '8.3', '8.4', '8.5']);
});
desc('Installs PHP packages');
task('provision:php', function () {
set('remote_user', get('provision_user'));
$version = get('php_version');
info("Installing PHP $version");
$packages = [
"php$version-bcmath",
"php$version-cli",
"php$version-curl",
"php$version-dev",
"php$version-fpm",
"php$version-gd",
"php$version-imap",
"php$version-intl",
"php$version-mbstring",
"php$version-mysql",
"php$version-pgsql",
"php$version-readline",
"php$version-soap",
"php$version-sqlite3",
"php$version-xml",
"php$version-zip",
];
run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive']);
// Configure PHP-CLI
run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini");
run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini");
run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini");
run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini");
run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini");
// Configure PHP-FPM
run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini");
run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/fpm/php.ini");
run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/fpm/php.ini");
run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/fpm/php.ini");
run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/fpm/php.ini");
run("sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/$version/fpm/php.ini");
// Configure FPM Pool
run("sed -i 's/;request_terminate_timeout = .*/request_terminate_timeout = 60/' /etc/php/$version/fpm/pool.d/www.conf");
run("sed -i 's/;catch_workers_output = .*/catch_workers_output = yes/' /etc/php/$version/fpm/pool.d/www.conf");
run("sed -i 's/;php_flag\[display_errors\] = .*/php_flag[display_errors] = yes/' /etc/php/$version/fpm/pool.d/www.conf");
run("sed -i 's/;php_admin_value\[error_log\] = .*/php_admin_value[error_log] = \/var\/log\/fpm-php.www.log/' /etc/php/$version/fpm/pool.d/www.conf");
run("sed -i 's/;php_admin_flag\[log_errors\] = .*/php_admin_flag[log_errors] = on/' /etc/php/$version/fpm/pool.d/www.conf");
// Configure PHP sessions directory
run('chmod 733 /var/lib/php/sessions');
run('chmod +t /var/lib/php/sessions');
})
->verbose()
->limit(1);
desc('Shows php-fpm logs');
task('logs:php-fpm', function () {
$fpmLogs = run("ls -1 /var/log | grep fpm");
if (empty($fpmLogs)) {
throw new \RuntimeException('No PHP-FPM logs found.');
}
run("sudo tail -f /var/log/$fpmLogs");
})->verbose();
desc('Installs Composer');
task('provision:composer', function () {
run('curl -sS https://getcomposer.org/installer | php');
run('mv composer.phar /usr/local/bin/composer');
})->oncePerNode();

Some files were not shown because too many files have changed in this diff Show More