Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from 077086 to fc94a1
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions src/Process/ProcessQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 14 additions & 17 deletions tests/Process/ProcessQueueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,24 @@ protected function setUp(): void
}

/**
* @param ObjectProphecy<Process> $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()));
}

/**
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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;

Expand Down Expand Up @@ -335,7 +334,6 @@ public function waitWillFlushFinishedDetachedOutputWithoutWaitingForEveryProcess
$capturedSecondCallback = null;

$firstProcess = $this->prophesize(Process::class);
$this->expectPtyConfiguration($firstProcess);
$firstProcess->getCommandLine()
->willReturn('first-command');
$firstProcess->getWorkingDirectory()
Expand All @@ -355,7 +353,6 @@ public function waitWillFlushFinishedDetachedOutputWithoutWaitingForEveryProcess
})->shouldBeCalled();

$secondProcess = $this->prophesize(Process::class);
$this->expectPtyConfiguration($secondProcess);
$secondProcess->getCommandLine()
->willReturn('second-command');
$secondProcess->getWorkingDirectory()
Expand Down
Loading