From a3b7cc7ba8cd0b913dd74221fcce7694884edae8 Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Thu, 18 Jun 2026 15:49:41 +0200 Subject: [PATCH 1/6] feat: implement task execute command --- .../src/Command/Task/TaskExecuteCommand.php | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 legacy/src/Command/Task/TaskExecuteCommand.php diff --git a/legacy/src/Command/Task/TaskExecuteCommand.php b/legacy/src/Command/Task/TaskExecuteCommand.php new file mode 100644 index 00000000..ccdbc515 --- /dev/null +++ b/legacy/src/Command/Task/TaskExecuteCommand.php @@ -0,0 +1,110 @@ +addArgument('task', InputArgument::REQUIRED, 'The name of the task to execute') + ->addOption('variable', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A variable to set when running the task, in the format type:name=value'); + + $this->selector->addProjectOption($this->getDefinition()); + $this->selector->addEnvironmentOption($this->getDefinition()); + $this->addCompleter($this->selector); + + $this->addExample('Execute the "migrate" task on the environment "main"', 'migrate --environment main'); + $this->addExample('Execute the "migrate" task, setting environment variable FOO=bar', 'migrate -e main --variable env:FOO=bar'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $selection = $this->selector->getSelection($input); + $environment = $selection->getEnvironment(); + + $taskName = $input->getArgument('task'); + $variables = $this->parseVariables($input->getOption('variable')); + + $this->stdErr->writeln(sprintf( + 'Executing task %s on the environment %s', + $taskName, + $this->api->getEnvironmentLabel($environment), + )); + + $url = $environment->getUri() . '/tasks/' . rawurlencode($taskName) . '/run'; + $response = $this->api->getHttpClient()->post($url, ['json' => ['variables' => (object) $variables]]); + + $result = new Result( + (array) Utils::jsonDecode((string) $response->getBody(), true), + $environment->getUri(), + $this->api->getHttpClient(), + Activity::class, + ); + $activities = $result->getActivities(); + + $this->stdErr->writeln(''); + $this->stdErr->writeln('The task has been triggered.'); + + $executable = $this->config->getStr('application.executable'); + if ($activities !== []) { + // Reference the exact activity ID so the log can be followed even + // when several activities are running in parallel. + $activity = reset($activities); + $this->stdErr->writeln(sprintf( + 'To follow its log, run: %s activity:log %s', + $executable, + $activity->id, + )); + } else { + $this->stdErr->writeln(sprintf( + 'To follow its log, run: %s activity:log --type environment.task -e %s', + $executable, + $environment->id, + )); + } + + return 0; + } + + /** + * Parses variables in the format type:name=value into a nested array. + * + * @param string[] $variables + * + * @return array> + */ + private function parseVariables(array $variables): array + { + $map = []; + $variable = new Variable(); + foreach ($variables as $var) { + [$type, $name, $value] = $variable->parse($var); + $map[$type][$name] = $value; + } + + return $map; + } +} From ee07e1e71c729def31200ca341b184b666ac18bc Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Fri, 19 Jun 2026 16:08:55 +0200 Subject: [PATCH 2/6] feat: add tasks list command --- legacy/src/Command/Task/TaskListCommand.php | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 legacy/src/Command/Task/TaskListCommand.php diff --git a/legacy/src/Command/Task/TaskListCommand.php b/legacy/src/Command/Task/TaskListCommand.php new file mode 100644 index 00000000..d3a296cd --- /dev/null +++ b/legacy/src/Command/Task/TaskListCommand.php @@ -0,0 +1,84 @@ + */ + private array $tableHeader = [ + 'name' => 'Name', + 'type' => 'Type', + 'command' => 'Command', + 'timeout' => 'Timeout (s)', + ]; + + public function __construct(private readonly Api $api, private readonly Selector $selector, private readonly Table $table) + { + parent::__construct(); + } + + protected function configure(): void + { + Table::configureInput($this->getDefinition(), $this->tableHeader); + $this->selector->addProjectOption($this->getDefinition()); + $this->selector->addEnvironmentOption($this->getDefinition()); + $this->addCompleter($this->selector); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $selection = $this->selector->getSelection($input); + $environment = $selection->getEnvironment(); + + try { + $response = $this->api->getHttpClient()->get($environment->getUri() . '/tasks'); + } catch (BadResponseException $e) { + throw ApiResponseException::create($e->getRequest(), $e->getResponse(), $e); + } + $tasks = (array) Utils::jsonDecode((string) $response->getBody(), true); + + if ($tasks === []) { + $this->stdErr->writeln(sprintf( + 'No tasks were found on the environment %s.', + $this->api->getEnvironmentLabel($environment), + )); + + return 0; + } + + $rows = []; + foreach ($tasks as $task) { + $rows[] = [ + 'name' => $task['name'] ?? '', + 'type' => $task['type'] ?? '', + 'command' => isset($task['run']['command']) ? trim((string) $task['run']['command']) : '', + 'timeout' => $task['run']['timeout'] ?? '', + ]; + } + + if (!$this->table->formatIsMachineReadable()) { + $this->stdErr->writeln(sprintf( + 'Tasks on the environment %s:', + $this->api->getEnvironmentLabel($environment), + )); + } + + $this->table->render($rows, $this->tableHeader); + + return 0; + } +} From f9d1f471efcda2033e91902a337492cca4707f3b Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Fri, 19 Jun 2026 16:11:51 +0200 Subject: [PATCH 3/6] refactor: add task activity to the const list --- legacy/src/Command/Task/TaskExecuteCommand.php | 2 +- legacy/src/Command/Task/TaskListCommand.php | 2 +- legacy/src/Service/ActivityLoader.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/legacy/src/Command/Task/TaskExecuteCommand.php b/legacy/src/Command/Task/TaskExecuteCommand.php index ccdbc515..62dc12e6 100644 --- a/legacy/src/Command/Task/TaskExecuteCommand.php +++ b/legacy/src/Command/Task/TaskExecuteCommand.php @@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); $url = $environment->getUri() . '/tasks/' . rawurlencode($taskName) . '/run'; - $response = $this->api->getHttpClient()->post($url, ['json' => ['variables' => (object) $variables]]); + $response = $this->api->getHttpClient()->request('POST', $url, ['json' => ['variables' => (object) $variables]]); $result = new Result( (array) Utils::jsonDecode((string) $response->getBody(), true), diff --git a/legacy/src/Command/Task/TaskListCommand.php b/legacy/src/Command/Task/TaskListCommand.php index d3a296cd..40c94303 100644 --- a/legacy/src/Command/Task/TaskListCommand.php +++ b/legacy/src/Command/Task/TaskListCommand.php @@ -45,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $environment = $selection->getEnvironment(); try { - $response = $this->api->getHttpClient()->get($environment->getUri() . '/tasks'); + $response = $this->api->getHttpClient()->request('GET', $environment->getUri() . '/tasks'); } catch (BadResponseException $e) { throw ApiResponseException::create($e->getRequest(), $e->getResponse(), $e); } diff --git a/legacy/src/Service/ActivityLoader.php b/legacy/src/Service/ActivityLoader.php index 55d4d4b0..cfb2581a 100644 --- a/legacy/src/Service/ActivityLoader.php +++ b/legacy/src/Service/ActivityLoader.php @@ -201,6 +201,7 @@ public static function getAvailableTypes(): array 'environment.source-operation', 'environment.subscription.update', 'environment.synchronize', + 'environment.task', 'environment.update.http_access', 'environment.update.restrict_robots', 'environment.update.smtp', From 97bfa79757fd231cfc8a4eb0459208bb2e008bb3 Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Mon, 22 Jun 2026 13:24:13 +0200 Subject: [PATCH 4/6] refactor: reword from execute to run --- .../Task/{TaskExecuteCommand.php => TaskRunCommand.php} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename legacy/src/Command/Task/{TaskExecuteCommand.php => TaskRunCommand.php} (90%) diff --git a/legacy/src/Command/Task/TaskExecuteCommand.php b/legacy/src/Command/Task/TaskRunCommand.php similarity index 90% rename from legacy/src/Command/Task/TaskExecuteCommand.php rename to legacy/src/Command/Task/TaskRunCommand.php index 62dc12e6..7e2605e4 100644 --- a/legacy/src/Command/Task/TaskExecuteCommand.php +++ b/legacy/src/Command/Task/TaskRunCommand.php @@ -18,8 +18,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -#[AsCommand(name: 'task:execute', description: 'Execute a task on an environment')] -class TaskExecuteCommand extends CommandBase +#[AsCommand(name: 'task:run', description: 'Execute a task on an environment')] +class TaskRunCommand extends CommandBase { public function __construct(private readonly Api $api, private readonly Config $config, private readonly Selector $selector) { @@ -36,8 +36,8 @@ protected function configure(): void $this->selector->addEnvironmentOption($this->getDefinition()); $this->addCompleter($this->selector); - $this->addExample('Execute the "migrate" task on the environment "main"', 'migrate --environment main'); - $this->addExample('Execute the "migrate" task, setting environment variable FOO=bar', 'migrate -e main --variable env:FOO=bar'); + $this->addExample('Run the "migrate" task on the environment "main"', 'migrate --environment main'); + $this->addExample('Run the "migrate" task, setting environment variable FOO=bar', 'migrate -e main --variable env:FOO=bar'); } protected function execute(InputInterface $input, OutputInterface $output): int From 3ba76b8c579daaa6de2fc37e8298cd0b3a7d1782 Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Mon, 22 Jun 2026 13:41:57 +0200 Subject: [PATCH 5/6] refactor: add exception wrapper --- legacy/src/Command/Task/TaskRunCommand.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/legacy/src/Command/Task/TaskRunCommand.php b/legacy/src/Command/Task/TaskRunCommand.php index 7e2605e4..2f9a225c 100644 --- a/legacy/src/Command/Task/TaskRunCommand.php +++ b/legacy/src/Command/Task/TaskRunCommand.php @@ -4,12 +4,14 @@ namespace Platformsh\Cli\Command\Task; +use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Utils; use Platformsh\Cli\Command\CommandBase; use Platformsh\Cli\Model\Variable; use Platformsh\Cli\Selector\Selector; use Platformsh\Cli\Service\Api; use Platformsh\Cli\Service\Config; +use Platformsh\Client\Exception\ApiResponseException; use Platformsh\Client\Model\Activity; use Platformsh\Client\Model\Result; use Symfony\Component\Console\Attribute\AsCommand; @@ -55,7 +57,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); $url = $environment->getUri() . '/tasks/' . rawurlencode($taskName) . '/run'; - $response = $this->api->getHttpClient()->request('POST', $url, ['json' => ['variables' => (object) $variables]]); + try { + $response = $this->api->getHttpClient()->request('POST', $url, ['json' => ['variables' => (object) $variables]]); + } catch (BadResponseException $e) { + throw ApiResponseException::create($e->getRequest(), $e->getResponse(), $e); + } $result = new Result( (array) Utils::jsonDecode((string) $response->getBody(), true), From 692bf0972002b14d20c91f53e30ea245d00477ca Mon Sep 17 00:00:00 2001 From: Veronika Tolkachova Date: Fri, 31 Jul 2026 17:04:48 +0200 Subject: [PATCH 6/6] feat(task:run): add --wait, production confirmation, shared variable parser Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BhxKwNnEqiy6VJbZw8KPSL --- .../Command/SourceOperation/RunCommand.php | 19 +------- legacy/src/Command/Task/TaskRunCommand.php | 43 +++++++++---------- legacy/src/Model/Variable.php | 19 ++++++++ 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/legacy/src/Command/SourceOperation/RunCommand.php b/legacy/src/Command/SourceOperation/RunCommand.php index 17955c58..61289ece 100644 --- a/legacy/src/Command/SourceOperation/RunCommand.php +++ b/legacy/src/Command/SourceOperation/RunCommand.php @@ -45,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); - $variables = $this->parseVariables($input->getOption('variable')); + $variables = (new Variable())->parseMultiple($input->getOption('variable')); $this->io->debug('Parsed variables: ' . json_encode($variables)); $environment = $selection->getEnvironment(); @@ -104,21 +104,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $success ? 0 : 1; } - - /** - * @param string[] $variables - * - * @return array> - */ - private function parseVariables(array $variables): array - { - $map = []; - $variable = new Variable(); - foreach ($variables as $var) { - [$type, $name, $value] = $variable->parse($var); - $map[$type][$name] = $value; - } - - return $map; - } } diff --git a/legacy/src/Command/Task/TaskRunCommand.php b/legacy/src/Command/Task/TaskRunCommand.php index 2f9a225c..bd7a203a 100644 --- a/legacy/src/Command/Task/TaskRunCommand.php +++ b/legacy/src/Command/Task/TaskRunCommand.php @@ -9,8 +9,10 @@ use Platformsh\Cli\Command\CommandBase; use Platformsh\Cli\Model\Variable; use Platformsh\Cli\Selector\Selector; +use Platformsh\Cli\Service\ActivityMonitor; use Platformsh\Cli\Service\Api; use Platformsh\Cli\Service\Config; +use Platformsh\Cli\Service\QuestionHelper; use Platformsh\Client\Exception\ApiResponseException; use Platformsh\Client\Model\Activity; use Platformsh\Client\Model\Result; @@ -23,7 +25,7 @@ #[AsCommand(name: 'task:run', description: 'Execute a task on an environment')] class TaskRunCommand extends CommandBase { - public function __construct(private readonly Api $api, private readonly Config $config, private readonly Selector $selector) + public function __construct(private readonly ActivityMonitor $activityMonitor, private readonly Api $api, private readonly Config $config, private readonly QuestionHelper $questionHelper, private readonly Selector $selector) { parent::__construct(); } @@ -32,7 +34,9 @@ protected function configure(): void { $this ->addArgument('task', InputArgument::REQUIRED, 'The name of the task to execute') - ->addOption('variable', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A variable to set when running the task, in the format type:name=value'); + ->addOption('variable', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A variable to set when running the task, in the format type:name=value') + // Tasks can run for a long time, so waiting is opt-in rather than the default. + ->addOption('wait', null, InputOption::VALUE_NONE, 'Wait for the task to complete'); $this->selector->addProjectOption($this->getDefinition()); $this->selector->addEnvironmentOption($this->getDefinition()); @@ -48,7 +52,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $environment = $selection->getEnvironment(); $taskName = $input->getArgument('task'); - $variables = $this->parseVariables($input->getOption('variable')); + $variables = (new Variable())->parseMultiple($input->getOption('variable')); + + if ($environment->type === 'production' && !$this->questionHelper->confirm(sprintf( + 'Are you sure you want to run the task %s on the production environment %s?', + $taskName, + $this->api->getEnvironmentLabel($environment, 'comment'), + ))) { + return 1; + } $this->stdErr->writeln(sprintf( 'Executing task %s on the environment %s', @@ -74,6 +86,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln(''); $this->stdErr->writeln('The task has been triggered.'); + // Waiting is opt-in so the exit code can reflect a failed activity, e.g. in CI. + if ($input->getOption('wait') && $activities !== []) { + $success = $this->activityMonitor->waitMultiple($activities, $selection->getProject()); + return $success ? 0 : 1; + } + $executable = $this->config->getStr('application.executable'); if ($activities !== []) { // Reference the exact activity ID so the log can be followed even @@ -94,23 +112,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - /** - * Parses variables in the format type:name=value into a nested array. - * - * @param string[] $variables - * - * @return array> - */ - private function parseVariables(array $variables): array - { - $map = []; - $variable = new Variable(); - foreach ($variables as $var) { - [$type, $name, $value] = $variable->parse($var); - $map[$type][$name] = $value; - } - - return $map; - } } diff --git a/legacy/src/Model/Variable.php b/legacy/src/Model/Variable.php index 1d744161..4e4fa9f1 100644 --- a/legacy/src/Model/Variable.php +++ b/legacy/src/Model/Variable.php @@ -28,6 +28,25 @@ public function parse(string $variable): array return [$this->validateType($type), $this->validateName($name), $value]; } + /** + * Parses multiple type:name=value definitions into a nested array. + * + * @param string[] $variables + * + * @return array> + * Values keyed by type and then by name. + */ + public function parseMultiple(array $variables): array + { + $map = []; + foreach ($variables as $var) { + [$type, $name, $value] = $this->parse($var); + $map[$type][$name] = $value; + } + + return $map; + } + /** * Validates the variable type (AKA namespace). *