aes3xs/yodler

此包已被弃用且不再维护。作者建议使用 aes3xs/tasker 包代替。

任务自动化工具

dev-master 2018-02-25 12:48 UTC

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,则选项被视为标志)。