├── Dockerfile ├── recipe ├── composer.php ├── zend_framework.php ├── deploy │ ├── env.php │ ├── cleanup.php │ ├── clear_paths.php │ ├── symlink.php │ ├── push.php │ ├── setup.php │ ├── lock.php │ ├── info.php │ ├── copy_dirs.php │ ├── vendors.php │ ├── check_remote.php │ ├── rollback.php │ └── shared.php ├── joomla.php ├── wordpress.php ├── fuelphp.php ├── codeigniter.php ├── provision │ ├── Caddyfile │ ├── nodejs.php │ ├── 404.html │ ├── website.php │ ├── databases.php │ ├── user.php │ └── php.php ├── yii.php ├── drupal8.php ├── pimcore.php ├── sulu.php ├── prestashop.php ├── cakephp.php ├── flow_framework.php ├── magento.php ├── silverstripe.php ├── drupal7.php ├── symfony.php ├── statamic.php ├── craftcms.php └── contao.php ├── src ├── Component │ ├── PharUpdate │ │ ├── Exception │ │ │ ├── FileException.php │ │ │ ├── ExceptionInterface.php │ │ │ ├── InvalidArgumentException.php │ │ │ ├── LogicException.php │ │ │ └── Exception.php │ │ ├── Version │ │ │ ├── Exception │ │ │ │ ├── VersionException.php │ │ │ │ ├── InvalidNumberException.php │ │ │ │ ├── InvalidIdentifierException.php │ │ │ │ └── InvalidStringRepresentationException.php │ │ │ ├── Dumper.php │ │ │ ├── Validator.php │ │ │ ├── Parser.php │ │ │ └── Version.php │ │ ├── Console │ │ │ ├── Helper.php │ │ │ └── Command.php │ │ ├── Manager.php │ │ └── Manifest.php │ └── Pimple │ │ └── Exception │ │ ├── ExpectedInvokableException.php │ │ ├── FrozenServiceException.php │ │ ├── UnknownIdentifierException.php │ │ └── InvalidServiceIdentifierException.php ├── Exception │ ├── HttpieException.php │ ├── ConfigurationException.php │ ├── WillAskUser.php │ ├── TimeoutException.php │ ├── GracefulShutdownException.php │ ├── Exception.php │ └── RunException.php ├── Logger │ ├── Handler │ │ ├── HandlerInterface.php │ │ ├── NullHandler.php │ │ └── FileHandler.php │ └── Logger.php ├── Host │ ├── Localhost.php │ ├── HostCollection.php │ └── Range.php ├── Documentation │ ├── DocConfig.php │ └── DocTask.php ├── Executor │ ├── Response.php │ ├── Worker.php │ └── Planner.php ├── Support │ ├── ObjectProxy.php │ └── Reporter.php ├── Task │ ├── TaskCollection.php │ ├── GroupTask.php │ ├── Context.php │ └── ScriptManager.php ├── Ssh │ ├── RunParams.php │ ├── IOArguments.php │ └── SshClient.php ├── Command │ ├── CustomOption.php │ ├── CommandCommon.php │ ├── WorkerCommand.php │ ├── ConfigCommand.php │ ├── RunCommand.php │ └── SshCommand.php ├── ProcessRunner │ ├── Printer.php │ └── ProcessRunner.php ├── Collection │ └── Collection.php ├── Selector │ └── Selector.php └── schema.json ├── .php-cs-fixer.dist.php ├── contrib ├── npm.php ├── yarn.php ├── webpack_encore.php ├── rollbar.php ├── raygun.php ├── bugsnag.php ├── hipchat.php ├── php-fpm.php ├── newrelic.php ├── grafana.php ├── cloudflare.php ├── rabbit.php ├── discord.php ├── yammer.php ├── workplace.php └── cachetool.php ├── SECURITY.md ├── LICENSE ├── bin ├── docgen ├── dep └── build ├── composer.json └── README.md /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3-cli-alpine 2 | 3 | RUN apk add --no-cache bash git openssh-client rsync 4 | 5 | COPY --chmod=755 deployer.phar /bin/dep 6 | 7 | WORKDIR /app 8 | 9 | ENTRYPOINT ["/bin/dep"] 10 | -------------------------------------------------------------------------------- /recipe/composer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FileException extends Exception {} 13 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ExceptionInterface {} 13 | -------------------------------------------------------------------------------- /recipe/zend_framework.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidArgumentException extends Exception {} 13 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LogicException extends Exception {} 13 | -------------------------------------------------------------------------------- /recipe/deploy/env.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class VersionException extends Exception {} 15 | -------------------------------------------------------------------------------- /src/Exception/HttpieException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | class HttpieException extends \RuntimeException {} 14 | -------------------------------------------------------------------------------- /src/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | class ConfigurationException extends \RuntimeException {} 14 | -------------------------------------------------------------------------------- /recipe/joomla.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Logger\Handler; 12 | 13 | interface HandlerInterface 14 | { 15 | public function log(string $message): void; 16 | } 17 | -------------------------------------------------------------------------------- /recipe/fuelphp.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Logger\Handler; 12 | 13 | class NullHandler implements HandlerInterface 14 | { 15 | public function log(string $message): void {} 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/WillAskUser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | class WillAskUser extends Exception 14 | { 15 | public function __construct(string $message) 16 | { 17 | parent::__construct($message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Host/Localhost.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Host; 12 | 13 | class Localhost extends Host 14 | { 15 | public function __construct(string $hostname = 'localhost') 16 | { 17 | parent::__construct($hostname); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /recipe/codeigniter.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | class TimeoutException extends Exception 14 | { 15 | public function __construct( 16 | string $command, 17 | ?float $timeout, 18 | ) { 19 | $message = sprintf('The command "%s" exceeded the timeout of %s seconds.', $command, $timeout); 20 | parent::__construct($message, 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /recipe/deploy/cleanup.php: -------------------------------------------------------------------------------- 1 | 0) { 17 | foreach (array_slice($releases, $keep) as $release) { 18 | run("$sudo rm -rf {{deploy_path}}/releases/$release"); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/Component/Pimple/Exception/ExpectedInvokableException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Component\Pimple\Exception; 12 | 13 | use Psr\Container\ContainerExceptionInterface; 14 | 15 | /** 16 | * A closure or invokable object was expected. 17 | * 18 | * @author Pascal Luna 19 | */ 20 | class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface {} 21 | -------------------------------------------------------------------------------- /src/Host/HostCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Host; 12 | 13 | use Deployer\Collection\Collection; 14 | 15 | /** 16 | * @method Host get($name) 17 | * @method Host[] getIterator() 18 | */ 19 | class HostCollection extends Collection 20 | { 21 | protected function notFound(string $name): \InvalidArgumentException 22 | { 23 | return new \InvalidArgumentException("Host \"$name\" not found."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /recipe/yii.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 5 | ->in(__DIR__ . '/recipe') 6 | ->in(__DIR__ . '/contrib') 7 | ->in(__DIR__ . '/tests'); 8 | 9 | return (new PhpCsFixer\Config()) 10 | ->setRules([ 11 | '@PER-CS' => true, 12 | 13 | // Due to historical reasons we have to keep this. 14 | // Docs parser expects comment right after php tag. 15 | 'blank_line_after_opening_tag' => false, 16 | 17 | // For PHP 7.4 compatibility. 18 | 'trailing_comma_in_multiline' => [ 19 | 'elements' => ['arguments', 'array_destructuring', 'arrays'] 20 | ], 21 | ]) 22 | ->setFinder($finder); 23 | -------------------------------------------------------------------------------- /src/Documentation/DocConfig.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Documentation; 12 | 13 | class DocConfig 14 | { 15 | /** 16 | * @var string 17 | */ 18 | public $name; 19 | /** 20 | * @var string 21 | */ 22 | public $defaultValue; 23 | /** 24 | * @var string 25 | */ 26 | public $comment; 27 | /** 28 | * @var string 29 | */ 30 | public $recipePath; 31 | /** 32 | * @var int 33 | */ 34 | public $lineNumber; 35 | } 36 | -------------------------------------------------------------------------------- /recipe/deploy/clear_paths.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Logger\Handler; 12 | 13 | class FileHandler implements HandlerInterface 14 | { 15 | /** 16 | * @var string 17 | */ 18 | private $filePath; 19 | 20 | public function __construct(string $filePath) 21 | { 22 | $this->filePath = $filePath; 23 | } 24 | 25 | public function log(string $message): void 26 | { 27 | file_put_contents($this->filePath, $message, FILE_APPEND); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Executor/Response.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Executor; 12 | 13 | class Response 14 | { 15 | private int $status; 16 | private mixed $body; 17 | 18 | public function __construct(int $status, mixed $body) 19 | { 20 | $this->status = $status; 21 | $this->body = $body; 22 | } 23 | 24 | public function getStatus(): int 25 | { 26 | return $this->status; 27 | } 28 | 29 | public function getBody(): mixed 30 | { 31 | return $this->body; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exception/GracefulShutdownException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | /** 14 | * Then this exception thrown, it will not trigger "fail" callback. 15 | * 16 | * fail('deploy', 'deploy:failed'); 17 | * 18 | * task('deploy', function () { 19 | * throw new GracefulShutdownException(...); 20 | * }); 21 | * 22 | * In example above task `deploy:failed` will not be called. 23 | */ 24 | class GracefulShutdownException extends Exception 25 | { 26 | public const EXIT_CODE = 42; 27 | } 28 | -------------------------------------------------------------------------------- /src/Support/ObjectProxy.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Support; 12 | 13 | class ObjectProxy 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private $objects; 19 | 20 | public function __construct(array $objects) 21 | { 22 | $this->objects = $objects; 23 | } 24 | 25 | public function __call(string $name, array $arguments): self 26 | { 27 | foreach ($this->objects as $object) { 28 | $object->$name(...$arguments); 29 | } 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /recipe/deploy/symlink.php: -------------------------------------------------------------------------------- 1 | false, 'options' => ['--relative']], 21 | ); 22 | 23 | // Mark this release as dirty. 24 | run("echo '{{user}}' > {{current_path}}/DIRTY_RELEASE"); 25 | }); 26 | -------------------------------------------------------------------------------- /src/Task/TaskCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Task; 12 | 13 | use Deployer\Collection\Collection; 14 | 15 | /** 16 | * @method Task get($name) 17 | * @method Task[] getIterator() 18 | */ 19 | class TaskCollection extends Collection 20 | { 21 | protected function notFound(string $name): \InvalidArgumentException 22 | { 23 | return new \InvalidArgumentException("Task `$name` not found."); 24 | } 25 | 26 | public function add(Task $task): void 27 | { 28 | $this->set($task->getName(), $task); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contrib/npm.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Helper extends Base 17 | { 18 | /** 19 | * Returns the update manager. 20 | * 21 | * @param string $uri The manifest file URI. 22 | * 23 | * @return Manager The update manager. 24 | */ 25 | public function getManager(string $uri): Manager 26 | { 27 | return new Manager(Manifest::loadFile($uri)); 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return 'phar-update'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Component/Pimple/Exception/FrozenServiceException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Component\Pimple\Exception; 12 | 13 | use Psr\Container\ContainerExceptionInterface; 14 | 15 | /** 16 | * An attempt to modify a frozen service was made. 17 | * 18 | * @author Pascal Luna 19 | */ 20 | class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface 21 | { 22 | /** 23 | * @param string $id Identifier of the frozen service 24 | */ 25 | public function __construct(string $id) 26 | { 27 | parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Component/Pimple/Exception/UnknownIdentifierException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Component\Pimple\Exception; 12 | 13 | use Psr\Container\NotFoundExceptionInterface; 14 | 15 | /** 16 | * The identifier of a valid service or parameter was expected. 17 | * 18 | * @author Pascal Luna 19 | */ 20 | class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface 21 | { 22 | /** 23 | * @param string $id The unknown identifier 24 | */ 25 | public function __construct(string $id) 26 | { 27 | parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Deployer is generally backwards compatible with very few exceptions, so we 6 | recommend users to always use the latest version to experience stability, 7 | performance and security. 8 | 9 | We generally backport security issues to a single previous major version, 10 | unless this is not possible or feasible with a reasonable effort. 11 | 12 | | Version | Supported | 13 | |---------|--------------------| 14 | | 8 | :white_check_mark: | 15 | | 7 | :white_check_mark: | 16 | | < 7 | :x: | 17 | 18 | ## Reporting a Vulnerability 19 | 20 | If you believe you've discovered a serious vulnerability, please contact the 21 | Expr core team at anton+security@medv.io. We will evaluate your report and if 22 | necessary issue a fix and an advisory. If the issue was previously undisclosed, 23 | we'll also mention your name in the credits. 24 | -------------------------------------------------------------------------------- /recipe/pimcore.php: -------------------------------------------------------------------------------- 1 | secrets = array_merge($params->secrets ?? [], $secrets ?? []); 27 | $params->timeout = $timeout ?? $params->timeout; 28 | return $params; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Component/Pimple/Exception/InvalidServiceIdentifierException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Component\Pimple\Exception; 12 | 13 | use Psr\Container\NotFoundExceptionInterface; 14 | 15 | /** 16 | * An attempt to perform an operation that requires a service identifier was made. 17 | * 18 | * @author Pascal Luna 19 | */ 20 | class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface 21 | { 22 | /** 23 | * @param string $id The invalid identifier 24 | */ 25 | public function __construct(string $id) 26 | { 27 | parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Documentation/DocTask.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Documentation; 12 | 13 | class DocTask 14 | { 15 | /** 16 | * @var string 17 | */ 18 | public $name; 19 | /** 20 | * @var string 21 | */ 22 | public $desc; 23 | /** 24 | * @var string 25 | */ 26 | public $comment; 27 | /** 28 | * @var array 29 | */ 30 | public $group; 31 | /** 32 | * @var string 33 | */ 34 | public $recipePath; 35 | /** 36 | * @var int 37 | */ 38 | public $lineNumber; 39 | 40 | public function mdLink(): string 41 | { 42 | $md = php_to_md($this->recipePath); 43 | $anchor = anchor($this->name); 44 | return "[$this->name](/docs/$md#$anchor)"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /recipe/sulu.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Exception extends \Exception implements ExceptionInterface 13 | { 14 | /** 15 | * Creates a new exception using a format and values. 16 | * 17 | * @param mixed $value,... The value(s). 18 | */ 19 | public static function create(string $format, $value = null): self 20 | { 21 | if (0 < func_num_args()) { 22 | $format = vsprintf($format, array_slice(func_get_args(), 1)); 23 | } 24 | 25 | return new static($format); 26 | } 27 | 28 | /** 29 | * Creates an exception for the last error message. 30 | */ 31 | public static function lastError(): self 32 | { 33 | $error = error_get_last(); 34 | 35 | return new static($error['message']); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2013 Anton Medvedev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Exception/InvalidNumberException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidNumberException extends VersionException 13 | { 14 | /** 15 | * The invalid version number. 16 | * 17 | * @var mixed 18 | */ 19 | private $number; 20 | 21 | /** 22 | * Sets the invalid version number. 23 | * 24 | * @param mixed $number The invalid version number. 25 | */ 26 | public function __construct($number) 27 | { 28 | parent::__construct( 29 | sprintf( 30 | 'The version number "%s" is invalid.', 31 | $number, 32 | ), 33 | ); 34 | 35 | $this->number = $number; 36 | } 37 | 38 | /** 39 | * Returns the invalid version number. 40 | * 41 | * @return mixed The invalid version number. 42 | */ 43 | public function getNumber() 44 | { 45 | return $this->number; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /recipe/deploy/lock.php: -------------------------------------------------------------------------------- 1 | {{deploy_path}}/.dep/deploy.lock"); 11 | if ($locked === '+locked') { 12 | $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); 13 | throw new GracefulShutdownException( 14 | "Deploy locked by $lockedUser.\n" . 15 | "Execute \"deploy:unlock\" task to unlock.", 16 | ); 17 | } 18 | }); 19 | 20 | desc('Unlocks deploy'); 21 | task('deploy:unlock', function () { 22 | run("rm -f {{deploy_path}}/.dep/deploy.lock");//always success 23 | }); 24 | 25 | desc('Checks if deploy is locked'); 26 | task('deploy:is_locked', function () { 27 | $locked = test("[ -f {{deploy_path}}/.dep/deploy.lock ]"); 28 | if ($locked) { 29 | $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); 30 | throw new GracefulShutdownException("Deploy is locked by $lockedUser."); 31 | } 32 | info('Deploy is unlocked.'); 33 | }); 34 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Exception/InvalidIdentifierException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidIdentifierException extends VersionException 13 | { 14 | /** 15 | * The invalid identifier. 16 | * 17 | * @var string 18 | */ 19 | private $identifier; 20 | 21 | /** 22 | * Sets the invalid identifier. 23 | * 24 | * @param string $identifier The invalid identifier. 25 | */ 26 | public function __construct(string $identifier) 27 | { 28 | parent::__construct( 29 | sprintf( 30 | 'The identifier "%s" is invalid.', 31 | $identifier, 32 | ), 33 | ); 34 | 35 | $this->identifier = $identifier; 36 | } 37 | 38 | /** 39 | * Returns the invalid identifier. 40 | * 41 | * @return string The invalid identifier. 42 | */ 43 | public function getIdentifier(): string 44 | { 45 | return $this->identifier; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Task/GroupTask.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Task; 12 | 13 | use function Deployer\invoke; 14 | 15 | class GroupTask extends Task 16 | { 17 | /** 18 | * List of tasks. 19 | * 20 | * @var string[] 21 | */ 22 | private $group; 23 | 24 | /** 25 | * @param string[] $group 26 | */ 27 | public function __construct(string $name, array $group) 28 | { 29 | parent::__construct($name); 30 | $this->group = $group; 31 | } 32 | 33 | public function run(Context $context): void 34 | { 35 | foreach ($this->group as $item) { 36 | invoke($item); 37 | } 38 | } 39 | 40 | /** 41 | * List of dependent tasks names 42 | * 43 | * @return string[] 44 | */ 45 | public function getGroup(): array 46 | { 47 | return $this->group; 48 | } 49 | 50 | public function setGroup(array $group): void 51 | { 52 | $this->group = $group; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /recipe/cakephp.php: -------------------------------------------------------------------------------- 1 | desc('Initialization'); 31 | 32 | /** 33 | * Run migrations 34 | */ 35 | task('deploy:run_migrations', function () { 36 | run('{{bin/php}} {{release_or_current_path}}/bin/cake.php migrations migrate --no-lock'); 37 | run('{{bin/php}} {{release_or_current_path}}/bin/cake.php schema_cache build'); 38 | })->desc('Run migrations'); 39 | 40 | /** 41 | * Main task 42 | */ 43 | task('deploy', [ 44 | 'deploy:prepare', 45 | 'deploy:vendors', 46 | 'deploy:init', 47 | 'deploy:run_migrations', 48 | 'deploy:publish', 49 | ])->desc('Deploy your project'); 50 | -------------------------------------------------------------------------------- /recipe/deploy/info.php: -------------------------------------------------------------------------------- 1 | getAlias(); 31 | }); 32 | 33 | desc('Displays info about deployment'); 34 | task('deploy:info', function () { 35 | $releaseName = test('[ -d {{deploy_path}}/.dep ]') ? get('release_name') : 1; 36 | 37 | info("deploying {{what}} to {{where}} (release {$releaseName})"); 38 | }); 39 | -------------------------------------------------------------------------------- /contrib/webpack_encore.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidStringRepresentationException extends VersionException 13 | { 14 | /** 15 | * The invalid string representation. 16 | * 17 | * @var string 18 | */ 19 | private $version; 20 | 21 | /** 22 | * Sets the invalid string representation. 23 | * 24 | * @param string $version The string representation. 25 | */ 26 | public function __construct(string $version) 27 | { 28 | parent::__construct( 29 | sprintf( 30 | 'The version string representation "%s" is invalid.', 31 | $version, 32 | ), 33 | ); 34 | 35 | $this->version = $version; 36 | } 37 | 38 | /** 39 | * Returns the invalid string representation. 40 | * 41 | * @return string The invalid string representation. 42 | */ 43 | public function getVersion(): string 44 | { 45 | return $this->version; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /recipe/provision/nodejs.php: -------------------------------------------------------------------------------- 1 | > /etc/profile.d/fnm.sh"); 37 | }) 38 | ->oncePerNode(); 39 | -------------------------------------------------------------------------------- /src/Host/Range.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Host; 12 | 13 | class Range 14 | { 15 | public const PATTERN = '/\[(.+?)\]/'; 16 | 17 | public static function expand(array $hostnames): array 18 | { 19 | $expanded = []; 20 | foreach ($hostnames as $hostname) { 21 | if (preg_match(self::PATTERN, $hostname, $matches)) { 22 | [$start, $end] = explode(':', $matches[1]); 23 | $zeroBased = (bool) preg_match('/^0[1-9]/', $start); 24 | 25 | foreach (range($start, $end) as $i) { 26 | $expanded[] = preg_replace(self::PATTERN, self::format((string) $i, $zeroBased), $hostname); 27 | } 28 | } else { 29 | $expanded[] = $hostname; 30 | } 31 | } 32 | 33 | return $expanded; 34 | } 35 | 36 | private static function format(string $i, bool $zeroBased): string 37 | { 38 | if ($zeroBased) { 39 | return strlen($i) === 1 ? "0$i" : $i; 40 | } else { 41 | return $i; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /recipe/magento.php: -------------------------------------------------------------------------------- 1 | cleanCache();\""); 28 | }); 29 | 30 | /** 31 | * Remove files that can be used to compromise Magento 32 | */ 33 | task('deploy:clear_version', function () { 34 | run("rm -f {{release_or_current_path}}/LICENSE.html"); 35 | run("rm -f {{release_or_current_path}}/LICENSE.txt"); 36 | run("rm -f {{release_or_current_path}}/LICENSE_AFL.txt"); 37 | run("rm -f {{release_or_current_path}}/RELEASE_NOTES.txt"); 38 | })->hidden(); 39 | 40 | after('deploy:update_code', 'deploy:clear_version'); 41 | 42 | 43 | /** 44 | * Main task 45 | */ 46 | desc('Deploys your project'); 47 | task('deploy', [ 48 | 'deploy:prepare', 49 | 'deploy:cache:clear', 50 | 'deploy:publish', 51 | ]); 52 | -------------------------------------------------------------------------------- /recipe/deploy/copy_dirs.php: -------------------------------------------------------------------------------- 1 | get('rollbar_token'), 37 | 'environment' => get('where'), 38 | 'revision' => runLocally('git log -n 1 --format="%h"'), 39 | 'local_username' => get('user'), 40 | 'rollbar_username' => get('rollbar_username'), 41 | 'comment' => get('rollbar_comment'), 42 | ]; 43 | 44 | Httpie::post('https://api.rollbar.com/api/1/deploy/') 45 | ->formBody($params) 46 | ->send(); 47 | }) 48 | ->once(); 49 | -------------------------------------------------------------------------------- /src/Command/CustomOption.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Host\Host; 14 | 15 | trait CustomOption 16 | { 17 | /** 18 | * @param Host[] $hosts 19 | * @param string[] $options 20 | */ 21 | protected function applyOverrides(array $hosts, array $options) 22 | { 23 | $override = []; 24 | foreach ($options as $option) { 25 | [$name, $value] = explode('=', $option); 26 | $value = $this->castValueToPhpType(trim($value)); 27 | $override[trim($name)] = $value; 28 | } 29 | 30 | foreach ($hosts as $host) { 31 | foreach ($override as $key => $value) { 32 | $host->set($key, $value); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @param mixed $value 39 | * @return bool|mixed 40 | */ 41 | protected function castValueToPhpType($value) 42 | { 43 | switch ($value) { 44 | case 'true': 45 | return true; 46 | case 'false': 47 | return false; 48 | default: 49 | return $value; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contrib/raygun.php: -------------------------------------------------------------------------------- 1 | get('raygun_api_key'), 31 | 'version' => get('raygun_version'), 32 | 'ownerName' => get('raygun_owner_name'), 33 | 'emailAddress' => get('raygun_email'), 34 | 'comment' => get('raygun_comment'), 35 | 'scmIdentifier' => get('raygun_scm_identifier'), 36 | 'scmType' => get('raygun_scm_type'), 37 | ]; 38 | 39 | Httpie::post('https://app.raygun.io/deployments') 40 | ->jsonBody($data) 41 | ->send(); 42 | }); 43 | -------------------------------------------------------------------------------- /contrib/bugsnag.php: -------------------------------------------------------------------------------- 1 | get('bugsnag_api_key'), 27 | 'releaseStage' => get('target'), 28 | 'repository' => get('repository'), 29 | 'provider' => get('bugsnag_provider', ''), 30 | 'branch' => get('branch'), 31 | 'revision' => runLocally('git log -n 1 --format="%h"'), 32 | 'appVersion' => get('bugsnag_app_version', ''), 33 | ]; 34 | 35 | Httpie::post('https://notify.bugsnag.com/deploy') 36 | ->jsonBody($data) 37 | ->send(); 38 | }); 39 | -------------------------------------------------------------------------------- /recipe/deploy/vendors.php: -------------------------------------------------------------------------------- 1 | &1'); 33 | }); 34 | -------------------------------------------------------------------------------- /src/Command/CommandCommon.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Support\Reporter; 15 | 16 | trait CommandCommon 17 | { 18 | /** 19 | * Collecting anonymous stat helps Deployer team improve developer experience. 20 | * If you are not comfortable with this, you will always be able to disable this 21 | * by setting DO_NOT_TRACK environment variable to `1`. 22 | * @codeCoverageIgnore 23 | */ 24 | protected function telemetry(array $data = []): void 25 | { 26 | if (getenv('DO_NOT_TRACK') === 'true') { 27 | return; 28 | } 29 | try { 30 | Reporter::report(array_merge([ 31 | 'command_name' => $this->getName(), 32 | 'deployer_version' => DEPLOYER_VERSION, 33 | 'deployer_phar' => Deployer::isPharArchive(), 34 | 'php_version' => phpversion(), 35 | 'os' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : (stristr(PHP_OS, 'DAR') ? 'OSX' : (stristr(PHP_OS, 'WIN') ? 'WIN' : (stristr(PHP_OS, 'LINUX') ? 'LINUX' : PHP_OS))), 36 | ], $data)); 37 | } catch (\Throwable $e) { 38 | return; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /contrib/hipchat.php: -------------------------------------------------------------------------------- 1 | get('hipchat_room_id'), 35 | 'from' => get('target'), 36 | 'message' => get('hipchat_message'), 37 | 'color' => get('hipchat_color'), 38 | 'auth_token' => get('hipchat_token'), 39 | 'notify' => 0, 40 | 'format' => 'json', 41 | ]; 42 | 43 | Httpie::get(get('hipchat_url')) 44 | ->query($params) 45 | ->send(); 46 | }); 47 | -------------------------------------------------------------------------------- /src/Logger/Logger.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Logger; 12 | 13 | use Deployer\ProcessRunner\Printer; 14 | use Deployer\Host\Host; 15 | use Deployer\Logger\Handler\HandlerInterface; 16 | 17 | class Logger 18 | { 19 | /** 20 | * @var HandlerInterface 21 | */ 22 | private $handler; 23 | 24 | public function __construct(HandlerInterface $handler) 25 | { 26 | $this->handler = $handler; 27 | } 28 | 29 | public function log(string $message): void 30 | { 31 | $this->handler->log("$message\n"); 32 | } 33 | 34 | public function callback(Host $host): \Closure 35 | { 36 | return function ($type, $buffer) use ($host) { 37 | $this->printBuffer($host, $type, $buffer); 38 | }; 39 | } 40 | 41 | public function printBuffer(Host $host, string $type, string $buffer): void 42 | { 43 | foreach (explode("\n", rtrim($buffer)) as $line) { 44 | $this->writeln($host, $type, $line); 45 | } 46 | } 47 | 48 | public function writeln(Host $host, string $type, string $line): void 49 | { 50 | // Omit empty lines 51 | if (empty($line)) { 52 | return; 53 | } 54 | 55 | $this->log("[{$host->getAlias()}] $line"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Executor/Worker.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Executor; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Exception\Exception; 15 | use Deployer\Exception\GracefulShutdownException; 16 | use Deployer\Exception\RunException; 17 | use Deployer\Host\Host; 18 | use Deployer\Task\Context; 19 | use Deployer\Task\Task; 20 | use Throwable; 21 | 22 | class Worker 23 | { 24 | private Deployer $deployer; 25 | 26 | public function __construct(Deployer $deployer) 27 | { 28 | $this->deployer = $deployer; 29 | } 30 | 31 | public function execute(Task $task, Host $host): int 32 | { 33 | try { 34 | Exception::setTaskSourceLocation($task->getSourceLocation()); 35 | 36 | $context = new Context($host); 37 | $task->run($context); 38 | 39 | if ($task->getName() !== 'connect') { 40 | $this->deployer->messenger->endOnHost($host); 41 | } 42 | return 0; 43 | } catch (Throwable $e) { 44 | $this->deployer->messenger->renderException($e, $host); 45 | if ($e instanceof GracefulShutdownException) { 46 | return GracefulShutdownException::EXIT_CODE; 47 | } 48 | if ($e instanceof RunException) { 49 | return $e->getExitCode(); 50 | } 51 | return 255; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Dumper.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Dumper 13 | { 14 | /** 15 | * Returns the components of a Version instance. 16 | * 17 | * @param Version $version A version. 18 | * 19 | * @return array The components. 20 | */ 21 | public static function toComponents(Version $version) 22 | { 23 | return [ 24 | Parser::MAJOR => $version->getMajor(), 25 | Parser::MINOR => $version->getMinor(), 26 | Parser::PATCH => $version->getPatch(), 27 | Parser::PRE_RELEASE => $version->getPreRelease(), 28 | Parser::BUILD => $version->getBuild(), 29 | ]; 30 | } 31 | 32 | /** 33 | * Returns the string representation of a Version instance. 34 | * 35 | * @param Version $version A version. 36 | * 37 | * @return string The string representation. 38 | */ 39 | public static function toString(Version $version) 40 | { 41 | return sprintf( 42 | '%d.%d.%d%s%s', 43 | $version->getMajor(), 44 | $version->getMinor(), 45 | $version->getPatch(), 46 | $version->getPreRelease() 47 | ? '-' . join('.', $version->getPreRelease()) 48 | : '', 49 | $version->getBuild() 50 | ? '+' . join('.', $version->getBuild()) 51 | : '', 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contrib/php-fpm.php: -------------------------------------------------------------------------------- 1 | get('user'), 43 | 'revision' => get('newrelic_revision'), 44 | 'description' => get('newrelic_description'), 45 | ]; 46 | 47 | Httpie::post("https://$endpoint/v2/applications/$appId/deployments.json") 48 | ->header("X-Api-Key", $apiKey) 49 | ->query(['deployment' => $data]) 50 | ->send(); 51 | } 52 | }) 53 | ->once() 54 | ->hidden(); 55 | -------------------------------------------------------------------------------- /recipe/silverstripe.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Support; 12 | 13 | use Deployer\Utility\Httpie; 14 | use Symfony\Component\Process\PhpProcess; 15 | 16 | /** 17 | * @codeCoverageIgnore 18 | */ 19 | class Reporter 20 | { 21 | public static function report(array $stats): void 22 | { 23 | $version = DEPLOYER_VERSION; 24 | $body = json_encode($stats); 25 | $length = strlen($body); 26 | $php = new PhpProcess(<<start(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bin/docgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace Deployer; 10 | 11 | use Deployer\Documentation\ApiGen; 12 | use Deployer\Documentation\DocGen; 13 | use Symfony\Component\Console\Application; 14 | use Symfony\Component\Console\Input\ArgvInput; 15 | use Symfony\Component\Console\Output\ConsoleOutput; 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | chdir(realpath(__DIR__ . '/..')); 20 | 21 | $input = new ArgvInput(); 22 | $output = new ConsoleOutput(); 23 | $app = new Application('DocGen', '1.0.0'); 24 | $app->setDefaultCommand('all'); 25 | 26 | $api = function () use ($output) { 27 | $parser = new ApiGen(); 28 | $parser->parse(file_get_contents(__DIR__ . '/../src/functions.php')); 29 | $md = $parser->markdown(); 30 | file_put_contents(__DIR__ . '/../docs/api.md', $md); 31 | $output->writeln('API Reference documentation updated.'); 32 | }; 33 | 34 | $recipes = function () use ($input, $output) { 35 | $docgen = new DocGen(__DIR__ . '/..'); 36 | $docgen->parse(__DIR__ . '/../recipe'); 37 | $docgen->parse(__DIR__ . '/../contrib'); 38 | 39 | if ($input->getOption('json')) { 40 | echo json_encode($docgen->recipes, JSON_PRETTY_PRINT); 41 | return; 42 | } 43 | 44 | $docgen->gen(__DIR__ . '/../docs'); 45 | $output->writeln('Recipes documentation updated.'); 46 | }; 47 | 48 | $app->register('api')->setCode($api); 49 | $app->register('recipes')->setCode($recipes)->addOption('json'); 50 | $app->register('all')->setCode(function () use ($recipes, $api) { 51 | $api(); 52 | $recipes(); 53 | echo `git status`; 54 | })->addOption('json'); 55 | 56 | $app->run($input, $output); 57 | -------------------------------------------------------------------------------- /src/Executor/Planner.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Executor; 12 | 13 | use Deployer\Host\Host; 14 | use Deployer\Task\Task; 15 | use Symfony\Component\Console\Helper\Table; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | 18 | class Planner 19 | { 20 | /** 21 | * @var Table 22 | */ 23 | private $table; 24 | /** 25 | * @var array 26 | */ 27 | private $template; 28 | 29 | /** 30 | * Planner constructor. 31 | * 32 | * @param Host[] $hosts 33 | */ 34 | public function __construct(OutputInterface $output, array $hosts) 35 | { 36 | $headers = []; 37 | $this->template = []; 38 | foreach ($hosts as $host) { 39 | $headers[] = $host->getTag(); 40 | $this->template[] = $host->getAlias(); 41 | } 42 | $this->table = new Table($output); 43 | $this->table->setHeaders($headers); 44 | $this->table->setStyle('box'); 45 | } 46 | 47 | /** 48 | * @param Host[] $hosts 49 | */ 50 | public function commit(array $hosts, Task $task): void 51 | { 52 | $row = []; 53 | foreach ($this->template as $alias) { 54 | $on = "-"; 55 | foreach ($hosts as $host) { 56 | if ($alias === $host->getAlias()) { 57 | $on = $task->getName(); 58 | break; 59 | } 60 | } 61 | $row[] = $on; 62 | } 63 | $this->table->addRow($row); 64 | } 65 | 66 | public function render() 67 | { 68 | $this->table->render(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Validator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Validator 13 | { 14 | /** 15 | * The regular expression for a valid identifier. 16 | */ 17 | public const IDENTIFIER_REGEX = '/^[0-9A-Za-z\-]+$/'; 18 | 19 | /** 20 | * The regular expression for a valid semantic version number. 21 | */ 22 | public const VERSION_REGEX = '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/'; 23 | 24 | /** 25 | * Checks if a identifier is valid. 26 | * 27 | * @param string $identifier A identifier. 28 | * 29 | * @return boolean TRUE if the identifier is valid, FALSE If not. 30 | */ 31 | public static function isIdentifier(string $identifier): bool 32 | { 33 | return (true == preg_match(self::IDENTIFIER_REGEX, $identifier)); 34 | } 35 | 36 | /** 37 | * Checks if a number is a valid version number. 38 | * 39 | * @param integer $number A number. 40 | * 41 | * @return boolean TRUE if the number is valid, FALSE If not. 42 | */ 43 | public static function isNumber(int $number): bool 44 | { 45 | return (true == preg_match('/^(0|[1-9]\d*)$/', (string) $number)); 46 | } 47 | 48 | /** 49 | * Checks if the string representation of a version number is valid. 50 | * 51 | * @param string $version The string representation. 52 | * 53 | * @return boolean TRUE if the string representation is valid, FALSE if not. 54 | */ 55 | public static function isVersion(string $version): bool 56 | { 57 | return (true == preg_match(self::VERSION_REGEX, $version)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /recipe/deploy/check_remote.php: -------------------------------------------------------------------------------- 1 | getOption('revision'); 21 | 22 | if (!$targetRevision) { 23 | $ref = 'HEAD'; 24 | $opt = ''; 25 | if ($tag = input()->getOption('tag')) { 26 | $ref = $tag; 27 | $opt = '--tags'; 28 | } elseif ($branch = get('branch')) { 29 | $ref = $branch; 30 | $opt = '--heads'; 31 | } 32 | $remoteLs = runLocally("git ls-remote $opt $repository $ref"); 33 | if (strstr($remoteLs, "\n")) { 34 | throw new Exception("Could not determine target revision. '$ref' matched multiple commits."); 35 | } 36 | if (!$remoteLs) { 37 | throw new Exception("Could not resolve a revision from '$ref'."); 38 | } 39 | $targetRevision = substr($remoteLs, 0, strpos($remoteLs, "\t")); 40 | } 41 | 42 | // Compare commit hashes. We use strpos to support short versions. 43 | $targetRevision = trim($targetRevision); 44 | $lastDeployedRevision = run('cat {{current_path}}/REVISION'); 45 | if ($targetRevision && strpos($lastDeployedRevision, $targetRevision) === 0) { 46 | throw new GracefulShutdownException("Already up-to-date."); 47 | } 48 | 49 | info("deployed different version"); 50 | }); 51 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | use Throwable; 14 | 15 | class Exception extends \Exception 16 | { 17 | /** 18 | * @var string 19 | */ 20 | private static $taskSourceLocation = ''; 21 | /** 22 | * @var string 23 | */ 24 | private $taskFilename = ''; 25 | /** 26 | * @var int|mixed 27 | */ 28 | private $taskLineNumber = 0; 29 | 30 | public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) 31 | { 32 | if (function_exists('debug_backtrace')) { 33 | $trace = debug_backtrace(); 34 | foreach ($trace as $t) { 35 | if (!empty($t['file']) && $t['file'] === self::$taskSourceLocation) { 36 | $this->taskFilename = basename($t['file']); 37 | $this->taskLineNumber = $t['line']; 38 | break; 39 | } 40 | } 41 | } 42 | parent::__construct($message, $code, $previous); 43 | } 44 | 45 | public static function setTaskSourceLocation(string $filepath): void 46 | { 47 | self::$taskSourceLocation = $filepath; 48 | } 49 | 50 | public function getTaskFilename(): string 51 | { 52 | return $this->taskFilename; 53 | } 54 | 55 | public function getTaskLineNumber(): int 56 | { 57 | return $this->taskLineNumber; 58 | } 59 | 60 | public function setTaskFilename(string $taskFilename): void 61 | { 62 | $this->taskFilename = $taskFilename; 63 | } 64 | 65 | public function setTaskLineNumber(int $taskLineNumber): void 66 | { 67 | $this->taskLineNumber = $taskLineNumber; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deployer/deployer", 3 | "description": "Deployment Tool", 4 | "license": "MIT", 5 | "homepage": "https://deployer.org", 6 | "support": { 7 | "docs": "https://deployer.org/docs", 8 | "source": "https://github.com/deployphp/deployer", 9 | "issues": "https://github.com/deployphp/deployer/issues" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Anton Medvedev", 14 | "email": "anton@medv.io" 15 | } 16 | ], 17 | "funding": [ 18 | { 19 | "type": "github", 20 | "url": "https://github.com/sponsors/antonmedv" 21 | } 22 | ], 23 | "autoload": { 24 | "psr-4": { 25 | "Deployer\\": "src/" 26 | }, 27 | "files": [ 28 | "src/functions.php", 29 | "src/Support/helpers.php" 30 | ] 31 | }, 32 | "scripts": { 33 | "test": "pest", 34 | "test:e2e": "pest --config tests/e2e/phpunit-e2e.xml", 35 | "check": "php-cs-fixer check", 36 | "fix": "php-cs-fixer fix", 37 | "phpstan": "phpstan analyse -c phpstan.neon --memory-limit 1G", 38 | "phpstan:baseline": "@phpstan --generate-baseline tests/phpstan-baseline.neon" 39 | }, 40 | "bin": [ 41 | "bin/dep" 42 | ], 43 | "require": { 44 | "php": ">=8.2", 45 | "symfony/console": "^7.2", 46 | "symfony/process": "^7.2", 47 | "symfony/yaml": "^7.2" 48 | }, 49 | "require-dev": { 50 | "friendsofphp/php-cs-fixer": "^3.68", 51 | "pestphp/pest": "^3.3", 52 | "phpstan/phpstan": "^1.4", 53 | "phpunit/php-code-coverage": "^11.0", 54 | "phpunit/phpunit": "^11.4" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "process-timeout": 0, 59 | "allow-plugins": { 60 | "pestphp/pest-plugin": true, 61 | "dealerdirect/phpcodesniffer-composer-installer": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Exception/RunException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Exception; 12 | 13 | use Deployer\Host\Host; 14 | use Symfony\Component\Process\Process; 15 | 16 | class RunException extends Exception 17 | { 18 | /** 19 | * @var Host 20 | */ 21 | private $host; 22 | /** 23 | * @var string 24 | */ 25 | private $command; 26 | /** 27 | * @var int 28 | */ 29 | private $exitCode; 30 | /** 31 | * @var string 32 | */ 33 | private $output; 34 | /** 35 | * @var string 36 | */ 37 | private $errorOutput; 38 | 39 | public function __construct( 40 | Host $host, 41 | string $command, 42 | int $exitCode, 43 | string $output, 44 | string $errorOutput, 45 | ) { 46 | $this->host = $host; 47 | $this->command = $command; 48 | $this->exitCode = $exitCode; 49 | $this->output = $output; 50 | $this->errorOutput = $errorOutput; 51 | 52 | $message = sprintf('The command "%s" failed.', $command); 53 | parent::__construct($message, $exitCode); 54 | } 55 | 56 | public function getHost(): Host 57 | { 58 | return $this->host; 59 | } 60 | 61 | public function getCommand(): string 62 | { 63 | return $this->command; 64 | } 65 | 66 | public function getExitCode(): int 67 | { 68 | return $this->exitCode; 69 | } 70 | 71 | public function getExitCodeText(): string 72 | { 73 | return Process::$exitCodes[$this->exitCode] ?? 'Unknown error'; 74 | } 75 | 76 | public function getOutput(): string 77 | { 78 | return $this->output; 79 | } 80 | 81 | public function getErrorOutput(): string 82 | { 83 | return $this->errorOutput; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ProcessRunner/Printer.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\ProcessRunner; 12 | 13 | use Deployer\Host\Host; 14 | use Symfony\Component\Console\Output\OutputInterface; 15 | 16 | class Printer 17 | { 18 | private OutputInterface $output; 19 | 20 | public function __construct(OutputInterface $output) 21 | { 22 | $this->output = $output; 23 | } 24 | 25 | public function command(Host $host, string $type, string $command): void 26 | { 27 | // -v for run command 28 | if ($this->output->isVerbose()) { 29 | $this->output->writeln("[$host] $type $command"); 30 | } 31 | } 32 | 33 | /** 34 | * Returns a callable for use with the symfony Process->run($callable) method. 35 | * 36 | * @return callable A function expecting a int $type (e.g. Process::OUT or Process::ERR) and string $buffer parameters. 37 | */ 38 | public function callback(Host $host, bool $forceOutput): callable 39 | { 40 | return function ($type, $buffer) use ($forceOutput, $host) { 41 | if ($this->output->isVerbose() || $forceOutput) { 42 | $this->printBuffer($type, $host, $buffer); 43 | } 44 | }; 45 | } 46 | 47 | /** 48 | * @param string $type Process::OUT or Process::ERR 49 | */ 50 | public function printBuffer(string $type, Host $host, string $buffer): void 51 | { 52 | foreach (explode("\n", rtrim($buffer)) as $line) { 53 | $this->writeln($type, $host, $line); 54 | } 55 | } 56 | 57 | public function writeln(string $type, Host $host, string $line): void 58 | { 59 | // Omit empty lines 60 | if (empty($line)) { 61 | return; 62 | } 63 | 64 | $this->output->writeln("[$host] $line"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Collection/Collection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Collection; 12 | 13 | use Countable; 14 | use IteratorAggregate; 15 | 16 | class Collection implements Countable, IteratorAggregate 17 | { 18 | protected array $values = []; 19 | 20 | public function all(): array 21 | { 22 | return $this->values; 23 | } 24 | 25 | public function get(string $name): mixed 26 | { 27 | if ($this->has($name)) { 28 | return $this->values[$name]; 29 | } 30 | throw $this->notFound($name); 31 | } 32 | 33 | public function has(string $name): bool 34 | { 35 | return array_key_exists($name, $this->values); 36 | } 37 | 38 | public function set(string $name, mixed $object) 39 | { 40 | $this->values[$name] = $object; 41 | } 42 | 43 | public function remove(string $name): void 44 | { 45 | if ($this->has($name)) { 46 | unset($this->values[$name]); 47 | } 48 | throw $this->notFound($name); 49 | } 50 | 51 | public function count(): int 52 | { 53 | return count($this->values); 54 | } 55 | 56 | public function select(callable $callback): array 57 | { 58 | $values = []; 59 | 60 | foreach ($this->values as $key => $value) { 61 | if ($callback($value, $key)) { 62 | $values[$key] = $value; 63 | } 64 | } 65 | 66 | return $values; 67 | } 68 | 69 | /** 70 | * @return \ArrayIterator|\Traversable 71 | */ 72 | #[\ReturnTypeWillChange] 73 | public function getIterator() 74 | { 75 | return new \ArrayIterator($this->values); 76 | } 77 | 78 | protected function notFound(string $name): \InvalidArgumentException 79 | { 80 | return new \InvalidArgumentException("Element \"$name\" not found in collection."); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contrib/grafana.php: -------------------------------------------------------------------------------- 1 | 'eyJrIj...', 17 | 'url' => 'http://grafana/api/annotations', 18 | 'tags' => ['deploy', 'production'], 19 | ]); 20 | 21 | ``` 22 | 23 | ## Usage 24 | 25 | If you want to create annotation about successful end of deployment. 26 | 27 | ```php 28 | after('deploy:success', 'grafana:annotation'); 29 | ``` 30 | 31 | */ 32 | 33 | namespace Deployer; 34 | 35 | use Deployer\Utility\Httpie; 36 | 37 | desc('Creates Grafana annotation of deployment'); 38 | task('grafana:annotation', function () { 39 | $defaultConfig = [ 40 | 'url' => null, 41 | 'token' => null, 42 | 'time' => round(microtime(true) * 1000), 43 | 'tags' => [], 44 | 'text' => null, 45 | ]; 46 | 47 | $config = array_merge($defaultConfig, (array) get('grafana')); 48 | if (!is_array($config) || !isset($config['url']) || !isset($config['token'])) { 49 | throw new \RuntimeException("Please configure Grafana: set('grafana', ['url' => 'https://localhost/api/annotations', token' => 'eyJrIjo...']);"); 50 | } 51 | 52 | $params = [ 53 | 'time' => $config['time'], 54 | 'isRegion' => false, 55 | 'tags' => $config['tags'], 56 | 'text' => $config['text'], 57 | ]; 58 | if (!isset($params['text'])) { 59 | $params['text'] = 'Deployed ' . trim(runLocally('git log -n 1 --format="%h"')); 60 | } 61 | 62 | Httpie::post($config['url']) 63 | ->header('Authorization', 'Bearer ' . $config['token']) 64 | ->jsonBody($params) 65 | ->send(); 66 | }); 67 | -------------------------------------------------------------------------------- /src/Command/WorkerCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Executor\Worker; 15 | use Deployer\Host\Localhost; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Input\InputOption as Option; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | use function Deployer\localhost; 21 | 22 | class WorkerCommand extends MainCommand 23 | { 24 | public function __construct(Deployer $deployer) 25 | { 26 | parent::__construct('worker', null, $deployer); 27 | $this->setHidden(true); 28 | } 29 | 30 | protected function configure() 31 | { 32 | parent::configure(); 33 | $this->addOption('task', null, Option::VALUE_REQUIRED); 34 | $this->addOption('host', null, Option::VALUE_REQUIRED); 35 | $this->addOption('port', null, Option::VALUE_REQUIRED); 36 | $this->addOption('decorated', null, Option::VALUE_NONE); 37 | } 38 | 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | $this->deployer->input = $input; 42 | $this->deployer->output = $output; 43 | $this->deployer['log'] = $input->getOption('log'); 44 | $output->setDecorated($input->getOption('decorated')); 45 | if (!$output->isDecorated() && !defined('NO_ANSI')) { 46 | define('NO_ANSI', 'true'); 47 | } 48 | 49 | define('MASTER_ENDPOINT', 'http://localhost:' . $input->getOption('port')); 50 | 51 | $task = $this->deployer->tasks->get($input->getOption('task')); 52 | $host = $this->deployer->hosts->get($input->getOption('host')); 53 | $host->config()->load(); 54 | 55 | $worker = new Worker($this->deployer); 56 | $exitCode = $worker->execute($task, $host); 57 | 58 | $host->config()->save(); 59 | return $exitCode; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Task/Context.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Task; 12 | 13 | use Deployer\Configuration; 14 | use Deployer\Exception\Exception; 15 | use Deployer\Host\Host; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | class Context 20 | { 21 | private Host $host; 22 | 23 | /** 24 | * @var Context[] 25 | */ 26 | private static array $contexts = []; 27 | 28 | public function __construct(Host $host) 29 | { 30 | $this->host = $host; 31 | } 32 | 33 | public static function push(Context $context): void 34 | { 35 | self::$contexts[] = $context; 36 | } 37 | 38 | public static function has(): bool 39 | { 40 | return !empty(self::$contexts); 41 | } 42 | 43 | public static function get(): Context 44 | { 45 | if (empty(self::$contexts)) { 46 | throw new Exception("Context was requested but was not available."); 47 | } 48 | return end(self::$contexts); 49 | } 50 | 51 | public static function pop(): ?Context 52 | { 53 | return array_pop(self::$contexts); 54 | } 55 | 56 | /** 57 | * Throws a Exception when not called within a task-context and therefore no Context is available. 58 | * 59 | * This method provides a useful error to the end-user to make him/her aware 60 | * to use a function in the required task-context. 61 | * 62 | * @throws Exception 63 | */ 64 | public static function required(string $callerName): void 65 | { 66 | if (empty(self::$contexts)) { 67 | throw new Exception("'$callerName' can only be used within a task."); 68 | } 69 | } 70 | 71 | public function getConfig(): Configuration 72 | { 73 | return $this->host->config(); 74 | } 75 | 76 | public function getHost(): Host 77 | { 78 | return $this->host; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /recipe/drupal7.php: -------------------------------------------------------------------------------- 1 | $value) { 50 | $keys = []; 51 | for ($i = $iterator->getDepth(); $i > 0; $i--) { 52 | $keys[] = $iterator->getSubIterator($i - 1)->key(); 53 | } 54 | $keys[] = $key; 55 | 56 | $replacements['{{' . implode('.', $keys) . '}}'] = $value; 57 | } 58 | 59 | //Create settings from template 60 | $settings = file_get_contents($template); 61 | 62 | $settings = strtr($settings, $replacements); 63 | 64 | writeln('settings.php created successfully'); 65 | 66 | $tmpFilename = tempnam(sys_get_temp_dir(), 'tmp_settings_'); 67 | file_put_contents($tmpFilename, $settings); 68 | 69 | upload($tmpFilename, '{{deploy_path}}/shared/sites/{{drupal_site}}/settings.php'); 70 | 71 | unlink($tmpFilename); 72 | } 73 | }); 74 | 75 | //Upload Drupal 7 files folder 76 | task('drupal:upload_files', function () { 77 | if (askConfirmation('Are you sure?')) { 78 | upload('sites/{{drupal_site}}/files', '{{deploy_path}}/shared/sites/{{drupal_site}}/files'); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /recipe/symfony.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Deployer Logo 6 | 7 | 8 | Deployer 9 | 10 |

The PHP deployment tool with support for popular frameworks out of the box.

11 | 12 |



Deployer Screenshot


13 | 14 | --- 15 | 16 |

Special thanks to:

17 | 18 |

Warp

19 |

Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software, faster.

20 |

Visit warp.dev to learn more.

21 |
22 | 23 | --- 24 | 25 |

Browser testing via 26 | 27 | 28 | 29 |

30 | 31 | --- 32 | 33 | Build Status 34 | Latest Stable Version 35 | License 36 | 37 | See [deployer.org](https://deployer.org) for more information and documentation. 38 | 39 | ## Features 40 | 41 | - Automatic server **provisioning**. 42 | - **Zero downtime** deployments. 43 | - Ready to use recipes for **most frameworks**. 44 | 45 | ## Additional resources 46 | 47 | * [GitHub Action for Deployer](https://github.com/deployphp/action) 48 | * [Deployer Docker Image](https://hub.docker.com/r/deployphp/deployer) 49 | 50 | ## License 51 | [MIT](https://github.com/deployphp/deployer/blob/master/LICENSE) 52 | -------------------------------------------------------------------------------- /recipe/provision/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 Not Found 7 | 39 | 40 | 41 |
42 | 43 | 46 | 47 |

Not Found

48 |

The requested URL was not found on this server.

49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Ssh/IOArguments.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Ssh; 12 | 13 | use Deployer\Exception\Exception; 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Output\OutputInterface; 16 | 17 | class IOArguments 18 | { 19 | public static function collect(InputInterface $input, OutputInterface $output): array 20 | { 21 | $arguments = []; 22 | foreach ($input->getOptions() as $name => $value) { 23 | if (!$input->getOption($name)) { 24 | continue; 25 | } 26 | if ($name === 'file') { 27 | $arguments[] = "--file"; 28 | $arguments[] = ltrim($value, '='); 29 | continue; 30 | } 31 | if (in_array($name, ['verbose'], true)) { 32 | continue; 33 | } 34 | if (!is_array($value)) { 35 | $value = [$value]; 36 | } 37 | foreach ($value as $v) { 38 | if (is_bool($v)) { 39 | $arguments[] = "--$name"; 40 | continue; 41 | } 42 | 43 | $arguments[] = "--$name"; 44 | $arguments[] = $v; 45 | } 46 | } 47 | 48 | if ($output->isDecorated()) { 49 | $arguments[] = '--decorated'; 50 | } 51 | $verbosity = self::verbosity($output->getVerbosity()); 52 | if (!empty($verbosity)) { 53 | $arguments[] = $verbosity; 54 | } 55 | return $arguments; 56 | } 57 | 58 | private static function verbosity(int $verbosity): string 59 | { 60 | switch ($verbosity) { 61 | case OutputInterface::VERBOSITY_QUIET: 62 | return '-q'; 63 | case OutputInterface::VERBOSITY_NORMAL: 64 | return ''; 65 | case OutputInterface::VERBOSITY_VERBOSE: 66 | return '-v'; 67 | case OutputInterface::VERBOSITY_VERY_VERBOSE: 68 | return '-vv'; 69 | case OutputInterface::VERBOSITY_DEBUG: 70 | return '-vvv'; 71 | default: 72 | throw new Exception('Unknown verbosity level: ' . $verbosity); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /recipe/provision/website.php: -------------------------------------------------------------------------------- 1 | /var/deployer/404.html"); 22 | })->oncePerNode(); 23 | 24 | desc('Provision website'); 25 | task('provision:website', function () { 26 | $restoreBecome = become('deployer'); 27 | 28 | run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); 29 | run("chown -R deployer:deployer {{deploy_path}}"); 30 | 31 | set('deploy_path', run("realpath {{deploy_path}}")); 32 | cd('{{deploy_path}}'); 33 | 34 | run("[ -d log ] || mkdir log"); 35 | run("chgrp caddy log"); 36 | 37 | $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile')); 38 | 39 | if (test('[ -f Caddyfile ]')) { 40 | run("echo $'$caddyfile' > Caddyfile.new"); 41 | $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true); 42 | if (empty($diff)) { 43 | run('rm Caddyfile.new'); 44 | } else { 45 | info('Found Caddyfile changes'); 46 | writeln("\n" . $diff); 47 | $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0); 48 | if ($answer === 'old') { 49 | run('rm Caddyfile.new'); 50 | } else { 51 | run('mv Caddyfile.new Caddyfile'); 52 | } 53 | } 54 | } else { 55 | run("echo $'$caddyfile' > Caddyfile"); 56 | } 57 | 58 | $restoreBecome(); 59 | 60 | if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) { 61 | run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile"); 62 | } 63 | run('service caddy reload'); 64 | 65 | info("Website {{domain}} configured!"); 66 | })->limit(1); 67 | 68 | desc('Shows access logs'); 69 | task('logs:access', function () { 70 | run('tail -f {{deploy_path}}/log/access.log'); 71 | })->verbose(); 72 | 73 | desc('Shows caddy syslog'); 74 | task('logs:caddy', function () { 75 | run('sudo journalctl -u caddy -f'); 76 | })->verbose(); 77 | -------------------------------------------------------------------------------- /src/Command/ConfigCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Exception\WillAskUser; 15 | use Deployer\Task\Context; 16 | use Symfony\Component\Console\Input\InputInterface as Input; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\NullOutput; 19 | use Symfony\Component\Console\Output\OutputInterface as Output; 20 | use Symfony\Component\Yaml\Yaml; 21 | 22 | class ConfigCommand extends SelectCommand 23 | { 24 | public function __construct(Deployer $deployer) 25 | { 26 | parent::__construct('config', $deployer); 27 | $this->setDescription('Get all configuration options for hosts'); 28 | } 29 | 30 | protected function configure() 31 | { 32 | parent::configure(); 33 | $this->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (json, yaml)', 'yaml'); 34 | $this->getDefinition()->getArgument('selector')->setDefault(['all']); 35 | } 36 | 37 | protected function execute(Input $input, Output $output): int 38 | { 39 | $this->deployer->input = $input; 40 | $this->deployer->output = new NullOutput(); 41 | $hosts = $this->selectHosts($input, $output); 42 | $data = []; 43 | $keys = $this->deployer->config->keys(); 44 | define('DEPLOYER_NO_ASK', true); 45 | foreach ($hosts as $host) { 46 | Context::push(new Context($host)); 47 | $values = []; 48 | foreach ($keys as $key) { 49 | try { 50 | $values[$key] = $host->get($key); 51 | } catch (WillAskUser $exception) { 52 | $values[$key] = ['ask' => $exception->getMessage()]; 53 | } catch (\Throwable $exception) { 54 | $values[$key] = ['error' => $exception->getMessage()]; 55 | } 56 | } 57 | foreach ($host->config()->persist() as $k => $v) { 58 | $values[$k] = $v; 59 | } 60 | ksort($values); 61 | $data[$host->getAlias()] = $values; 62 | Context::pop(); 63 | } 64 | $format = $input->getOption('format'); 65 | switch ($format) { 66 | case 'json': 67 | $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); 68 | break; 69 | 70 | case 'yaml': 71 | $output->write(Yaml::dump($data)); 72 | break; 73 | 74 | default: 75 | throw new \Exception("Unknown format: $format."); 76 | } 77 | return 0; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Command/RunCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Task\Context; 15 | use Deployer\Task\Task; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface as Input; 18 | use Symfony\Component\Console\Input\InputOption as Option; 19 | use Symfony\Component\Console\Output\OutputInterface as Output; 20 | 21 | use function Deployer\cd; 22 | use function Deployer\get; 23 | use function Deployer\has; 24 | use function Deployer\run; 25 | use function Deployer\test; 26 | 27 | class RunCommand extends SelectCommand 28 | { 29 | use CustomOption; 30 | 31 | public function __construct(Deployer $deployer) 32 | { 33 | parent::__construct('run', $deployer); 34 | $this->setDescription('Run any arbitrary command on hosts'); 35 | } 36 | 37 | protected function configure() 38 | { 39 | $this->addArgument( 40 | 'command-to-run', 41 | InputArgument::REQUIRED, 42 | 'Command to run on a remote host', 43 | ); 44 | parent::configure(); 45 | $this->addOption( 46 | 'option', 47 | 'o', 48 | Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 49 | 'Set configuration option', 50 | ); 51 | $this->addOption( 52 | 'timeout', 53 | 't', 54 | Option::VALUE_REQUIRED, 55 | 'Command timeout in seconds', 56 | ); 57 | } 58 | 59 | protected function execute(Input $input, Output $output): int 60 | { 61 | $this->deployer->input = $input; 62 | $this->deployer->output = $output; 63 | 64 | $command = $input->getArgument('command-to-run') ?? ''; 65 | $hosts = $this->selectHosts($input, $output); 66 | $this->applyOverrides($hosts, $input->getOption('option')); 67 | 68 | $task = new Task($command, function () use ($input, $command) { 69 | if (has('current_path')) { 70 | $path = get('current_path'); 71 | if (test("[ -d $path ]")) { 72 | cd($path); 73 | } 74 | } 75 | run( 76 | $command, 77 | timeout: intval($input->getOption('timeout')), 78 | forceOutput: true, 79 | ); 80 | }); 81 | 82 | foreach ($hosts as $host) { 83 | try { 84 | $task->run(new Context($host)); 85 | } catch (\Throwable $exception) { 86 | $this->deployer->messenger->renderException($exception, $host); 87 | } 88 | } 89 | 90 | return 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /recipe/deploy/rollback.php: -------------------------------------------------------------------------------- 1 | $currentRelease."); 70 | 71 | if (!test("[ -d releases/$candidate ]")) { 72 | throw new \RuntimeException(parse("Release \"$candidate\" not found in \"{{deploy_path}}/releases\".")); 73 | } 74 | if (test("[ -f releases/$candidate/BAD_RELEASE ]")) { 75 | writeln("Candidate $candidate marked as bad release."); 76 | if (!askConfirmation("Continue rollback to $candidate?")) { 77 | writeln('Rollback aborted.'); 78 | return; 79 | } 80 | } 81 | writeln("Rolling back to $candidate release."); 82 | 83 | // Symlink to old release. 84 | run("{{bin/symlink}} releases/$candidate {{current_path}}"); 85 | 86 | // Mark release as bad. 87 | $timestamp = timestamp(); 88 | run("echo '$timestamp,{{user}}' > releases/$currentRelease/BAD_RELEASE"); 89 | 90 | writeln("rollback to release $candidate was successful"); 91 | }); 92 | -------------------------------------------------------------------------------- /src/ProcessRunner/ProcessRunner.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\ProcessRunner; 12 | 13 | use Deployer\Exception\RunException; 14 | use Deployer\Exception\TimeoutException; 15 | use Deployer\Host\Host; 16 | use Deployer\Logger\Logger; 17 | use Deployer\Ssh\RunParams; 18 | use Symfony\Component\Process\Exception\ProcessFailedException; 19 | use Symfony\Component\Process\Exception\ProcessTimedOutException; 20 | use Symfony\Component\Process\Process; 21 | 22 | use function Deployer\Support\deployer_root; 23 | use function Deployer\Support\env_stringify; 24 | 25 | class ProcessRunner 26 | { 27 | private Printer $pop; 28 | private Logger $logger; 29 | 30 | public function __construct(Printer $pop, Logger $logger) 31 | { 32 | $this->pop = $pop; 33 | $this->logger = $logger; 34 | } 35 | 36 | public function run(Host $host, string $command, RunParams $params): string 37 | { 38 | $this->pop->command($host, 'run', $command); 39 | 40 | $terminalOutput = $this->pop->callback($host, $params->forceOutput); 41 | $callback = function ($type, $buffer) use ($host, $terminalOutput) { 42 | $this->logger->printBuffer($host, $type, $buffer); 43 | $terminalOutput($type, $buffer); 44 | }; 45 | 46 | if (!empty($params->secrets)) { 47 | foreach ($params->secrets as $key => $value) { 48 | $command = str_replace('%' . $key . '%', $value, $command); 49 | } 50 | } 51 | 52 | if (!empty($params->env)) { 53 | $env = env_stringify($params->env); 54 | $command = "export $env; $command"; 55 | } 56 | 57 | if (!empty($params->dotenv)) { 58 | $command = "source $params->dotenv; $command"; 59 | } 60 | 61 | $process = Process::fromShellCommandline($params->shell) 62 | ->setInput($command) 63 | ->setTimeout($params->timeout) 64 | ->setIdleTimeout($params->idleTimeout) 65 | ->setWorkingDirectory($params->cwd ?? deployer_root()); 66 | 67 | try { 68 | $process->mustRun($callback); 69 | return $process->getOutput(); 70 | } catch (ProcessFailedException) { 71 | if ($params->nothrow) { 72 | return ''; 73 | } 74 | throw new RunException( 75 | $host, 76 | $command, 77 | $process->getExitCode(), 78 | $process->getOutput(), 79 | $process->getErrorOutput(), 80 | ); 81 | } catch (ProcessTimedOutException $exception) { // @phpstan-ignore-line PHPStan doesn't know about ProcessTimedOutException for some reason. 82 | throw new TimeoutException( 83 | $command, 84 | $exception->getExceededTimeout(), 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /recipe/provision/databases.php: -------------------------------------------------------------------------------- 1 | limit(1); 38 | 39 | desc('Provision MySQL'); 40 | task('provision:mysql', function () { 41 | run('apt-get install -y mysql-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); 42 | run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); 43 | run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); 44 | run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); 45 | run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); 46 | run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); 47 | run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); 48 | }); 49 | 50 | desc('Provision MariaDB'); 51 | task('provision:mariadb', function () { 52 | run('apt-get install -y mariadb-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); 53 | run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); 54 | run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); 55 | run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); 56 | run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); 57 | run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); 58 | run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); 59 | }); 60 | 61 | desc('Provision PostgreSQL'); 62 | task('provision:postgresql', function () { 63 | run('apt-get install -y postgresql postgresql-contrib', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); 64 | run("sudo -u postgres psql <<< $'CREATE DATABASE {{db_name}};'"); 65 | run("sudo -u postgres psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%secret%\';'", secret: get('db_password')); 66 | run("sudo -u postgres psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'"); 67 | }); 68 | -------------------------------------------------------------------------------- /recipe/provision/user.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>&1')) { 17 | // TODO: Check what created deployer user configured correctly. 18 | // TODO: Update sudo_password of deployer user. 19 | // TODO: Copy ssh_copy_id to deployer ssh dir. 20 | info('deployer user already exist'); 21 | } else { 22 | run('useradd deployer'); 23 | run('mkdir -p /home/deployer/.ssh'); 24 | run('mkdir -p /home/deployer/.deployer'); 25 | run('adduser deployer sudo'); 26 | 27 | run('chsh -s /bin/bash deployer'); 28 | run('cp /root/.profile /home/deployer/.profile'); 29 | run('cp /root/.bashrc /home/deployer/.bashrc'); 30 | run('touch /home/deployer/.sudo_as_admin_successful'); 31 | 32 | // Make color prompt. 33 | run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); 34 | 35 | $password = run("mkpasswd -m sha-512 '%secret%'", secret: get('sudo_password')); 36 | run("usermod --password '%secret%' deployer", secret: $password); 37 | 38 | // Copy root public key to deployer user so user can login without password. 39 | run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); 40 | 41 | // Create ssh key if not already exists. 42 | run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); 43 | 44 | try { 45 | run('chown -R deployer:deployer /home/deployer'); 46 | run('chmod -R 755 /home/deployer'); 47 | run('chmod 700 /home/deployer/.ssh'); 48 | run('chmod 600 /home/deployer/.ssh/id_ed25519'); 49 | run('chmod 600 /home/deployer/.ssh/authorized_keys'); 50 | } catch (\Throwable $e) { 51 | warning($e->getMessage()); 52 | } 53 | 54 | run('usermod -a -G www-data deployer'); 55 | run('usermod -a -G caddy deployer'); 56 | } 57 | })->oncePerNode(); 58 | 59 | 60 | desc('Copy public key to remote server'); 61 | task('provision:ssh_copy_id', function () { 62 | $defaultKeys = [ 63 | '~/.ssh/id_rsa.pub', 64 | '~/.ssh/id_ed25519.pub', 65 | '~/.ssh/id_ecdsa.pub', 66 | '~/.ssh/id_dsa.pub', 67 | ]; 68 | 69 | $publicKeyContent = false; 70 | foreach ($defaultKeys as $key) { 71 | $file = parse_home_dir($key); 72 | if (file_exists($file)) { 73 | $publicKeyContent = file_get_contents($file); 74 | break; 75 | } 76 | } 77 | 78 | if (!$publicKeyContent) { 79 | $publicKeyContent = ask(' Public key: ', ''); 80 | } 81 | 82 | if (empty($publicKeyContent)) { 83 | info('Skipping public key copy as no public key was found or provided.'); 84 | return; 85 | } 86 | 87 | run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', env: ['PUBLIC_KEY' => $publicKeyContent]); 88 | }); 89 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Manager.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Manager 17 | { 18 | /** 19 | * The update manifest. 20 | * 21 | * @var Manifest 22 | */ 23 | private $manifest; 24 | 25 | /** 26 | * The running file (the Phar that will be updated). 27 | * 28 | * @var string 29 | */ 30 | private $runningFile; 31 | 32 | /** 33 | * Sets the update manifest. 34 | * 35 | * @param Manifest $manifest The manifest. 36 | */ 37 | public function __construct(Manifest $manifest) 38 | { 39 | $this->manifest = $manifest; 40 | } 41 | 42 | /** 43 | * Returns the manifest. 44 | * 45 | * @return Manifest The manifest. 46 | */ 47 | public function getManifest(): Manifest 48 | { 49 | return $this->manifest; 50 | } 51 | 52 | /** 53 | * Returns the running file (the Phar that will be updated). 54 | * 55 | * @return string The file. 56 | */ 57 | public function getRunningFile(): string 58 | { 59 | if (null === $this->runningFile) { 60 | $this->runningFile = realpath($_SERVER['argv'][0]); 61 | } 62 | 63 | return $this->runningFile; 64 | } 65 | 66 | /** 67 | * Sets the running file (the Phar that will be updated). 68 | * 69 | * @param string $file The file name or path. 70 | * 71 | * @throws Exception\Exception 72 | * @throws InvalidArgumentException If the file path is invalid. 73 | */ 74 | public function setRunningFile(string $file): void 75 | { 76 | if (false === is_file($file)) { 77 | throw InvalidArgumentException::create( 78 | 'The file "%s" is not a file or it does not exist.', 79 | $file, 80 | ); 81 | } 82 | 83 | $this->runningFile = $file; 84 | } 85 | 86 | /** 87 | * Updates the running Phar if any is available. 88 | * 89 | * @param string|Version $version The current version. 90 | * @param boolean $major Lock to current major version? 91 | * @param boolean $pre Allow pre-releases? 92 | * 93 | * @return boolean TRUE if an update was performed, FALSE if none available. 94 | */ 95 | public function update($version, bool $major = false, bool $pre = false): bool 96 | { 97 | if (false === ($version instanceof Version)) { 98 | $version = Parser::toVersion($version); 99 | } 100 | 101 | if (null !== ($update = $this->manifest->findRecent( 102 | $version, 103 | $major, 104 | $pre, 105 | ))) { 106 | $update->getFile(); 107 | $update->copyTo($this->getRunningFile()); 108 | 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /bin/dep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | // Check PHP version 10 | if (PHP_VERSION_ID < 80200) { 11 | fwrite(STDERR, "PHP 8.2 or higher is required.\n"); 12 | exit(1); 13 | } 14 | 15 | // Detect deploy.php location 16 | $deployFile = null; 17 | foreach ($argv as $i => $arg) { 18 | if (preg_match('/^(-f|--file)$/', $arg, $match) && $i + 1 < count($argv)) { 19 | $deployFile = $argv[$i + 1]; 20 | break; 21 | } 22 | if (preg_match('/^--file=(?.+)$/', $arg, $match)) { 23 | $deployFile = $match['file']; 24 | break; 25 | } 26 | if (preg_match('/^-f=?(?.+)$/', $arg, $match)) { 27 | $deployFile = $match['file']; 28 | break; 29 | } 30 | } 31 | if (!empty($deployFile)) { 32 | $deployFile = realpath($deployFile); 33 | } 34 | $lookUp = function (string $name): ?string { 35 | $dir = getcwd(); 36 | for ($i = 0; $i < 10; $i++) { 37 | $path = "$dir/$name"; 38 | if (is_readable($path)) { 39 | return $path; 40 | } 41 | $dir = dirname($dir); 42 | } 43 | return ''; 44 | }; 45 | if (empty($deployFile)) { 46 | $deployFile = $lookUp('deploy.php'); 47 | } 48 | if (empty($deployFile)) { 49 | $deployFile = $lookUp('deploy.yaml'); 50 | } 51 | if (empty($deployFile)) { 52 | $deployFile = $lookUp('deploy.yml'); 53 | } 54 | 55 | // Detect autoload location 56 | $autoload = [ 57 | __DIR__ . '/../vendor/autoload.php', // The dep located at "deployer.phar/bin" or in development. 58 | __DIR__ . '/../../../autoload.php', // The dep located at "vendor/deployer/deployer/bin". 59 | __DIR__ . '/../autoload.php', // The dep located at "vendor/bin". 60 | ]; 61 | $includes = [ 62 | __DIR__ . '/..', 63 | __DIR__ . '/../../../deployer/deployer', 64 | __DIR__ . '/../deployer/deployer', 65 | ]; 66 | $includePath = false; 67 | for ($i = 0; $i < count($autoload); $i++) { 68 | if (file_exists($autoload[$i]) && is_dir($includes[$i])) { 69 | require $autoload[$i]; 70 | $includePath = $includes[$i]; 71 | break; 72 | } 73 | } 74 | if (empty($includePath)) { 75 | fwrite(STDERR, "Error: The `autoload.php` file not found in:\n"); 76 | for ($i = 0; $i < count($autoload); $i++) { 77 | $a = file_exists($autoload[$i]) ? 'true' : 'false'; 78 | $b = is_dir($includes[$i]) ? 'true' : 'false'; 79 | fwrite(STDERR, " - file_exists($autoload[$i]) = $a\n"); 80 | fwrite(STDERR, " is_dir($includes[$i]) = $b\n"); 81 | } 82 | exit(1); 83 | } 84 | 85 | // Errors to exception 86 | set_error_handler(function ($severity, $message, $filename, $lineno) { 87 | if (error_reporting() == 0) { 88 | return; 89 | } 90 | if (error_reporting() & $severity) { 91 | throw new ErrorException($message, 0, $severity, $filename, $lineno); 92 | } 93 | }); 94 | 95 | // Enable recipe loading 96 | set_include_path($includePath . PATH_SEPARATOR . get_include_path()); 97 | 98 | // Deployer constants 99 | define('DEPLOYER', true); 100 | define('DEPLOYER_BIN', __FILE__); 101 | define('DEPLOYER_DEPLOY_FILE', $deployFile); 102 | 103 | Deployer\Deployer::run('master', $deployFile); 104 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Parser.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Parser 15 | { 16 | /** 17 | * The build metadata component. 18 | */ 19 | public const BUILD = 'build'; 20 | 21 | /** 22 | * The major version number component. 23 | */ 24 | public const MAJOR = 'major'; 25 | 26 | /** 27 | * The minor version number component. 28 | */ 29 | public const MINOR = 'minor'; 30 | 31 | /** 32 | * The patch version number component. 33 | */ 34 | public const PATCH = 'patch'; 35 | 36 | /** 37 | * The pre-release version number component. 38 | */ 39 | public const PRE_RELEASE = 'pre'; 40 | 41 | /** 42 | * Returns a Version builder for the string representation. 43 | * 44 | * @param string $version The string representation. 45 | * 46 | * @return Builder A Version builder. 47 | */ 48 | public static function toBuilder(string $version): Builder 49 | { 50 | return Builder::create()->importComponents( 51 | self::toComponents($version), 52 | ); 53 | } 54 | 55 | /** 56 | * Returns the components of the string representation. 57 | * 58 | * @param string $version The string representation. 59 | * 60 | * @return array The components of the version. 61 | * 62 | * @throws InvalidStringRepresentationException If the string representation 63 | * is invalid. 64 | */ 65 | public static function toComponents(string $version): array 66 | { 67 | if (!Validator::isVersion($version)) { 68 | throw new InvalidStringRepresentationException($version); 69 | } 70 | 71 | if (false !== strpos($version, '+')) { 72 | [$version, $build] = explode('+', $version); 73 | 74 | $build = explode('.', $build); 75 | } 76 | 77 | if (false !== strpos($version, '-')) { 78 | [$version, $pre] = explode('-', $version); 79 | 80 | $pre = explode('.', $pre); 81 | } 82 | 83 | [ 84 | $major, 85 | $minor, 86 | $patch, 87 | ] = explode('.', $version); 88 | 89 | return [ 90 | self::MAJOR => intval($major), 91 | self::MINOR => intval($minor), 92 | self::PATCH => intval($patch), 93 | self::PRE_RELEASE => $pre ?? [], 94 | self::BUILD => $build ?? [], 95 | ]; 96 | } 97 | 98 | /** 99 | * Returns a Version instance for the string representation. 100 | * 101 | * @param string $version The string representation. 102 | * 103 | * @return Version A Version instance. 104 | */ 105 | public static function toVersion(string $version): Version 106 | { 107 | $components = self::toComponents($version); 108 | 109 | return new Version( 110 | $components['major'], 111 | $components['minor'], 112 | $components['patch'], 113 | $components['pre'], 114 | $components['build'], 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /recipe/deploy/shared.php: -------------------------------------------------------------------------------- 1 | getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : ''; 36 | 37 | foreach (get('shared_dirs') as $dir) { 38 | // Make sure all path without tailing slash. 39 | $dir = trim($dir, '/'); 40 | 41 | // Check if shared dir does not exist. 42 | if (!test("[ -d $sharedPath/$dir ]")) { 43 | // Create shared dir if it does not exist. 44 | run("mkdir -p $sharedPath/$dir"); 45 | // If release contains shared dir, copy that dir from release to shared. 46 | if (test("[ -d $(echo {{release_path}}/$dir) ]")) { 47 | run("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir)); 48 | } 49 | } 50 | 51 | // Remove from source. 52 | run("rm -rf {{release_path}}/$dir"); 53 | 54 | // Create path to shared dir in release dir if it does not exist. 55 | // Symlink will not create the path and will fail otherwise. 56 | run("mkdir -p `dirname {{release_path}}/$dir`"); 57 | 58 | // Symlink shared dir to release dir 59 | run("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir"); 60 | } 61 | 62 | foreach (get('shared_files') as $file) { 63 | $dirname = dirname(parse($file)); 64 | 65 | // Create dir of shared file if not existing 66 | if (!test("[ -d $sharedPath/$dirname ]")) { 67 | run("mkdir -p $sharedPath/$dirname"); 68 | } 69 | 70 | // Check if shared file does not exist in shared. 71 | // and file exist in release 72 | if (!test("[ -f $sharedPath/$file ]") && test("[ -f {{release_path}}/$file ]")) { 73 | // Copy file in shared dir if not present 74 | run("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file"); 75 | } 76 | 77 | // Remove from source. 78 | run("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi"); 79 | 80 | // Ensure dir is available in release 81 | run("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi"); 82 | 83 | // Touch shared 84 | run("[ -f $sharedPath/$file ] || touch $sharedPath/$file"); 85 | 86 | // Symlink shared dir to release dir 87 | run("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file"); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | if (ini_get('phar.readonly') === '1') { 9 | throw new \Exception('Writing to phar files is disabled. Change your `php.ini` or append `-d phar.readonly=false` to the shebang, if supported by your `env` executable.'); 10 | } 11 | 12 | define('__ROOT__', realpath(__DIR__ . '/..')); 13 | chdir(__ROOT__); 14 | 15 | $opt = getopt('v:', ['nozip']); 16 | 17 | $version = $opt['v'] ?? null; 18 | if (empty($version)) { 19 | echo "Please, specify version as \"-v8.0.0\".\n"; 20 | exit(1); 21 | } 22 | if (!preg_match('/^\d+\.\d+\.\d+(\-\w+(\.\d+)?)?$/', $version)) { 23 | echo "Version must be \"7.0.0-beta.42\". Got \"$version\".\n"; 24 | exit(1); 25 | } 26 | 27 | echo `set -x; composer install --no-dev --prefer-dist --optimize-autoloader`; 28 | 29 | $pharName = "deployer.phar"; 30 | $pharFile = __ROOT__ . '/' . $pharName; 31 | if (file_exists($pharFile)) { 32 | unlink($pharFile); 33 | } 34 | 35 | $ignore = [ 36 | '.anton', 37 | '.git', 38 | 'Tests', 39 | 'tests', 40 | 'deploy.php', 41 | '.php-cs-fixer.dist.php', 42 | ]; 43 | 44 | $phar = new \Phar($pharFile, 0, $pharName); 45 | $phar->setSignatureAlgorithm(\Phar::SHA1); 46 | $phar->startBuffering(); 47 | $iterator = new RecursiveDirectoryIterator(__ROOT__, FilesystemIterator::SKIP_DOTS); 48 | $iterator = new RecursiveCallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) use ($ignore) { 49 | return !in_array($fileInfo->getBasename(), $ignore, true); 50 | }); 51 | $iterator = new RecursiveIteratorIterator($iterator); 52 | $iterator = new CallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) { 53 | //'bash', 'fish', 'zsh' is a completion templates 54 | return in_array($fileInfo->getExtension(), ['php', 'exe', 'bash', 'fish', 'zsh'], true); 55 | }); 56 | 57 | foreach ($iterator as $fileInfo) { 58 | $file = str_replace(__ROOT__, '', $fileInfo->getRealPath()); 59 | echo "+ " . $file . "\n"; 60 | $phar->addFile($fileInfo->getRealPath(), $file); 61 | 62 | if (!array_key_exists('nozip', $opt)) { 63 | $phar[$file]->compress(Phar::GZ); 64 | 65 | if (!$phar[$file]->isCompressed()) { 66 | echo "Could not compress File: $file\n"; 67 | } 68 | } 69 | } 70 | 71 | // Add Caddyfile 72 | echo "+ /recipe/provision/Caddyfile\n"; 73 | $phar->addFile(realpath(__DIR__ . '/../recipe/provision/Caddyfile'), '/recipe/provision/Caddyfile'); 74 | 75 | // Add 404.html 76 | echo "+ /recipe/provision/404.html\n"; 77 | $phar->addFile(realpath(__DIR__ . '/../recipe/provision/404.html'), '/recipe/provision/404.html'); 78 | 79 | // Add bin/dep file 80 | echo "+ /bin/dep\n"; 81 | $depContent = file_get_contents(__ROOT__ . '/bin/dep'); 82 | $depContent = str_replace("#!/usr/bin/env php\n", '', $depContent); 83 | $depContent = str_replace('__FILE__', 'str_replace("phar://", "", Phar::running())', $depContent); 84 | $depContent = preg_replace("/run\('.+?'/", "run('$version'", $depContent); 85 | $phar->addFromString('bin/dep', $depContent); 86 | 87 | $phar->setStub( 88 | <<stopBuffering(); 97 | unset($phar); 98 | 99 | echo "$pharName was created successfully.\n"; 100 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Version/Version.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Version 13 | { 14 | /** 15 | * The build metadata identifiers. 16 | * 17 | * @var array 18 | */ 19 | protected $build; 20 | 21 | /** 22 | * The major version number. 23 | * 24 | * @var integer 25 | */ 26 | protected $major; 27 | 28 | /** 29 | * The minor version number. 30 | * 31 | * @var integer 32 | */ 33 | protected $minor; 34 | 35 | /** 36 | * The patch version number. 37 | * 38 | * @var integer 39 | */ 40 | protected $patch; 41 | 42 | /** 43 | * The pre-release version identifiers. 44 | * 45 | * @var array 46 | */ 47 | protected $preRelease; 48 | 49 | /** 50 | * Sets the version information. 51 | * 52 | * @param int $major The major version number. 53 | * @param int $minor The minor version number. 54 | * @param int $patch The patch version number. 55 | * @param array $pre The pre-release version identifiers. 56 | * @param array $build The build metadata identifiers. 57 | */ 58 | public function __construct( 59 | int $major = 0, 60 | int $minor = 0, 61 | int $patch = 0, 62 | array $pre = [], 63 | array $build = [], 64 | ) { 65 | $this->build = $build; 66 | $this->major = $major; 67 | $this->minor = $minor; 68 | $this->patch = $patch; 69 | $this->preRelease = $pre; 70 | } 71 | 72 | /** 73 | * Returns the build metadata identifiers. 74 | * 75 | * @return array The build metadata identifiers. 76 | */ 77 | public function getBuild(): array 78 | { 79 | return $this->build; 80 | } 81 | 82 | /** 83 | * Returns the major version number. 84 | * 85 | * @return int The major version number. 86 | */ 87 | public function getMajor(): int 88 | { 89 | return $this->major; 90 | } 91 | 92 | /** 93 | * Returns the minor version number. 94 | * 95 | * @return int The minor version number. 96 | */ 97 | public function getMinor(): int 98 | { 99 | return $this->minor; 100 | } 101 | 102 | /** 103 | * Returns the patch version number. 104 | * 105 | * @return int The patch version number. 106 | */ 107 | public function getPatch(): int 108 | { 109 | return $this->patch; 110 | } 111 | 112 | /** 113 | * Returns the pre-release version identifiers. 114 | * 115 | * @return array The pre-release version identifiers. 116 | */ 117 | public function getPreRelease(): array 118 | { 119 | return $this->preRelease; 120 | } 121 | 122 | /** 123 | * Checks if the version number is stable. 124 | * 125 | * @return boolean TRUE if it is stable, FALSE if not. 126 | */ 127 | public function isStable(): bool 128 | { 129 | return empty($this->preRelease) && $this->major !== 0; 130 | } 131 | 132 | /** 133 | * Returns string representation. 134 | * 135 | * @return string The string representation. 136 | */ 137 | public function __toString(): string 138 | { 139 | return Dumper::toString($this); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /recipe/statamic.php: -------------------------------------------------------------------------------- 1 | $config['service_key'], 32 | ]; 33 | } elseif (!empty($config['email']) && !empty($config['api_key'])) { 34 | $headers = [ 35 | 'X-Auth-Key' => $config['api_key'], 36 | 'X-Auth-Email' => $config['email'], 37 | ]; 38 | } elseif (!empty($config['api_token'])) { 39 | $headers = [ 40 | 'Authorization' => 'Bearer ' . $config['api_token'], 41 | ]; 42 | } else { 43 | throw new \RuntimeException("Set a service key or email / api key"); 44 | } 45 | 46 | $headers['Content-Type'] = 'application/json'; 47 | 48 | $makeRequest = function ($url, $opts = []) use ($headers) { 49 | $ch = curl_init("https://api.cloudflare.com/client/v4/$url"); 50 | 51 | $parsedHeaders = []; 52 | foreach ($headers as $key => $value) { 53 | $parsedHeaders[] = "$key: $value"; 54 | } 55 | 56 | curl_setopt_array($ch, [ 57 | CURLOPT_HTTPHEADER => $parsedHeaders, 58 | CURLOPT_RETURNTRANSFER => true, 59 | ]); 60 | 61 | curl_setopt_array($ch, $opts); 62 | 63 | $res = curl_exec($ch); 64 | 65 | if (curl_errno($ch)) { 66 | throw new \RuntimeException("Error making curl request (result: $res)"); 67 | } 68 | 69 | if (PHP_MAJOR_VERSION < 8) { 70 | curl_close($ch); 71 | } 72 | 73 | return $res; 74 | }; 75 | 76 | $zoneId = $config['zone_id']; 77 | if (empty($zoneId)) { 78 | if (empty($config['domain'])) { 79 | throw new \RuntimeException("Set a domain"); 80 | } 81 | 82 | // get the mysterious zone id from Cloud Flare 83 | $zones = json_decode($makeRequest( 84 | "zones?name={$config['domain']}", 85 | ), true); 86 | 87 | if (!empty($zones['errors'])) { 88 | throw new \RuntimeException('Problem with zone data'); 89 | } else { 90 | $zoneId = current($zones['result'])['id']; 91 | } 92 | } 93 | 94 | // make purge request 95 | $makeRequest( 96 | "zones/$zoneId/purge_cache", 97 | [ 98 | CURLOPT_CUSTOMREQUEST => 'DELETE', 99 | CURLOPT_POSTFIELDS => json_encode( 100 | [ 101 | 'purge_everything' => true, 102 | ], 103 | ), 104 | ], 105 | ); 106 | }); 107 | -------------------------------------------------------------------------------- /contrib/rabbit.php: -------------------------------------------------------------------------------- 1 | 'localhost', 32 | 'port' => '5672', 33 | 'username' => 'guest', 34 | 'password' => 'guest', 35 | 'channel' => 'notify-channel', 36 | 'vhost' => '/my-app' 37 | ]); 38 | ``` 39 | 40 | ### Suggested Usage 41 | 42 | Since you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end. 43 | 44 | ```php 45 | // deploy.php 46 | 47 | before('deploy:end', 'deploy:rabbit'); 48 | ``` 49 | */ 50 | 51 | namespace Deployer; 52 | 53 | use Deployer\Task\Context; 54 | use PhpAmqpLib\Connection\AMQPConnection; 55 | use PhpAmqpLib\Message\AMQPMessage; 56 | 57 | desc('Notifies RabbitMQ channel about deployment'); 58 | task('deploy:rabbit', function () { 59 | 60 | if (!class_exists('PhpAmqpLib\Connection\AMQPConnection')) { 61 | throw new \RuntimeException("Please install php package videlalvaro/php-amqplib to use rabbitmq"); 62 | } 63 | 64 | $config = get('rabbit', []); 65 | 66 | if (!isset($config['message'])) { 67 | $releasePath = get('release_path'); 68 | $host = Context::get()->getHost(); 69 | 70 | $stage = get('stage', false); 71 | $stageInfo = ($stage) ? sprintf(' on *%s*', $stage) : ''; 72 | 73 | $message = "Deployment to '%s'%s was successful\n(%s)"; 74 | $config['message'] = sprintf( 75 | $message, 76 | $host->getHostname(), 77 | $stageInfo, 78 | $releasePath, 79 | ); 80 | } 81 | 82 | $defaultConfig = [ 83 | 'host' => 'localhost', 84 | 'port' => 5672, 85 | 'username' => 'guest', 86 | 'password' => 'guest', 87 | 'vhost' => '/', 88 | ]; 89 | 90 | $config = array_merge($defaultConfig, $config); 91 | 92 | if (!is_array($config) || 93 | !isset($config['channel']) || 94 | !isset($config['host']) || 95 | !isset($config['port']) || 96 | !isset($config['username']) || 97 | !isset($config['password']) || 98 | !isset($config['vhost'])) { 99 | throw new \RuntimeException("Please configure rabbit config: set('rabbit', array('channel' => 'channel', 'host' => 'host', 'port' => 'port', 'username' => 'username', 'password' => 'password'));"); 100 | } 101 | 102 | $connection = new AMQPConnection($config['host'], $config['port'], $config['username'], $config['password'], $config['vhost']); 103 | $channel = $connection->channel(); 104 | 105 | $msg = new AMQPMessage($config['message']); 106 | $channel->basic_publish($msg, $config['channel'], $config['channel']); 107 | 108 | $channel->close(); 109 | $connection->close(); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /contrib/discord.php: -------------------------------------------------------------------------------- 1 | parse(':information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'), 62 | ]; 63 | }); 64 | set('discord_success_text', function () { 65 | return [ 66 | 'text' => parse(':white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'), 67 | ]; 68 | }); 69 | set('discord_failure_text', function () { 70 | return [ 71 | 'text' => parse(':no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'), 72 | ]; 73 | }); 74 | 75 | // The message 76 | set('discord_message', 'discord_notify_text'); 77 | 78 | // Helpers 79 | task('discord_send_message', function () { 80 | $message = get(get('discord_message')); 81 | 82 | Httpie::post(get('discord_webhook'))->jsonBody($message)->send(); 83 | }); 84 | 85 | // Tasks 86 | desc('Tests messages'); 87 | task('discord:test', function () { 88 | set('discord_message', 'discord_notify_text'); 89 | invoke('discord_send_message'); 90 | set('discord_message', 'discord_success_text'); 91 | invoke('discord_send_message'); 92 | set('discord_message', 'discord_failure_text'); 93 | invoke('discord_send_message'); 94 | }) 95 | ->once(); 96 | 97 | desc('Notifies Discord'); 98 | task('discord:notify', function () { 99 | set('discord_message', 'discord_notify_text'); 100 | invoke('discord_send_message'); 101 | }) 102 | ->once() 103 | ->isHidden(); 104 | 105 | desc('Notifies Discord about deploy finish'); 106 | task('discord:notify:success', function () { 107 | set('discord_message', 'discord_success_text'); 108 | invoke('discord_send_message'); 109 | }) 110 | ->once() 111 | ->isHidden(); 112 | 113 | desc('Notifies Discord about deploy failure'); 114 | task('discord:notify:failure', function () { 115 | set('discord_message', 'discord_failure_text'); 116 | invoke('discord_send_message'); 117 | }) 118 | ->once() 119 | ->isHidden(); 120 | -------------------------------------------------------------------------------- /contrib/yammer.php: -------------------------------------------------------------------------------- 1 | {{user}} deploying {{what}} to {{where}} 19 | ``` 20 | - `yammer_success_body` – success template, default: 21 | ``` 22 | Deploy to {{where}} successful 23 | ``` 24 | - `yammer_failure_body` – failure template, default: 25 | ``` 26 | Deploy to {{where}} failed 27 | ``` 28 | 29 | ## Usage 30 | 31 | If you want to notify only about beginning of deployment add this line only: 32 | 33 | ```php 34 | before('deploy', 'yammer:notify'); 35 | ``` 36 | 37 | If you want to notify about successful end of deployment add this too: 38 | 39 | ```php 40 | after('deploy:success', 'yammer:notify:success'); 41 | ``` 42 | 43 | If you want to notify about failed deployment add this too: 44 | 45 | ```php 46 | after('deploy:failed', 'yammer:notify:failure'); 47 | ``` 48 | 49 | */ 50 | 51 | namespace Deployer; 52 | 53 | use Deployer\Utility\Httpie; 54 | 55 | set('yammer_url', 'https://www.yammer.com/api/v1/messages.json'); 56 | 57 | // Title of project 58 | set('yammer_title', function () { 59 | return get('application', 'Project'); 60 | }); 61 | 62 | // Deploy message 63 | set('yammer_body', '{{user}} deploying {{what}} to {{where}}'); 64 | set('yammer_success_body', 'Deploy to {{where}} successful'); 65 | set('yammer_failure_body', 'Deploy to {{where}} failed'); 66 | 67 | desc('Notifies Yammer'); 68 | task('yammer:notify', function () { 69 | $params = [ 70 | 'is_rich_text' => 'true', 71 | 'message_type' => 'announcement', 72 | 'group_id' => get('yammer_group_id'), 73 | 'title' => get('yammer_title'), 74 | 'body' => get('yammer_body'), 75 | ]; 76 | 77 | Httpie::post(get('yammer_url')) 78 | ->header('Authorization', 'Bearer ' . get('yammer_token')) 79 | ->header('Content-type', 'application/json') 80 | ->jsonBody($params) 81 | ->send(); 82 | }) 83 | ->once() 84 | ->hidden(); 85 | 86 | desc('Notifies Yammer about deploy finish'); 87 | task('yammer:notify:success', function () { 88 | $params = [ 89 | 'is_rich_text' => 'true', 90 | 'message_type' => 'announcement', 91 | 'group_id' => get('yammer_group_id'), 92 | 'title' => get('yammer_title'), 93 | 'body' => get('yammer_success_body'), 94 | ]; 95 | 96 | Httpie::post(get('yammer_url')) 97 | ->header('Authorization', 'Bearer ' . get('yammer_token')) 98 | ->header('Content-type', 'application/json') 99 | ->jsonBody($params) 100 | ->send(); 101 | }) 102 | ->once() 103 | ->hidden(); 104 | 105 | desc('Notifies Yammer about deploy failure'); 106 | task('yammer:notify:failure', function () { 107 | $params = [ 108 | 'is_rich_text' => 'true', 109 | 'message_type' => 'announcement', 110 | 'group_id' => get('yammer_group_id'), 111 | 'title' => get('yammer_title'), 112 | 'body' => get('yammer_failure_body'), 113 | ]; 114 | 115 | Httpie::post(get('yammer_url')) 116 | ->header('Authorization', 'Bearer ' . get('yammer_token')) 117 | ->header('Content-type', 'application/json') 118 | ->jsonBody($params) 119 | ->send(); 120 | }) 121 | ->once() 122 | ->hidden(); 123 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Console/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Command extends Base 20 | { 21 | /** 22 | * Disable the ability to upgrade? 23 | * 24 | * @var boolean 25 | */ 26 | private $disableUpgrade = false; 27 | 28 | /** 29 | * The manifest file URI. 30 | * 31 | * @var string 32 | */ 33 | private $manifestUri; 34 | 35 | /** 36 | * The running file (the Phar that will be updated). 37 | * 38 | * @var string 39 | */ 40 | private $runningFile; 41 | 42 | /** 43 | * @param string $name The command name. 44 | * @param boolean $disable Disable upgrading? 45 | */ 46 | public function __construct(string $name, bool $disable = false) 47 | { 48 | $this->disableUpgrade = $disable; 49 | 50 | parent::__construct($name); 51 | } 52 | 53 | /** 54 | * Sets the manifest URI. 55 | * 56 | * @param string $uri The URI. 57 | */ 58 | public function setManifestUri(string $uri) 59 | { 60 | $this->manifestUri = $uri; 61 | } 62 | 63 | /** 64 | * Sets the running file (the Phar that will be updated). 65 | * 66 | * @param string $file The file name or path. 67 | */ 68 | public function setRunningFile(string $file): void 69 | { 70 | $this->runningFile = $file; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | protected function configure() 77 | { 78 | $this->setDescription('Updates the application.'); 79 | $this->addOption( 80 | 'pre', 81 | 'p', 82 | InputOption::VALUE_NONE, 83 | 'Allow pre-release updates.', 84 | ); 85 | $this->addOption( 86 | 'redo', 87 | 'r', 88 | InputOption::VALUE_NONE, 89 | 'Redownload update if already using current version.', 90 | ); 91 | 92 | if (false === $this->disableUpgrade) { 93 | $this->addOption( 94 | 'upgrade', 95 | 'u', 96 | InputOption::VALUE_NONE, 97 | 'Upgrade to next major release, if available.', 98 | ); 99 | } 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | protected function execute(InputInterface $input, OutputInterface $output): int 106 | { 107 | if (null === $this->manifestUri) { 108 | throw new LogicException( 109 | 'No manifest URI has been configured.', 110 | ); 111 | } 112 | 113 | $output->writeln('Looking for updates...'); 114 | 115 | /** @var Helper */ 116 | $pharUpdate = $this->getHelper('phar-update'); 117 | /** @var Manager $manager */ 118 | $manager = $pharUpdate->getManager($this->manifestUri); 119 | $manager->setRunningFile($this->runningFile); 120 | 121 | if ($manager->update( 122 | $this->getApplication()->getVersion(), 123 | $this->disableUpgrade ?: (false === $input->getOption('upgrade')), 124 | $input->getOption('pre'), 125 | )) { 126 | $output->writeln('Update successful!'); 127 | } else { 128 | $output->writeln('Already up-to-date.'); 129 | } 130 | 131 | return self::SUCCESS; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Task/ScriptManager.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Task; 12 | 13 | use Deployer\Exception\Exception; 14 | 15 | use function Deployer\Support\array_flatten; 16 | 17 | class ScriptManager 18 | { 19 | /** 20 | * @var TaskCollection 21 | */ 22 | private $tasks; 23 | /** 24 | * @var bool 25 | */ 26 | private $hooksEnabled = true; 27 | /** 28 | * @var array 29 | */ 30 | private $visitedTasks = []; 31 | 32 | public function __construct(TaskCollection $tasks) 33 | { 34 | $this->tasks = $tasks; 35 | } 36 | 37 | /** 38 | * Return tasks to run. 39 | * 40 | * @return Task[] 41 | */ 42 | public function getTasks(string $name, ?string $startFrom = null, array &$skipped = []): array 43 | { 44 | $tasks = []; 45 | $this->visitedTasks = []; 46 | $allTasks = $this->doGetTasks($name); 47 | 48 | if ($startFrom === null) { 49 | $tasks = $allTasks; 50 | } else { 51 | $skip = true; 52 | foreach ($allTasks as $task) { 53 | if ($skip) { 54 | if ($task->getName() === $startFrom) { 55 | $skip = false; 56 | } else { 57 | $skipped[] = $task->getName(); 58 | continue; 59 | } 60 | } 61 | $tasks[] = $task; 62 | } 63 | if (count($tasks) === 0) { 64 | throw new Exception('All tasks skipped via --start-from option. Nothing to run.'); 65 | } 66 | } 67 | 68 | $enabledTasks = []; 69 | foreach ($tasks as $task) { 70 | if ($task->isEnabled()) { 71 | $enabledTasks[] = $task; 72 | } 73 | } 74 | 75 | return $enabledTasks; 76 | } 77 | 78 | /** 79 | * @return Task[] 80 | */ 81 | public function doGetTasks(string $name): array 82 | { 83 | if (array_key_exists($name, $this->visitedTasks)) { 84 | if ($this->visitedTasks[$name] >= 100) { 85 | throw new Exception("Looks like a circular dependency with \"$name\" task."); 86 | } 87 | $this->visitedTasks[$name]++; 88 | } else { 89 | $this->visitedTasks[$name] = 1; 90 | } 91 | 92 | $tasks = []; 93 | $task = $this->tasks->get($name); 94 | if ($this->hooksEnabled) { 95 | $tasks = array_merge(array_map([$this, 'doGetTasks'], $task->getBefore()), $tasks); 96 | } 97 | if ($task instanceof GroupTask) { 98 | foreach ($task->getGroup() as $taskName) { 99 | $subTasks = $this->doGetTasks($taskName); 100 | foreach ($subTasks as $subTask) { 101 | $subTask->addSelector($task->getSelector()); 102 | if ($task->isOnce()) { 103 | $subTask->once(); 104 | } 105 | $tasks[] = $subTask; 106 | } 107 | } 108 | } else { 109 | $tasks[] = $task; 110 | } 111 | if ($this->hooksEnabled) { 112 | $tasks = array_merge($tasks, array_map([$this, 'doGetTasks'], $task->getAfter())); 113 | } 114 | return array_flatten($tasks); 115 | } 116 | 117 | public function getHooksEnabled(): bool 118 | { 119 | return $this->hooksEnabled; 120 | } 121 | 122 | public function setHooksEnabled(bool $hooksEnabled): void 123 | { 124 | $this->hooksEnabled = $hooksEnabled; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Component/PharUpdate/Manifest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Manifest 17 | { 18 | /** 19 | * The list of updates in the manifest. 20 | * 21 | * @var Update[] 22 | */ 23 | private $updates; 24 | 25 | /** 26 | * Sets the list of updates from the manifest. 27 | * 28 | * @param Update[] $updates The updates. 29 | */ 30 | public function __construct(array $updates = []) 31 | { 32 | $this->updates = $updates; 33 | } 34 | 35 | /** 36 | * Finds the most recent update and returns it. 37 | * 38 | * @param Version $version The current version. 39 | * @param boolean $major Lock to major version? 40 | * @param boolean $pre Allow pre-releases? 41 | */ 42 | public function findRecent(Version $version, bool $major = false, bool $pre = false): ?Update 43 | { 44 | /** @var Update|null */ 45 | $current = null; 46 | $major = $major ? $version->getMajor() : null; 47 | 48 | foreach ($this->updates as $update) { 49 | if ($major && ($major !== $update->getVersion()->getMajor())) { 50 | continue; 51 | } 52 | 53 | if ((false === $pre) 54 | && !$update->getVersion()->isStable()) { 55 | continue; 56 | } 57 | 58 | $test = $current ? $current->getVersion() : $version; 59 | 60 | if (false === $update->isNewer($test)) { 61 | continue; 62 | } 63 | 64 | $current = $update; 65 | } 66 | 67 | return $current; 68 | } 69 | 70 | /** 71 | * Returns the list of updates in the manifest. 72 | * 73 | * @return Update[] The updates. 74 | */ 75 | public function getUpdates(): array 76 | { 77 | return $this->updates; 78 | } 79 | 80 | /** 81 | * Loads the manifest from a JSON encoded string. 82 | * 83 | * @param string $json The JSON encoded string. 84 | */ 85 | public static function load(string $json): self 86 | { 87 | return self::create(json_decode($json)); 88 | } 89 | 90 | /** 91 | * Loads the manifest from a JSON encoded file. 92 | * 93 | * @param string $file The JSON encoded file. 94 | */ 95 | public static function loadFile(string $file): self 96 | { 97 | return self::create(json_decode(file_get_contents($file))); 98 | } 99 | 100 | /** 101 | * Validates the data, processes it, and returns a new instance of Manifest. 102 | * 103 | * @param array $decoded The decoded JSON data. 104 | * 105 | * @return static The new instance. 106 | */ 107 | private static function create(array $decoded): self 108 | { 109 | $updates = []; 110 | 111 | foreach ($decoded as $update) { 112 | $updates[] = new Update( 113 | $update->name, 114 | $update->sha1, 115 | $update->url, 116 | Parser::toVersion($update->version), 117 | $update->publicKey ?? null, 118 | ); 119 | } 120 | 121 | usort( 122 | $updates, 123 | function (Update $a, Update $b) { 124 | return Comparator::isGreaterThan( 125 | $a->getVersion(), 126 | $b->getVersion(), 127 | ) ? 1 : 0; 128 | }, 129 | ); 130 | 131 | return new static($updates); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Selector/Selector.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Selector; 12 | 13 | use Deployer\Host\Host; 14 | use Deployer\Host\HostCollection; 15 | 16 | use function Deployer\Support\array_all; 17 | 18 | class Selector 19 | { 20 | /** 21 | * @var HostCollection 22 | */ 23 | private $hosts; 24 | 25 | public function __construct(HostCollection $hosts) 26 | { 27 | $this->hosts = $hosts; 28 | } 29 | 30 | /** 31 | * @return Host[] 32 | */ 33 | public function select(string $selectExpression) 34 | { 35 | $conditions = self::parse($selectExpression); 36 | 37 | $hosts = []; 38 | foreach ($this->hosts as $host) { 39 | if (self::apply($conditions, $host)) { 40 | $hosts[] = $host; 41 | } 42 | } 43 | 44 | return $hosts; 45 | } 46 | 47 | public static function apply(?array $conditions, Host $host): bool 48 | { 49 | if (empty($conditions)) { 50 | return true; 51 | } 52 | 53 | $labels = $host->get('labels', []); 54 | $labels['alias'] = $host->getAlias(); 55 | $labels['true'] = 'true'; 56 | $isTrue = function ($value) { 57 | return $value; 58 | }; 59 | 60 | foreach ($conditions as $hmm) { 61 | $ok = []; 62 | foreach ($hmm as [$op, $var, $value]) { 63 | if (is_array($value)) { 64 | $orOk = []; 65 | foreach ($value as $val) { 66 | $orOk[] = self::compare($op, $labels[$var] ?? null, $val); 67 | } 68 | $ok[] = count(array_filter($orOk, $isTrue)) > 0; 69 | } else { 70 | $ok[] = self::compare($op, $labels[$var] ?? null, $value); 71 | } 72 | } 73 | if (count($ok) > 0 && array_all($ok, $isTrue)) { 74 | return true; 75 | } 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * @param string|string[] $a 82 | */ 83 | private static function compare(string $op, $a, ?string $b): bool 84 | { 85 | $matchFunction = function ($a, ?string $b) { 86 | foreach ((array) $a as $item) { 87 | if ($item === $b) { 88 | return true; 89 | } 90 | } 91 | 92 | return false; 93 | }; 94 | 95 | if ($op === '=') { 96 | return $matchFunction($a, $b); 97 | } 98 | if ($op === '!=') { 99 | return !$matchFunction($a, $b); 100 | } 101 | return false; 102 | } 103 | 104 | public static function parse(string $expression): array 105 | { 106 | $all = []; 107 | foreach (explode(',', $expression) as $sub) { 108 | $conditions = []; 109 | foreach (explode('&', $sub) as $part) { 110 | $part = trim($part); 111 | if ($part === 'all') { 112 | $conditions[] = ['=', 'true', 'true']; 113 | continue; 114 | } 115 | if (preg_match('/(?.+?)(?!?=)(?.+)/', $part, $match)) { 116 | $values = array_map('trim', explode('|', trim($match['value']))); 117 | $conditions[] = [$match['op'], trim($match['var']), $values]; 118 | } else { 119 | $conditions[] = ['=', 'alias', trim($part)]; 120 | } 121 | } 122 | $all[] = $conditions; 123 | } 124 | return $all; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /recipe/provision/php.php: -------------------------------------------------------------------------------- 1 | 2) { 11 | $defaultPhpVersion = "$parts[0].$parts[1]"; 12 | } 13 | 14 | return ask(' What PHP version to install? ', $defaultPhpVersion, ['5.6', '7.4', '8.0', '8.1', '8.2', '8.3']); 15 | }); 16 | 17 | desc('Installs PHP packages'); 18 | task('provision:php', function () { 19 | set('remote_user', get('provision_user')); 20 | 21 | $version = get('php_version'); 22 | info("Installing PHP $version"); 23 | $packages = [ 24 | "php$version-bcmath", 25 | "php$version-cli", 26 | "php$version-curl", 27 | "php$version-dev", 28 | "php$version-fpm", 29 | "php$version-gd", 30 | "php$version-imap", 31 | "php$version-intl", 32 | "php$version-mbstring", 33 | "php$version-mysql", 34 | "php$version-pgsql", 35 | "php$version-readline", 36 | "php$version-soap", 37 | "php$version-sqlite3", 38 | "php$version-xml", 39 | "php$version-zip", 40 | ]; 41 | run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive']); 42 | 43 | // Configure PHP-CLI 44 | run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini"); 45 | run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini"); 46 | run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini"); 47 | run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini"); 48 | run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini"); 49 | 50 | // Configure PHP-FPM 51 | run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini"); 52 | run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/fpm/php.ini"); 53 | run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/fpm/php.ini"); 54 | run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/fpm/php.ini"); 55 | run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/fpm/php.ini"); 56 | run("sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/$version/fpm/php.ini"); 57 | 58 | // Configure FPM Pool 59 | run("sed -i 's/;request_terminate_timeout = .*/request_terminate_timeout = 60/' /etc/php/$version/fpm/pool.d/www.conf"); 60 | run("sed -i 's/;catch_workers_output = .*/catch_workers_output = yes/' /etc/php/$version/fpm/pool.d/www.conf"); 61 | run("sed -i 's/;php_flag\[display_errors\] = .*/php_flag[display_errors] = yes/' /etc/php/$version/fpm/pool.d/www.conf"); 62 | 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"); 63 | run("sed -i 's/;php_admin_flag\[log_errors\] = .*/php_admin_flag[log_errors] = on/' /etc/php/$version/fpm/pool.d/www.conf"); 64 | 65 | // Configure PHP sessions directory 66 | run('chmod 733 /var/lib/php/sessions'); 67 | run('chmod +t /var/lib/php/sessions'); 68 | }) 69 | ->verbose() 70 | ->limit(1); 71 | 72 | desc('Shows php-fpm logs'); 73 | task('logs:php-fpm', function () { 74 | $fpmLogs = run("ls -1 /var/log | grep fpm"); 75 | if (empty($fpmLogs)) { 76 | throw new \RuntimeException('No PHP-FPM logs found.'); 77 | } 78 | run("sudo tail -f /var/log/$fpmLogs"); 79 | })->verbose(); 80 | 81 | desc('Installs Composer'); 82 | task('provision:composer', function () { 83 | run('curl -sS https://getcomposer.org/installer | php'); 84 | run('mv composer.phar /usr/local/bin/composer'); 85 | })->oncePerNode(); 86 | -------------------------------------------------------------------------------- /src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://deployer.org/schema.json#", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "version": { 8 | "type": "string" 9 | }, 10 | "import": { 11 | "oneOf": [ 12 | { 13 | "type": "string" 14 | }, 15 | { 16 | "type": "array", 17 | "items": { 18 | "type": "string" 19 | } 20 | } 21 | ] 22 | }, 23 | "config": { 24 | "type": "object" 25 | }, 26 | "hosts": { 27 | "type": "object", 28 | "patternProperties": { 29 | "^": { 30 | "oneOf": [ 31 | { 32 | "type": "object", 33 | "properties": { 34 | "local": { 35 | "type": "boolean" 36 | } 37 | } 38 | }, 39 | { 40 | "type": "null" 41 | } 42 | ] 43 | } 44 | } 45 | }, 46 | "tasks": { 47 | "type": "object", 48 | "patternProperties": { 49 | "^": { 50 | "oneOf": [ 51 | { 52 | "type": "array", 53 | "items": { 54 | "type": "object", 55 | "properties": { 56 | "cd": { 57 | "type": "string" 58 | }, 59 | "run": { 60 | "type": "string" 61 | }, 62 | "run_locally": { 63 | "type": "string" 64 | }, 65 | "upload": { 66 | "type": "object", 67 | "required": [ 68 | "src", 69 | "dest" 70 | ], 71 | "properties": { 72 | "src": { 73 | "oneOf": [ 74 | { 75 | "type": "string" 76 | }, 77 | { 78 | "type": "array", 79 | "items": { 80 | "type": "string" 81 | } 82 | } 83 | ] 84 | }, 85 | "dest": { 86 | "type": "string" 87 | } 88 | } 89 | }, 90 | "download": { 91 | "type": "object", 92 | "required": [ 93 | "src", 94 | "dest" 95 | ], 96 | "properties": { 97 | "src": { 98 | "type": "string" 99 | }, 100 | "dest": { 101 | "type": "string" 102 | } 103 | } 104 | }, 105 | "desc": { 106 | "type": "string" 107 | }, 108 | "once": { 109 | "type": "boolean" 110 | }, 111 | "hidden": { 112 | "type": "boolean" 113 | }, 114 | "limit": { 115 | "type": "number" 116 | }, 117 | "select": { 118 | "type": "string" 119 | } 120 | } 121 | } 122 | }, 123 | { 124 | "type": "array", 125 | "items": { 126 | "type": "string" 127 | } 128 | } 129 | ] 130 | } 131 | } 132 | }, 133 | "before": { 134 | "type": "object" 135 | }, 136 | "after": { 137 | "type": "object" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /contrib/workplace.php: -------------------------------------------------------------------------------- 1 | /feed?access_token='); 18 | 19 | // With publishing bot 20 | set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token='); 21 | 22 | // Use markdown on message 23 | set('workplace_webhook', 'https://graph.facebook.com//feed?access_token=&formatting=MARKDOWN'); 24 | ``` 25 | 26 | - `workplace_text` - notification message 27 | ``` 28 | set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); 29 | ``` 30 | 31 | - `workplace_success_text` – success template, default: 32 | ``` 33 | set('workplace_success_text', 'Deploy to *{{where}}* successful'); 34 | ``` 35 | - `workplace_failure_text` – failure template, default: 36 | ``` 37 | set('workplace_failure_text', 'Deploy to *{{where}}* failed'); 38 | ``` 39 | - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post: 40 | ``` 41 | set('workplace_edit_post', false); 42 | ``` 43 | 44 | ## Usage 45 | 46 | If you want to notify only about beginning of deployment add this line only: 47 | 48 | ```php 49 | before('deploy', 'workplace:notify'); 50 | ``` 51 | 52 | If you want to notify about successful end of deployment add this too: 53 | 54 | ```php 55 | after('deploy:success', 'workplace:notify:success'); 56 | ``` 57 | 58 | If you want to notify about failed deployment add this too: 59 | 60 | ```php 61 | after('deploy:failed', 'workplace:notify:failure'); 62 | ``` 63 | 64 | */ 65 | 66 | namespace Deployer; 67 | 68 | use Deployer\Utility\Httpie; 69 | 70 | // Deploy message 71 | set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); 72 | set('workplace_success_text', 'Deploy to *{{where}}* successful'); 73 | set('workplace_failure_text', 'Deploy to *{{where}}* failed'); 74 | 75 | // By default, create a new post for every message 76 | set('workplace_edit_post', false); 77 | 78 | desc('Notifies Workplace'); 79 | task('workplace:notify', function () { 80 | if (!get('workplace_webhook', false)) { 81 | return; 82 | } 83 | $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_text')); 84 | $response = Httpie::post($url)->getJson(); 85 | 86 | if (get('workplace_edit_post', false)) { 87 | // Endpoint will be something like: https//graph.facebook.com/? 88 | $url = sprintf( 89 | '%s://%s/%s?%s', 90 | parse_url(get('workplace_webhook'), PHP_URL_SCHEME), 91 | parse_url(get('workplace_webhook'), PHP_URL_HOST), 92 | $response['id'], 93 | parse_url(get('workplace_webhook'), PHP_URL_QUERY), 94 | ); 95 | // Replace the webhook with a url that points to the created post 96 | set('workplace_webhook', $url); 97 | } 98 | }) 99 | ->once() 100 | ->hidden(); 101 | 102 | desc('Notifies Workplace about deploy finish'); 103 | task('workplace:notify:success', function () { 104 | if (!get('workplace_webhook', false)) { 105 | return; 106 | } 107 | $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_success_text')); 108 | Httpie::post($url)->send(); 109 | }) 110 | ->once() 111 | ->hidden(); 112 | 113 | desc('Notifies Workplace about deploy failure'); 114 | task('workplace:notify:failure', function () { 115 | if (!get('workplace_webhook', false)) { 116 | return; 117 | } 118 | $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_failure_text')); 119 | Httpie::post($url)->send(); 120 | }) 121 | ->once() 122 | ->hidden(); 123 | -------------------------------------------------------------------------------- /recipe/craftcms.php: -------------------------------------------------------------------------------- 1 | $output"); 53 | } 54 | }; 55 | } 56 | 57 | /* 58 | * Migrations 59 | */ 60 | 61 | desc('Runs all pending Craft, plugin, and content migrations'); 62 | task('craft:migrate/all', craft('migrate/all')); 63 | 64 | desc('Upgrades Craft by applying new migrations'); 65 | task('craft:migrate/up', craft('migrate/up')); 66 | 67 | /* 68 | * Generate keys 69 | */ 70 | 71 | desc('Generates a new application ID and saves it in the `.env` file'); 72 | task('craft:setup/app-id', craft('setup/app-id')); 73 | 74 | desc('Generates a new security key and saves it in the `.env` file'); 75 | task('craft:setup/security-key', craft('setup/security-key')); 76 | 77 | /* 78 | * Project config 79 | */ 80 | 81 | desc('Applies project config file changes.'); 82 | task('craft:project-config/apply', craft('project-config/apply')); 83 | 84 | /* 85 | * Caches 86 | */ 87 | 88 | desc('Flushes all caches registered in the system'); 89 | task('craft:cache/flush-all', craft('cache/flush-all')); 90 | 91 | desc('Clear all caches'); 92 | task('craft:clear-caches/all', craft('clear-caches/all')); 93 | 94 | desc('Clear all Asset caches'); 95 | task('craft:clear-caches/asset', craft('clear-caches/asset')); 96 | 97 | desc('Clear all Asset indexing data'); 98 | task('craft:clear-caches/asset-indexing-data', craft('clear-caches/asset-indexing-data')); 99 | 100 | desc('Clear all compiled classes'); 101 | task('craft:clear-caches/compiled-classes', craft('clear-caches/compiled-classes')); 102 | 103 | desc('Clear all compiled templates'); 104 | task('craft:clear-caches/compiled-templates', craft('clear-caches/compiled-templates')); 105 | 106 | desc('Clear all control panel resources'); 107 | task('craft:clear-caches/cp-resources', craft('clear-caches/cp-resources')); 108 | 109 | desc('Clear all data caches'); 110 | task('craft:clear-caches/data', craft('clear-caches/data')); 111 | 112 | desc('Clear all temp files'); 113 | task('craft:clear-caches/temp-files', craft('clear-caches/temp-files')); 114 | 115 | /* 116 | * Garbage collection 117 | */ 118 | 119 | desc('Runs garbage collection'); 120 | task('craft:gc', craft('gc --delete-all-trashed=1 --silent-exit-on-exception=1', ['showOutput'])); 121 | 122 | /* 123 | * Main deploy 124 | */ 125 | 126 | desc('Deploys Craft CMS'); 127 | task('deploy', [ 128 | 'deploy:prepare', 129 | 'deploy:vendors', 130 | 'craft:clear-caches/compiled-classes', 131 | 'craft:migrate/all', 132 | 'craft:project-config/apply', 133 | 'craft:gc', 134 | 'craft:clear-caches/all', 135 | 'deploy:publish', 136 | ]); 137 | -------------------------------------------------------------------------------- /src/Ssh/SshClient.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Ssh; 12 | 13 | use Deployer\ProcessRunner\Printer; 14 | use Deployer\Exception\RunException; 15 | use Deployer\Exception\TimeoutException; 16 | use Deployer\Host\Host; 17 | use Deployer\Logger\Logger; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Process\Exception\ProcessTimedOutException; 20 | use Symfony\Component\Process\Process; 21 | 22 | use function Deployer\Support\env_stringify; 23 | 24 | class SshClient 25 | { 26 | private OutputInterface $output; 27 | private Printer $pop; 28 | private Logger $logger; 29 | 30 | public function __construct(OutputInterface $output, Printer $pop, Logger $logger) 31 | { 32 | $this->output = $output; 33 | $this->pop = $pop; 34 | $this->logger = $logger; 35 | } 36 | 37 | public function run(Host $host, string $command, RunParams $params): string 38 | { 39 | $shellId = 'id$' . bin2hex(random_bytes(10)); 40 | $shellCommand = $host->getShell(); 41 | if ($host->has('become') && !empty($host->get('become'))) { 42 | $shellCommand = "sudo -H -u {$host->get('become')} " . $shellCommand; 43 | } 44 | 45 | $ssh = array_merge(['ssh'], $host->connectionOptionsArray(), [$host->connectionString(), ": $shellId; $shellCommand"]); 46 | 47 | // -vvv for ssh command 48 | if ($this->output->isDebug()) { 49 | $sshString = $ssh[0]; 50 | for ($i = 1; $i < count($ssh); $i++) { 51 | $sshString .= ' ' . escapeshellarg((string) $ssh[$i]); 52 | } 53 | $this->output->writeln("[$host] $sshString"); 54 | } 55 | 56 | if (!empty($params->cwd)) { 57 | $command = "cd $params->cwd && ($command)"; 58 | } 59 | 60 | if (!empty($params->env)) { 61 | $env = env_stringify($params->env); 62 | $command = "export $env; $command"; 63 | } 64 | 65 | if (!empty($params->secrets)) { 66 | foreach ($params->secrets as $key => $value) { 67 | $command = str_replace('%' . $key . '%', strval($value), $command); 68 | } 69 | } 70 | 71 | $this->pop->command($host, 'run', $command); 72 | $this->logger->log("[{$host->getAlias()}] run $command"); 73 | 74 | 75 | $process = new Process($ssh); 76 | $process 77 | ->setInput($command) 78 | ->setTimeout($params->timeout) 79 | ->setIdleTimeout($params->idleTimeout); 80 | 81 | $callback = function ($type, $buffer) use ($params, $host) { 82 | $this->logger->printBuffer($host, $type, $buffer); 83 | $this->pop->callback($host, $params->forceOutput)($type, $buffer); 84 | }; 85 | 86 | try { 87 | $process->run($callback); 88 | } catch (ProcessTimedOutException $exception) { 89 | // Let's try to kill all processes started by this command. 90 | $pid = $this->run($host, "ps x | grep $shellId | grep -v grep | awk '{print \$1}'", $params->with(timeout: 10)); 91 | // Minus before pid means all processes in this group. 92 | $this->run($host, "kill -9 -$pid", $params->with(timeout: 20)); 93 | throw new TimeoutException( 94 | $command, 95 | $exception->getExceededTimeout(), 96 | ); 97 | } 98 | 99 | $output = $process->getOutput(); 100 | $exitCode = $process->getExitCode(); 101 | 102 | if ($exitCode !== 0 && !$params->nothrow) { 103 | throw new RunException( 104 | $host, 105 | $command, 106 | $exitCode, 107 | $output, 108 | $process->getErrorOutput(), 109 | ); 110 | } 111 | 112 | return $output; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Command/SshCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Deployer\Command; 12 | 13 | use Deployer\Deployer; 14 | use Deployer\Host\Localhost; 15 | use Deployer\Task\Context; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Completion\CompletionInput; 18 | use Symfony\Component\Console\Completion\CompletionSuggestions; 19 | use Symfony\Component\Console\Helper\QuestionHelper; 20 | use Symfony\Component\Console\Input\InputArgument; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | use Symfony\Component\Console\Question\ChoiceQuestion; 24 | 25 | /** 26 | * @codeCoverageIgnore 27 | */ 28 | class SshCommand extends Command 29 | { 30 | use CommandCommon; 31 | 32 | /** 33 | * @var Deployer 34 | */ 35 | private $deployer; 36 | 37 | public function __construct(Deployer $deployer) 38 | { 39 | parent::__construct('ssh'); 40 | $this->setDescription('Connect to host through ssh'); 41 | $this->deployer = $deployer; 42 | } 43 | 44 | protected function configure() 45 | { 46 | $this->addArgument( 47 | 'hostname', 48 | InputArgument::OPTIONAL, 49 | 'Hostname', 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $this->telemetry(); 56 | $hostname = $input->getArgument('hostname'); 57 | if (!empty($hostname)) { 58 | $host = $this->deployer->hosts->get($hostname); 59 | } else { 60 | $hostsAliases = []; 61 | foreach ($this->deployer->hosts as $host) { 62 | if ($host instanceof Localhost) { 63 | continue; 64 | } 65 | $hostsAliases[] = $host->getAlias(); 66 | } 67 | 68 | if (count($hostsAliases) === 0) { 69 | $output->writeln('No remote hosts.'); 70 | return 2; // Because there are no hosts. 71 | } 72 | 73 | if (count($hostsAliases) === 1) { 74 | $host = $this->deployer->hosts->get($hostsAliases[0]); 75 | } else { 76 | /** @var QuestionHelper $helper */ 77 | $helper = $this->getHelper('question'); 78 | $question = new ChoiceQuestion( 79 | 'Select host:', 80 | $hostsAliases, 81 | ); 82 | $question->setErrorMessage('There is no "%s" host.'); 83 | 84 | $hostname = $helper->ask($input, $output, $question); 85 | $host = $this->deployer->hosts->get($hostname); 86 | } 87 | } 88 | 89 | $shell_path = 'exec $SHELL -l'; 90 | if ($host->has('shell_path')) { 91 | $shell_path = 'exec ' . $host->get('shell_path') . ' -l'; 92 | } 93 | 94 | Context::push(new Context($host)); 95 | $host->setSshMultiplexing(false); 96 | $options = $host->connectionOptionsString(); 97 | $deployPath = $host->get('deploy_path', '~'); 98 | 99 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 100 | passthru("ssh -t $options {$host->connectionString()} \"cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path\""); 101 | } else { 102 | passthru("ssh -t $options {$host->connectionString()} 'cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path'"); 103 | } 104 | return 0; 105 | } 106 | 107 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 108 | { 109 | parent::complete($input, $suggestions); 110 | if ($input->mustSuggestArgumentValuesFor('hostname')) { 111 | $suggestions->suggestValues(array_keys($this->deployer->hosts->all())); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contrib/cachetool.php: -------------------------------------------------------------------------------- 1 | set('cachetool', '127.0.0.1:9000'); 20 | 21 | host('production') 22 | ->set('cachetool', '/var/run/php-fpm.sock'); 23 | ``` 24 | 25 | By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting. 26 | 27 | If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use 28 | the web adapter that creates a temporary php file and makes a web request to it with a configuration like 29 | ```php 30 | set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}'); 31 | ``` 32 | 33 | ## Usage 34 | 35 | Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release: 36 | 37 | ```php 38 | after('deploy:symlink', 'cachetool:clear:opcache'); 39 | // or 40 | after('deploy:symlink', 'cachetool:clear:apcu'); 41 | ``` 42 | 43 | ## Read more 44 | 45 | Read more information about cachetool on the website: 46 | http://gordalina.github.io/cachetool/ 47 | */ 48 | 49 | namespace Deployer; 50 | 51 | set('cachetool', ''); 52 | /** 53 | * URL to download cachetool from if it is not available 54 | * 55 | * CacheTool 9.x works with PHP >=8.1 56 | * CacheTool 8.x works with PHP >=8.0 57 | * CacheTool 7.x works with PHP >=7.3 58 | */ 59 | set('cachetool_url', 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar'); 60 | set('cachetool_args', ''); 61 | set('bin/cachetool', function () { 62 | if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) { 63 | run("cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}"); 64 | } 65 | return '{{release_or_current_path}}/cachetool.phar'; 66 | }); 67 | set('cachetool_options', function () { 68 | $options = (array) get('cachetool'); 69 | $fullOptions = (string) get('cachetool_args'); 70 | $return = []; 71 | 72 | if ($fullOptions !== '') { 73 | $return = [$fullOptions]; 74 | } elseif (count($options) > 0) { 75 | foreach ($options as $option) { 76 | if (is_string($option) && $option !== '') { 77 | $return[] = "--fcgi={$option}"; 78 | } 79 | } 80 | } 81 | 82 | return $return ?: ['']; 83 | }); 84 | 85 | /** 86 | * Clear opcache cache 87 | */ 88 | desc('Clears OPcode cache'); 89 | task('cachetool:clear:opcache', function () { 90 | $options = get('cachetool_options'); 91 | foreach ($options as $option) { 92 | run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} opcache:reset $option"); 93 | } 94 | }); 95 | 96 | /** 97 | * Clear APCu cache 98 | */ 99 | desc('Clears APCu system cache'); 100 | task('cachetool:clear:apcu', function () { 101 | $options = get('cachetool_options'); 102 | foreach ($options as $option) { 103 | run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} apcu:cache:clear $option"); 104 | } 105 | }); 106 | 107 | /** 108 | * Clear file status cache, including the realpath cache 109 | */ 110 | desc('Clears file status and realpath caches'); 111 | task('cachetool:clear:stat', function () { 112 | $options = get('cachetool_options'); 113 | foreach ($options as $option) { 114 | run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} stat:clear $option"); 115 | } 116 | }); 117 | -------------------------------------------------------------------------------- /recipe/contao.php: -------------------------------------------------------------------------------- 1 | contao-manager/login.lock'); 77 | }); 78 | 79 | desc('Enable maintenance mode'); 80 | task('contao:maintenance:enable', function () { 81 | // Enable maintenance mode in both the current and release build, so that the maintenance mode will be enabled 82 | // for the current installation before the symlink changes and the new installation after the symlink changed. 83 | foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { 84 | // The current path might not be present during first deploy. 85 | if (!test("[ -d $path ]")) { 86 | continue; 87 | } 88 | 89 | cd($path); 90 | run('{{bin/console}} contao:maintenance-mode enable {{console_options}}'); 91 | } 92 | }); 93 | 94 | desc('Disable maintenance mode'); 95 | task('contao:maintenance:disable', function () { 96 | foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { 97 | if (!test("[ -d $path ]")) { 98 | continue; 99 | } 100 | 101 | cd($path); 102 | run('{{bin/console}} contao:maintenance-mode disable {{console_options}}'); 103 | } 104 | }); 105 | 106 | desc('Deploy the project'); 107 | task('deploy', [ 108 | 'deploy:prepare', 109 | 'deploy:vendors', 110 | 'contao:maintenance:enable', 111 | 'contao:migrate', 112 | 'contao:maintenance:disable', 113 | 'deploy:publish', 114 | ]); 115 | --------------------------------------------------------------------------------