diff --git a/.github/wiki b/.github/wiki index 077086ae8c..fc94a144a0 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 077086ae8cb01397e60d148989955955efbafbda +Subproject commit fc94a144a07a30e7dd7b55f354633bb5e19227ce diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b85dc7d8b..5ce0409c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Keep Composer plugin command discovery compatible with consumer environments by moving unsupported Symfony Console named parameters out of command metadata/configuration and by decoupling the custom filesystem wrapper from Composer's bundled Symfony Filesystem signatures (#185) - Keep Composer autoload, Rector, and ECS from traversing nested fixture `vendor` directories when the composer-plugin consumer fixture has installed dependencies (#179) - Skip LICENSE generation cleanly when a consumer composer manifest omits or leaves the `license` field empty (#227) +- Run nested DevTools subprocesses without forcing PTY, fixing aggregate commands in non-interactive environments (#171) ## [1.20.0] - 2026-04-23 diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 556fc5b5bb..064c55da83 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -142,6 +142,11 @@ Likely causes: - a tool is prompting for confirmation; - CI is missing required environment variables. +DevTools streams child-process output through Symfony Process callbacks rather +than forcing pseudo-terminal execution. Aggregated commands such as +``reports``, ``standards``, and ``dev-tools:fix`` MUST keep nested commands +non-PTY in CI or other non-interactive process runners. + Recovery: .. code-block:: bash diff --git a/src/Process/ProcessQueue.php b/src/Process/ProcessQueue.php index a003bbb54a..fc626c52f2 100644 --- a/src/Process/ProcessQueue.php +++ b/src/Process/ProcessQueue.php @@ -87,10 +87,6 @@ public function add( bool $detached = false, ?string $label = null ): void { - if (Process::isPtySupported()) { - $process->setPty(true); - } - $this->entries[] = [ 'process' => $process, 'ignoreFailure' => $ignoreFailure, diff --git a/tests/Process/ProcessQueueTest.php b/tests/Process/ProcessQueueTest.php index 9091a863de..abe9022f60 100644 --- a/tests/Process/ProcessQueueTest.php +++ b/tests/Process/ProcessQueueTest.php @@ -76,19 +76,24 @@ protected function setUp(): void } /** - * @param ObjectProphecy $process - * * @return void */ - private function expectPtyConfiguration(ObjectProphecy $process): void + #[Test] + public function addWillNotEnablePtyForQueuedProcesses(): void { - if (! Process::isPtySupported()) { - return; - } + $process = $this->prophesize(Process::class); + $process->setPty(Argument::any()) + ->shouldNotBeCalled(); + $process->run(Argument::any()) + ->willReturn(ProcessQueueInterface::SUCCESS); + $process->getExitCode() + ->willReturn(ProcessQueueInterface::SUCCESS); + $process->isRunning() + ->willReturn(false); - $process->setPty(true) - ->willReturn($process->reveal()) - ->shouldBeCalled(); + $this->queue->add($process->reveal()); + + self::assertSame(ProcessQueueInterface::SUCCESS, $this->queue->run($this->output->reveal())); } /** @@ -102,7 +107,6 @@ private function createBlockingProcessMock( bool $isRunning = false, ): ObjectProphecy { $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->run(Argument::any()) ->willReturn($exitCode ?? ProcessQueueInterface::FAILURE); $process->getCommandLine() @@ -127,7 +131,6 @@ private function createBlockingProcessMock( private function createDetachedProcessMock(bool ...$runningSequence): ObjectProphecy { $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->getCommandLine() ->willReturn('test-command'); $process->getWorkingDirectory() @@ -212,7 +215,6 @@ public function runWithNullExitCodeReturnsFailure(): void public function runBlockingProcessExceptionReturnsFailure(): void { $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->getCommandLine() ->willReturn('test-command'); $process->getWorkingDirectory() @@ -249,7 +251,6 @@ public function runDetachedProcessStartsWithoutBlockingAndWaitsAtTheEnd(): void public function runDetachedProcessStartFailureReturnsFailure(): void { $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->getCommandLine() ->willReturn('test-command'); $process->getWorkingDirectory() @@ -271,7 +272,6 @@ public function runDetachedProcessStartFailureReturnsFailure(): void public function runDetachedProcessStartFailureWithIgnoreFailureReturnsSuccess(): void { $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->getCommandLine() ->willReturn('test-command'); $process->getWorkingDirectory() @@ -295,7 +295,6 @@ public function runWillWriteBlockingProcessOutputToStandardAndErrorOutputs(): vo $capturedCallback = null; $process = $this->prophesize(Process::class); - $this->expectPtyConfiguration($process); $process->run(Argument::that(function ($callback) use (&$capturedCallback): bool { $capturedCallback = $callback; @@ -335,7 +334,6 @@ public function waitWillFlushFinishedDetachedOutputWithoutWaitingForEveryProcess $capturedSecondCallback = null; $firstProcess = $this->prophesize(Process::class); - $this->expectPtyConfiguration($firstProcess); $firstProcess->getCommandLine() ->willReturn('first-command'); $firstProcess->getWorkingDirectory() @@ -355,7 +353,6 @@ public function waitWillFlushFinishedDetachedOutputWithoutWaitingForEveryProcess })->shouldBeCalled(); $secondProcess = $this->prophesize(Process::class); - $this->expectPtyConfiguration($secondProcess); $secondProcess->getCommandLine() ->willReturn('second-command'); $secondProcess->getWorkingDirectory()