From e0964dc6edab96f488906e6767df97f296a3235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Thu, 3 Oct 2024 12:41:58 +0200 Subject: [PATCH] Laying groundwork for testing the library --- .github/workflows/build.yaml | 82 ++++++++++++++++++-------- .gitignore | 5 ++ README.md | 26 +++++++++ composer-dependency-analyser.php | 22 +++++++ composer.json | 8 ++- src/task/setono_dotenv.php | 93 +++++++++++++++++++----------- symfony.lock | 58 +++++++++++++++++++ tests/Integration/DeployerTest.php | 36 ++++++++++++ tests/Integration/deploy.php | 24 ++++++++ tests/docker/Dockerfile | 13 +++++ tests/docker/ssh/id_rsa | 7 +++ tests/docker/ssh/id_rsa.pub | 1 + 12 files changed, 314 insertions(+), 61 deletions(-) create mode 100644 composer-dependency-analyser.php create mode 100644 symfony.lock create mode 100644 tests/Integration/DeployerTest.php create mode 100644 tests/Integration/deploy.php create mode 100644 tests/docker/Dockerfile create mode 100644 tests/docker/ssh/id_rsa create mode 100644 tests/docker/ssh/id_rsa.pub diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fe6dd45..df249c8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,12 +1,18 @@ name: "build" - on: push: branches: - "*.x" - pull_request: ~ + paths-ignore: + - "*.md" + pull_request: + paths-ignore: + - "*.md" workflow_dispatch: ~ +env: + PHP_EXTENSIONS: "mbstring" + jobs: coding-standards: name: "Coding Standards" @@ -25,24 +31,25 @@ jobs: - name: "Checkout" uses: "actions/checkout@v4" - - name: "Install PHP with extensions" + - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" with: - coverage: "none" php-version: "${{ matrix.php-version }}" - - - name: "Validate composer.json and composer.lock" - run: "composer validate --strict" + extensions: "${{ env.PHP_EXTENSIONS }}" + coverage: "none" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Run ergebnis/composer-normalize" + - name: "Validate composer" + run: "composer validate --strict" + + - name: "Check composer normalized" run: "composer normalize --dry-run" - - name: "Run symplify/easy-coding-standard" + - name: "Check style" run: "composer check-style" dependency-analysis: @@ -54,31 +61,40 @@ jobs: matrix: php-version: - "8.1" + - "8.2" + - "8.3" dependencies: + - "lowest" - "highest" steps: - name: "Checkout" uses: "actions/checkout@v4" - - name: "Install PHP with extensions" + - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" with: coverage: "none" + extensions: "${{ env.PHP_EXTENSIONS }}" php-version: "${{ matrix.php-version }}" - tools: "composer-require-checker, composer-unused" + + - name: "Remove require-dev section in composer.json" + run: "composer config --unset require-dev" + + - name: "Remove autoload-dev section in composer.json" + run: "composer config --unset autoload-dev" + + - name: "Add shipmonk/composer-dependency-analyser to composer.json" + run: "composer require --dev --no-install --no-update --no-plugins --no-scripts shipmonk/composer-dependency-analyser" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Run maglnet/composer-require-checker" - run: "composer-require-checker check" - - - name: "Run icanhazstring/composer-unused" - run: "composer-unused" + - name: "Run dependency analysis" + run: "vendor/bin/composer-dependency-analyser" static-code-analysis: name: "Static Code Analysis" @@ -89,25 +105,31 @@ jobs: matrix: php-version: - "8.1" + - "8.2" + - "8.3" dependencies: + - "lowest" - "highest" steps: - name: "Checkout" uses: "actions/checkout@v4" - - name: "Install PHP with extensions" + - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" with: - coverage: "none" php-version: "${{ matrix.php-version }}" + extensions: "${{ env.PHP_EXTENSIONS }}" + coverage: "none" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" + - name: "Static analysis" + run: "composer analyse" unit-tests: name: "Unit tests" @@ -117,6 +139,8 @@ jobs: matrix: php-version: - "8.1" + - "8.2" + - "8.3" dependencies: - "lowest" @@ -126,16 +150,26 @@ jobs: - name: "Checkout" uses: "actions/checkout@v4" + - name: "Build Docker image" + run: "docker build -t setono/deployer-cron --no-cache ./tests/docker" + + - name: "Run Docker container" + run: "docker run -d -p 2222:22 setono/deployer-cron" + + - name: "Change permissions" + run: "chmod 600 ./tests/docker/ssh/id_rsa && chmod 644 ./tests/docker/ssh/id_rsa.pub" + - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" with: - php-version: "${{ matrix.php-version }}" coverage: "none" + extensions: "${{ env.PHP_EXTENSIONS }}" + php-version: "${{ matrix.php-version }}" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Run phpunit" - run: "composer phpunit" + run: "vendor/bin/phpunit" diff --git a/.gitignore b/.gitignore index f99cb76..04478f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ /composer.lock /.phpunit.result.cache /.build/ + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### diff --git a/README.md b/README.md index f36df3b..6bc8c28 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,32 @@ require_once 'recipe/setono_dotenv.php'; This will automatically hook into the default flow of Deployer. +## Testing + +1. Set correct permissions on the SSH keys: + + ```shell + chmod 600 tests/docker/ssh/id_rsa && chmod 644 tests/docker/ssh/id_rsa.pub + ``` + +2. Build the Docker image: + + ```shell + docker build -t setono/deployer-dotenv --no-cache ./tests/docker + ``` + +3. Run the Docker container: + + ```shell + docker run -d -p 2222:22 setono/deployer-dotenv + ``` + +4. Run the tests: + + ```shell + vendor/bin/phpunit + ``` + [ico-version]: https://poser.pugx.org/setono/deployer-dotenv/v/stable [ico-license]: https://poser.pugx.org/setono/deployer-dotenv/license [ico-github-actions]: https://github.com/Setono/deployer-dotenv/workflows/build/badge.svg diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php new file mode 100644 index 0000000..700db37 --- /dev/null +++ b/composer-dependency-analyser.php @@ -0,0 +1,22 @@ +addPathToExclude(__DIR__ . '/tests') + ->ignoreUnknownClasses([ + Deployer\Deployer::class, + Deployer\Task\Context::class, + ]) + ->ignoreUnknownFunctions([ + 'Deployer\after', + 'Deployer\before', + 'Deployer\get', + 'Deployer\run', + 'Deployer\set', + 'Deployer\task', + 'Deployer\upload', + ]) + ->ignoreErrorsOnPackage('deployer/deployer', [ErrorType::UNUSED_DEPENDENCY]) +; diff --git a/composer.json b/composer.json index 50d4bff..f56ae8f 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,11 @@ "webmozart/assert": "^1.11" }, "require-dev": { + "phpseclib/phpseclib": "^3.0", "phpunit/phpunit": "^10.5", - "setono/code-quality-pack": "^2.8.2" + "setono/code-quality-pack": "^2.8.3", + "shipmonk/composer-dependency-analyser": "^1.7", + "symfony/flex": "^2.4" }, "prefer-stable": true, "autoload": { @@ -34,7 +37,8 @@ "config": { "allow-plugins": { "ergebnis/composer-normalize": true, - "dealerdirect/phpcodesniffer-composer-installer": false + "dealerdirect/phpcodesniffer-composer-installer": false, + "symfony/flex": true }, "sort-packages": true }, diff --git a/src/task/setono_dotenv.php b/src/task/setono_dotenv.php index 6a29180..e8bdcd0 100644 --- a/src/task/setono_dotenv.php +++ b/src/task/setono_dotenv.php @@ -14,12 +14,11 @@ use function Deployer\run; use function Deployer\task; use function Deployer\test; +use function Deployer\upload; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Dotenv\Dotenv; use Webmozart\Assert\Assert; -use function Deployer\upload; /** * This step has to come AFTER the deploy:update_code step because @@ -66,45 +65,14 @@ return; } - $output = output(); - - $outputVariablesFunction = static function (OutputInterface $output, array $variables): void { - ksort($variables); - - $table = new Table($output); - $table->setRows([ - ['Variable', 'Value'], - new TableSeparator(), - ]); - - /** - * @var string $key - * @var string $val - */ - foreach ($variables as $key => $val) { - $table->addRow([$key, $val]); - } - - $table->render(); - }; - - $evalEnv = static function (string $envContents): array { - /** @var array $res */ - $res = eval('?>' . $envContents); - Assert::isArray($res); - Assert::allScalar($res); - - return $res; - }; - /** * We want two arrays to begin with. This allows us to easily compare the two arrays later on * when the $variables may have been changed by the user */ - $variables = $initialVariables = $evalEnv(run('cat {{release_path}}/.env.local.php')); + $variables = $initialVariables = evaluatePhpEnvFile('{{release_path}}/.env.local.php'); while (true) { - $outputVariablesFunction($output, $variables); + outputEnvironmentVariables($variables); $confirmation = askConfirmation('Do you want to update ' . (isset($confirmation) ? 'more' : 'any') . ' environment variables?'); if (false === $confirmation) { @@ -124,6 +92,24 @@ } } + while (true) { + outputEnvironmentVariables($variables); + + $confirmation = askConfirmation('Do you want to remove ' . (isset($confirmation) ? 'more' : 'any') . ' environment variables?'); + if (false === $confirmation) { + break; + } + + while (true) { + $variable = ask('Input environment variable. Press when you are finished removing', '', array_keys($variables)); + if ('' === $variable) { + break; + } + + unset($variables[$variable]); + } + } + $stage = getStage(); /** @@ -187,3 +173,40 @@ function getStage(): string return $stage; } + +function outputEnvironmentVariables(array $variables): void +{ + ksort($variables); + + $table = new Table(output()); + $table->setRows([ + ['Variable', 'Value'], + new TableSeparator(), + ]); + + /** + * @var string $key + * @var string $val + */ + foreach ($variables as $key => $val) { + $table->addRow([$key, $val]); + } + + $table->render(); +} + +/** + * @return array + */ +function evaluatePhpEnvFile(string $path): array +{ + $data = run(sprintf('cat %s', $path)); + Assert::stringNotEmpty($data); + + /** @var array $res */ + $res = eval('?>' . $data); + Assert::isArray($res); + Assert::allScalar($res); + + return $res; +} diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000..44b6a40 --- /dev/null +++ b/symfony.lock @@ -0,0 +1,58 @@ +{ + "phpstan/phpstan": { + "version": "1.12", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + } + }, + "phpunit/phpunit": { + "version": "10.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.6", + "ref": "7364a21d87e658eb363c5020c072ecfdc12e2326" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "squizlabs/php_codesniffer": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.6", + "ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f" + } + }, + "symfony/console": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + } +} diff --git a/tests/Integration/DeployerTest.php b/tests/Integration/DeployerTest.php new file mode 100644 index 0000000..1823485 --- /dev/null +++ b/tests/Integration/DeployerTest.php @@ -0,0 +1,36 @@ +assertSame(0, $return); + + $key = PublicKeyLoader::load(file_get_contents(__DIR__ . '/../docker/ssh/id_rsa')); + + $ssh = new SSH2('127.0.0.1', 2222); + if (!$ssh->login('root', $key)) { + throw new \RuntimeException('Could not connection to the server'); + } + + // todo add assertions + } +} diff --git a/tests/Integration/deploy.php b/tests/Integration/deploy.php new file mode 100644 index 0000000..551ee61 --- /dev/null +++ b/tests/Integration/deploy.php @@ -0,0 +1,24 @@ +setPort(2222) + ->setRemoteUser('root') + ->setIdentityFile(__DIR__ . '/../docker/ssh/id_rsa') + ->setSshArguments(['-o UserKnownHostsFile=/dev/null', '-o StrictHostKeyChecking=no']) + ->set('deploy_path', '~/deployer'); + +// Hooks +after('deploy:failed', 'deploy:unlock'); diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 0000000..ad88ffe --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM php:8.2-cli-alpine + +RUN apk --update add --no-cache openssh bash git rsync \ + && rm -rf /var/cache/apk/* + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +RUN ssh-keygen -A + +COPY ssh/id_rsa.pub /root/.ssh/authorized_keys + +EXPOSE 22 +CMD ["/usr/sbin/sshd", "-D"] diff --git a/tests/docker/ssh/id_rsa b/tests/docker/ssh/id_rsa new file mode 100644 index 0000000..e069cb6 --- /dev/null +++ b/tests/docker/ssh/id_rsa @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCdiagktpAtXMnuUNYx2f0SmEd1poPtKOt8ak36svY72QAAAKBl54lxZeeJ +cQAAAAtzc2gtZWQyNTUxOQAAACCdiagktpAtXMnuUNYx2f0SmEd1poPtKOt8ak36svY72Q +AAAEBImjcgdW3172W2/T7QVGxtaUJQkNHcJ5itbaaulHrjHJ2JqCS2kC1cye5Q1jHZ/RKY +R3Wmg+0o63xqTfqy9jvZAAAAG2xvZXZnYWFyZEBNYWNCb29rLVByby5sb2NhbAEC +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/docker/ssh/id_rsa.pub b/tests/docker/ssh/id_rsa.pub new file mode 100644 index 0000000..4268297 --- /dev/null +++ b/tests/docker/ssh/id_rsa.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ2JqCS2kC1cye5Q1jHZ/RKYR3Wmg+0o63xqTfqy9jvZ deployer@local