From d672e9d305cdf502b1a557a32e96d9c71bc06396 Mon Sep 17 00:00:00 2001 From: poespas Date: Tue, 27 Oct 2026 11:28:44 +0100 Subject: [PATCH] add optional stage argument to tree command --- src/Command/Tree.php | 103 +++++++++++++++++++++++++++++++- src/DeployRunner.php | 7 +++ src/Deployer/TaskBuilder.php | 7 +++ tests/Unit/Command/TreeTest.php | 45 ++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Command/TreeTest.php diff --git a/src/Command/Tree.php b/src/Command/Tree.php index f3cb121..91832e3 100644 --- a/src/Command/Tree.php +++ b/src/Command/Tree.php @@ -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; @@ -14,6 +15,10 @@ class Tree extends TreeCommand { + private const ARGUMENT_STAGE = 'stage'; + + private Deployer $deployer; + /** * @var DeployRunner */ @@ -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('Stage "%s" is not defined in the deploy configuration.', $stage) + ); + return 1; + } + + $this->disableTasksNotApplicableToStage($stage); + $output->writeln(sprintf('Showing tasks for stage %s:', $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, + }; } } diff --git a/src/DeployRunner.php b/src/DeployRunner.php index f12f32c..c615c6f 100644 --- a/src/DeployRunner.php +++ b/src/DeployRunner.php @@ -57,6 +57,7 @@ class DeployRunner private array $deployedHostnames = []; private string $deployedStage = ''; + private ?Configuration $configuration = null; public function __construct( TaskFactory $taskFactory, @@ -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) { @@ -410,4 +412,9 @@ public function getDeploymentReport() $this->brancherHypernodesRegistered ); } + + public function getConfiguration(): ?Configuration + { + return $this->configuration; + } } diff --git a/src/Deployer/TaskBuilder.php b/src/Deployer/TaskBuilder.php index 051ce00..15572e9 100644 --- a/src/Deployer/TaskBuilder.php +++ b/src/Deployer/TaskBuilder.php @@ -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); diff --git a/tests/Unit/Command/TreeTest.php b/tests/Unit/Command/TreeTest.php new file mode 100644 index 0000000..35b9713 --- /dev/null +++ b/tests/Unit/Command/TreeTest.php @@ -0,0 +1,45 @@ +assertSame($expected, Tree::selectorMatchesStage($parsedSelector, $stage)); + } + + /** + * @return array + */ + 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], + ]; + } +}