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],
+ ];
+ }
+}