aes3xs / yodler
Requires
- php: >=5.6.0
- doctrine/inflector: ^1.1
- herzult/php-ssh: ~1.0
- monolog/monolog: ~1.22
- phpseclib/phpseclib: ^2.0
- symfony/config: ~2.7|~3.0
- symfony/console: ~2.7|~3.0
- symfony/dependency-injection: ~2.7|~3.0
- symfony/expression-language: ~2.7|~3.0
- symfony/filesystem: ~2.7|~3.0
- symfony/process: ~2.7|~3.0
- symfony/yaml: ~2.7|~3.0
- twig/twig: ~1.32
Requires (Dev)
- phpunit/phpunit: ~5.2
This package is not auto-updated.
Last update: 2022-02-01 13:05:45 UTC
README
Tasker 是一个自动化工具,用于编写小型和简单的部署脚本。
它适用于没有 CI 集成的单服务器项目。
如果您需要更多功能,请查看 Jenkins、TeamCity 等其他工具。它并不试图取代它们。Tasker 受 Deployer 项目的启发,您也可以看看它。Tasker 被设计成更加灵活(可覆盖)和面向对象,但功能较少。
安装
使用 composer 安装
composer require aes3xs/tasker
如何部署
将部署分为本地和远程两部分是一个不错的选择。
本地部分准备代码、预热文件缓存、下载供应商和其他操作,这些都影响项目目录内的所有内容。
没有外部依赖、服务或文件(除了供应商管理和 PHP 本身)。
没有数据库请求,没有迁移。
此脚本可以在任何地方执行,在构建服务器或本地机器上。它创建一个准备就绪的部署模块,可以直接在生产环境中复制和运行。
在项目中拥有此类脚本非常实用,并且它不应该有依赖关系,例如 composer。因为它负责运行它,如果您从仓库中获取裸项目。
这些步骤中的一些有时在 composer.json 脚本部分中定义。但我更喜欢独立的文件。
以下是一个示例(./bin/deploy)
#!/bin/bash set -e set -o pipefail SYMFONY_ENV=${SYMFONY_ENV:="dev"} SYMFONY_DEBUG=${SYMFONY_DEBUG:="1"} for i in "$@"; do case $i in -e=*|--env=*) SYMFONY_ENV="${i#*=}"; shift;; --no-debug) SYMFONY_DEBUG="0"; shift;; *);; esac done ROOT="$(dirname "$(dirname "$(readlink -fm "$0")")")" PHP=$(which "php") || { echo "PHP is not found" ; exit 1; } YARN=$(which "yarn") || { echo "Yarn is not found" ; exit 1; } COMPOSER=$(which "composer") || { echo "Composer is not found" ; exit 1; } if [ "$SYMFONY_DEBUG" == "0" ]; then NO_DEBUG="--no-debug"; fi CONSOLE="${ROOT}/bin/console --quiet --env=${SYMFONY_ENV} ${NO_DEBUG}" echo "SYMFONY_ENV = ${SYMFONY_ENV}" echo "SYMFONY_DEBUG = ${SYMFONY_DEBUG}" echo "PROJECT_ROOT = ${ROOT}" echo "PHP = ${PHP}" echo "YARN = ${YARN}" echo "COMPOSER = ${COMPOSER}" echo "CONSOLE = ${CONSOLE}" echo "" _exec () { echo -e "\033[1m[$(date +%T)] >\033[0m" $1 eval $1 } # Vendors _exec "cd ${ROOT} && ${COMPOSER} install --prefer-dist --no-progress --no-interaction --optimize-autoloader" _exec "cd ${ROOT} && ${YARN} install --prod --non-interactive" # Cache _exec "rm -rf ${ROOT}/var/cache/${SYMFONY_ENV}" _exec "chmod 0775 ${ROOT}/var/cache" _exec "${CONSOLE} cache:warmup" # Assets PATHS=( "${ROOT}/web/js" "${ROOT}/web/css" "${ROOT}/web/bundles" ) _exec "rm -rf ${PATHS[*]}" _exec "${CONSOLE} assets:install ${ROOT}/web --symlink --relative" _exec "${CONSOLE} assetic:dump ${ROOT}/web" # Writable _exec "if [ ! -w ${ROOT}/var/cache ]; then { echo 'Is not writable' ; exit 1; }; fi" _exec "if [ ! -w ${ROOT}/var/logs ]; then { echo 'Is not writable' ; exit 1; }; fi" _exec "if [ ! -w ${ROOT}/var/spool ]; then { echo 'Is not writable' ; exit 1; }; fi"
您也可以使用 Tasker 来编写它,但我建议保留独立的 shell 脚本。
部署过程的远程部分与外部服务(如 nginx、php-fpm、数据库)一起工作。
您还必须能够访问项目仓库以克隆它。
有一些重要的事情。
首先,以其他用户执行命令。
出于安全原因,服务器上的每个人必须有自己的凭据。
但是,项目本身配置为从单个用户工作,例如,www-data
。
因此,您必须为每个调用添加类似 sudo -u USER
的内容,这已经实现。
其次,对服务器上克隆仓库进行身份验证。
GitLab 有登录/密码选项或公钥,例如。
因此,更好的方法是使用 SSH 密钥,并且使用相同的密钥对 GitLab 或另一个系统进行身份验证。
这可以通过 SSH 转发身份验证来完成。
部署脚本示例
#!/usr/bin/env php <?php require_once "./vendor/autoload.php"; use Aes3xs\Tasker\Connection\Connection; use Aes3xs\Tasker\Service\Git; use Aes3xs\Tasker\Service\Releaser; use Aes3xs\Tasker\Service\Shell; use Aes3xs\Tasker\Service\Symfony; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { public $repository = 'REPOSITORY_PATH'; public $deployPath = '/var/www/PROJECT'; public $runUser = 'www-data'; public $branch = 'master'; protected function configure() { $this ->addArgument('server', InputArgument::REQUIRED) ->addOption('branch', 'b', InputOption::VALUE_OPTIONAL); } public function execute($server, Connection $connection) { if (!in_array($server, ['prod', 'dev'])) { throw new \RuntimeException('Server available values: prod, dev'); } $connection ->getParameters() ->setHost(null) ->setForwarding(true); // Configure to deploy on different servers $this->branch = 'master'; // Set branch if needed try { $this->runActions(array_filter($this->getAvailableActions(), function ($actionName) { return !in_array($actionName, ['execute', 'failNotify']); }), 'Deploy'); } catch (\Exception $e) { $this->runActions(['failNotify'], 'Failback'); } } public function switchUser(Shell $shell, $runUser) { $shell->setUser($runUser); return $runUser; } public function createRelease(Releaser $releaser, Git $git, Shell $shell, $deployPath, $repository, $branch) { $releaser->setDeployPath($deployPath); $releaser->prepare(); if ($releaser->isLocked() && $this->askConfirmationQuestion('Deploy is locked. Unlock?')) { $releaser->unlock(); } $releaser->lock(); $releaser->create(); $releasePath = $releaser->getReleasePath(); $releases = $releaser->getReleaseList(); $reference = $releases ? $releaser->getReleasePathByName(reset($releases)) : null; $shell->setCwd($releasePath); $git->cloneAt($repository, $releasePath, $branch, $reference); // Uses SSH forwarding if presented $releaser->updateReleaseShares(['var/logs', 'var/spool', 'var/sessions'], ['app/config/parameters.yml']); $shell->chmod('./bin', 0777); $shell->exec('./bin/deploy --env=prod --no-debug'); } public function migrate(Symfony $symfony) { $symfony->runCommand('doctrine:migrations:migrate', [], ['allow-no-migration']); } public function release(Releaser $releaser, Git $git, Shell $shell, $server, $branch) { $releaser->release(); $releaser->unlock(); $last_commits = $git->log($releaser->getReleasePath(), 3); $this->info(<<<EOL Server: {{ server }} Released: {{ releaser.getReleaseName() }} Branch: {{ server }} Last commits: $last_commits EOL ); $releaser->cleanup(5); $shell->setUser(null); $shell->exec("sudo service nginx reload"); $shell->exec("sudo service php-fpm reload"); } public function shutdownRoutine(Shell $shell) { $shell->setUser(null); } public function failNotify() { $this->error(<<<EOL Server: {{ server }} Release: {{ releaser.getReleaseName() }} Branch: {{ server }} FALURE EOL ); } } \TestRecipe::run();
概述
Tasker 的目的是使用一系列操作创建 PHP 二进制文件。
这些操作可以在本地或远程服务器上执行。
它们位于 "recipe" 类中。
Recipe 是一个自执行的命令行命令。
#!/usr/bin/env php <?php require_once "./vendor/autoload.php"; class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { public function execute() { // Do smth } } \TestRecipe::run();
使用第一行 #!/usr/bin/env php
使您的文件可执行
添加权限 chmod a+x ./testRecipe
然后添加 composer 的自动加载 require_once "./vendor/autoload.php";
,确保路径正确。
定义您自己的 recipe 类,从 \Aes3xs\Tasker\AbstractRecipe
扩展它
添加 execute
(默认)或其他将首先执行的方法。
调用静态 ::run()
或 ::run('yourMethodName')
操作
在动作内部,您可以实际完成工作。
动作是公开的非静态方法。
魔术方法(__*)或以get[A-Z](例如getSmth)开头的方法无法执行。
使用runActions(['prepare', 'release'])
或runAction('release')
来调用动作
您可以使用getAvailableActions()
获取配方中所有可用动作的列表
如果想在执行过程中跳过动作,请调用skipAction('only in production')
,类似于PhpUnit的$this->markTestSkipped()
#!/usr/bin/env php <?php require_once "./vendor/autoload.php"; class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { public function execute() { $this->runActions($this->getAvailableActions(), 'deployment'); } public function prepare() { } public function deploy($env) { if ('prod' === $env) { $this->runAction('restart'); } } public function restart() { } public function notify($env) { if ('prod' !== $env) { $this->skipAction('Production only'); } } } \TestRecipe::run();
命令
配方是一个带有参数和选项的命令。
它基于Symfony Console组件。
所以您有configure()
来定义哪些输入将可用。
以及相同的方法addArgument()
和addOption()
,您可以使用所有Symfony默认选项
- 使用--help显示有关命令的信息
- -v, -vv, -vvv以使输出更详细
- -q禁用输出,除了错误
- -n禁用用户输入(非交互式模式)
<?php use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { protected function configure() { $this ->addArgument('argument', InputArgument::REQUIRED) ->addOption('option', null, InputOption::VALUE_REQUIRED); } public function doSmthAction(InputInterface $input, $argument, $option) { $argumentValue = $input->getArgument('argument'); $optionValue = $input->getArgument('option'); $argumentValue = $argument; // same $optionValue = $option; // same } public function doSmth2Action(OutputInterface $output) { $output->writeln('Hello'); } }
您可以通过其名称访问定义的输入。
有一些辅助方法用于用户交互
要获取true/false结果,请使用$this->askConfirmationQuestion()
要从数组选项中选择,请使用$this->askChoiceQuestion()
(它返回值,而不是键)
要获取字符串输入,请使用$this->askQuestion()
<?php class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { public function askSmthAction() { $result = $this->askConfirmationQuestion('Are you sure?', false); $result = $this->askChoiceQuestion('Pick a color', ['red', 'green', 'blue'], 'green'); $result = $this->askQuestion('Enter your name', 'anonymous'); } }
您可以实现自己的问题或覆盖这些方法。
自动装配的资源
资源是动作的参数。
您可以使用资源名称或类名称将现有资源连接到动作调用。
类和名称可以指向不同的资源,因此结果可能是不可预测的。请避免这些情况。
资源按以下顺序排列(按优先级降序)
- 配方中的get[A-Z]方法,某些类型的动态属性
- 公开的非静态配方属性
- 输入参数
- 输入选项
- 容器服务(内部)
- 容器参数(内部)
如果有具有相同名称或类名称的资源,则将使用第一个出现。
Snake_case和camelCase处理相同。
可用的容器服务
- 输入(Symfony\Component\Console\Input\InputInterface)
- 输出(Symfony\Component\Console\Output\OutputInterface)
- 样式(Symfony\Component\Console\Style\SymfonyStyle)
- 连接(Aes3xs\Tasker\Connection\Connection)
- 记录器(Monolog\Logger)
- 运行器(Aes3xs\Tasker\Runner\Runner)
以及一些辅助工具
- shell(Aes3xs\Tasker\Service\Shell)
- composer(Aes3xs\Tasker\Service\Composer)
- git(Aes3xs\Tasker\Service\Git)
- 发布者(Aes3xs\Tasker\Service\Releaser)
- symfony(Aes3xs\Tasker\Service\Symfony)
<?php class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { /** * Can be obtained by: * dynamicRecipeResource * dynamic_recipe_resource */ public function getDynamicRecipeResource($dependency) { return $dependency * 10; } /** * Can be obtained by: * SomeClass * recipePropertyObject * recipe_property_object */ public $recipePropertyObject; // Contains \SomeClass /** * Can be obtained by: * recipeProperty * recipe_property */ public $recipeProperty; /** * Can be obtained by: * recipePropertyCallback * recipe_property_callback */ public $recipePropertyCallback; public function setupRecipeCallbackProperty() { // Callback arguments resolving same way as actions $this->recipePropertyCallback = function ($dependency) { return $dependency * 10; }; } protected function configure() { /** * Can be obtained by: * inputArgument * input_argument */ $this->addArgument('input_argument'); /** * Can be obtained by: * inputOption * input_option */ $this->addOption('input_option'); } /** * Can be obtained by class or name */ public function containerServicesAction( \Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output, \Symfony\Component\Console\Style\SymfonyStyle $style, \Aes3xs\Tasker\Connection\Connection $connection, \Monolog\Logger $logger, \Aes3xs\Tasker\Runner\Runner $runner, \Aes3xs\Tasker\Service\Shell $shell, \Aes3xs\Tasker\Service\Composer $composer, \Aes3xs\Tasker\Service\Git $git, \Aes3xs\Tasker\Service\Releaser $releaser, \Aes3xs\Tasker\Service\Symfony $symfony ) { // Do smth } }
连接
在开始使用之前,只需简单设置连接参数即可。
它会在第一次调用时自动初始化。目前不提供重用或重新连接。
本地连接相当简单。要使用它,请将主机留为null。
远程连接提供三种认证类型
- 登录/密码,使用
setPassword('password')
- 公钥(可选密钥),使用
setPublicKey($path or key itself)
- 代理转发,使用
setForwarding(true)
基于PhpSecLib的远程连接
实现了ssh扩展,但在转发损坏时禁用。
当您手动使用ssh转发时,存在环境变量$SSH_AUTH_SOCK,其中包含代理套接字的路径。因此,您可以使用转发继续连接到另一个服务器。
使用php ssh扩展此变量不存在。这是一个非常有必要的功能,因此我现在切换回PhpSecLib。
<?php class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe { protected function connect(\Aes3xs\Tasker\Connection\Connection $connection) { // By default connection is local // Local means host is null, 127.0.0.1 or localhost // Remote connection $connection ->getParameters() ->setHost('192.168.1.1') // Default port 22 ->setLogin('root') ->setPassword('password'); $connection->exec('echo hello'); } }
服务
Shell
Shell建立在连接之上。因此,如果您已经连接到远程(或本地)服务器,您也可以使用Shell。
它包含大多数可用的shell命令
- exec
- ln
- chmod
- chown
- rm
- mkdir
- touch
- readlink
- realpath
- dirname
- ls
- which
辅助方法
- exists()
- isFile()
- isDir()
- isLink()
- isWritable()
- isReadable()
- write()
- read()
- copy()
- copyPaths()
- linkPaths()
- checkWritable()
- checkReadable()
如果您想以其他用户身份运行所有命令,请使用setUser()进行配置。这样,所有命令都将添加sudo -EHu USER bash -c "COMMAND"
前缀。SSH代理转发也将可用。
如果您想从特定目录运行所有命令,请使用setCwd()进行配置。
这些选项只对Shell服务本身有效,而不是连接。
其他服务(发布者、Git、Composer、Symfony)也使用Shell,请考虑这一点。
发布者
发布者管理发布。它准备目录结构以存储您的发布,并在部署或回滚时链接它们。
首先调用setDeployPath()指向包含所有相关内容的根目录。
/var/www/project <- deploy_path
│
├─ releases
│ ├─ 20180101221100 (YmdHis format)
│ ├─ 20180101221101
│ ├─ 20180101221102
│ ├─ 20180101221103
│ ├─ 20180101221104 (Symfony example) <- current_path
│ │ ├─ app
│ │ │ └─ config
│ │ │ └─ ~parameters.yml (symlink in shared)
│ │ ├─ var
│ │ │ └─ ~logs (symlink in shared)
│ │ └─ release.lock (exists only in completed releases)
│ │
│ └─ 20180101221105 (deploy in progress...) <- release_path
│
├─ ~current (symlink to 20180101221104 for example)
│
├─ shared
│ ├─ app
│ │ └─ config
│ │ └─ parameters.yml
│ └─ var
│ └─ logs
│ └─ ...
│
└─ deploy.lock
然后调用prepare()构建目录结构。
使用lock()、unlock()和isLocked()控制deploy.lock文件,使用它来防止同时部署。
调用create()创建新版本,release()创建符号链接作为当前版本并添加release.lock文件,link()创建符号链接到特定现有版本,rollback()创建符号链接到上一个版本,cleanup()删除未使用的版本。通过getReleaseList()获取所有可用的版本,它只显示完成的版本,忽略损坏和意外的目录和文件。
使用updateReleaseShares()更新共享文件和目录。共享文件在所有版本中相同,并且它们被单独存储。
使用getCurrentReleasePath()获取当前链接版本的路径。
使用getCurrentReleaseName()获取当前版本的名称(目录名显然)。
Git
首选使用私有存储库的方式是代理转发。但您也可以设置setKeyPath()使用您的公钥。
方法
- checkout()
- cloneAt()
- log()
- fetch()
- getBranches()
- getCurrentBranch()
Composer
方法
- install()
- update()
- download()下载phar存档
Symfony
首先设置控制台路径,使用setConsolePath(),通常是release_path/bin/console
- setEnv()
- setDebug()
- setInteractive()
- runCommand()
向runCommand()传递参数和选项。选项是一个关联数组,键是选项名称(如果值为null,则选项被视为标志)。