Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions src/Command/Tree.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Deployer\Deployer;
use Deployer\Command\TreeCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface as Input;
use Symfony\Component\Console\Output\OutputInterface as Output;
use Hypernode\Deploy\DeployRunner;
Expand All @@ -14,6 +15,10 @@

class Tree extends TreeCommand
{
private const ARGUMENT_STAGE = 'stage';

private Deployer $deployer;

/**
* @var DeployRunner
*/
Expand All @@ -24,19 +29,111 @@ public function __construct(
DeployRunner $deployRunner,
) {
parent::__construct($deployer);
$this->deployer = $deployer;
$this->deployRunner = $deployRunner;
}

protected function configure(): void
{
parent::configure();
$this->addArgument(
self::ARGUMENT_STAGE,
InputArgument::OPTIONAL,
'Only show tasks that are applicable to the given stage'
);
}

protected function execute(Input $input, Output $output): int
{
$stage = $input->getArgument(self::ARGUMENT_STAGE);

try {
$this->deployRunner->prepare(true, true, $input->getArgument('task'), false);
$this->deployRunner->prepare(true, false, $stage ?? '', false);
} catch (InvalidConfigurationException | ValidationException $e) {
$output->write($e->getMessage());
return 1;
}
$result = parent::execute($input, $output);

return $result;
if ($stage !== null) {
if (!$this->isConfiguredStage($stage)) {
$output->writeln(
sprintf('<error>Stage "%s" is not defined in the deploy configuration.</error>', $stage)
);
return 1;
}

$this->disableTasksNotApplicableToStage($stage);
$output->writeln(sprintf('Showing tasks for stage <info>%s</info>:', $stage));
}

return parent::execute($input, $output);
}

private function isConfiguredStage(string $stage): bool
{
$config = $this->deployRunner->getConfiguration();
if ($config === null) {
return false;
}

foreach ($config->getStages() as $configStage) {
if ($configStage->getName() === $stage) {
return true;
}
}

return false;
}

private function disableTasksNotApplicableToStage(string $stage): void
{
foreach ($this->deployer->tasks->all() as $task) {
if (!self::selectorMatchesStage($task->getSelector(), $stage)) {
$task->disable();
}
}
}

/**
* Check whether a Deployer task selector can match a stage.
*
* Non-stage conditions (e.g. roles) are ignored, since a task is applicable
* to a stage as soon as one of its selectors or selector groups matches it.
*/
public static function selectorMatchesStage(?array $selector, string $stage): bool
{
if (empty($selector)) {
return true;
}

foreach ($selector as $conditions) {
$matchesStage = true;

foreach ($conditions as [$operator, $variable, $value]) {
if ($variable !== 'stage') {
continue;
}

if (!self::compareStage($operator, $value, $stage)) {
$matchesStage = false;
break;
}
}

if ($matchesStage) {
return true;
}
}

return false;
}

private static function compareStage(string $operator, string $expected, string $stage): bool
{
return match ($operator) {
'=' => $expected === $stage,
'!=' => $expected !== $stage,
default => false,
};
}
}
7 changes: 7 additions & 0 deletions src/DeployRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class DeployRunner

private array $deployedHostnames = [];
private string $deployedStage = '';
private ?Configuration $configuration = null;

public function __construct(
TaskFactory $taskFactory,
Expand Down Expand Up @@ -120,6 +121,7 @@ public function prepare(
$config = $this->configurationLoader->load(
$this->input->getOption('file') ?: 'deploy.php'
);
$this->configuration = $config;
$config->setLogger($this->log);

if ($configureBuildStage) {
Expand Down Expand Up @@ -410,4 +412,9 @@ public function getDeploymentReport()
$this->brancherHypernodesRegistered
);
}

public function getConfiguration(): ?Configuration
{
return $this->configuration;
}
}
7 changes: 7 additions & 0 deletions src/Deployer/TaskBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public function buildAll(array $commands, string $namePrefix): array
{
$tasks = [];
foreach ($commands as $command) {
// Only commands are executed by the task builder. Other task
// configurations (e.g. Tideways or Rumvision) are registered by
// their own ConfigurableTaskInterface implementation.
if (!$command instanceof Command) {
continue;
}

$name = $namePrefix . ':' . \count($tasks);

$this->build($command, $name);
Expand Down
45 changes: 45 additions & 0 deletions tests/Unit/Command/TreeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Hypernode\Deploy\Tests\Unit\Command;

use Deployer\Selector\Selector;
use Hypernode\Deploy\Command\Tree;
use PHPUnit\Framework\TestCase;

class TreeTest extends TestCase
{
/**
* @dataProvider selectorProvider
*/
public function testSelectorMatchesStage(?string $selector, string $stage, bool $expected): void
{
$parsedSelector = $selector === null ? null : Selector::parse($selector);

$this->assertSame($expected, Tree::selectorMatchesStage($parsedSelector, $stage));
}

/**
* @return array<string, array{?string, string, bool}>
*/
public static function selectorProvider(): array
{
return [
'no selector applies to every stage' => [null, 'production', true],
'matching stage' => ['stage=production', 'production', true],
'non-matching stage' => ['stage=production', 'staging', false],
'matching stage with roles' => [
'stage=production&roles=web,stage=production&roles=db',
'production',
true,
],
'non-matching stage with roles' => ['stage=production&roles=web', 'staging', false],
'roles only applies to every stage' => ['roles=web', 'staging', true],
'negated stage matches other stage' => ['stage!=production', 'staging', true],
'negated stage excludes stage' => ['stage!=production', 'production', false],
'any of multiple stages matches' => ['stage=production,stage=staging', 'staging', true],
'none of multiple stages matches' => ['stage=production,stage=staging', 'dev', false],
];
}
}
Loading