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 6176cf to 6248aa
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ composer dev-tools tests -- --coverage=.dev-tools/coverage
- Rector rules: `src/Rector/`
- Composer plugin: `src/Composer/`

**Architecture Direction:**

- Avoid introducing new dependencies on `composer/composer` outside the
existing Composer plugin integration and legacy surfaces already awaiting
decoupling.
- Prefer DevTools-owned interfaces for generic runtime concerns such as
environment variables, process execution, filesystem access, and console
presentation instead of reaching for Composer utility classes.
- When an existing Composer utility is convenient, first check whether a small
local abstraction would support the ongoing Composer decoupling work with
minimal code.
- During the Composer-to-Symfony command migration, preserve the global
execution affordances already relied on by nested tools: `--ansi`/`--no-ansi`,
cache and `--cache-dir` handling, and working-directory behavior.
- Keep color behavior explicit in command wrappers for tools with known flags
instead of probing binaries dynamically; for example, Symfony/Composer-style
tools can receive `--ansi`, while PHPUnit uses `--colors=always`.
- Keep child-process environment policy centralized in `ProcessQueue`
configurators, including disabling Xdebug for non-coverage subprocesses while
preserving coverage drivers when PCOV is unavailable.
- Prefer the project-approved `Safe\*` function imports whenever replacing or
adding native PHP calls that have Safe equivalents, so Rector does not have
to rewrite them during commit checks.

**Naming Conventions:**

- Classes: PascalCase
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Preserve color-friendly nested command environments, explicit Symfony Console ANSI flags, concise process section labels, and fixture-safe PhpMetrics execution with bounded Packagist lookups without restoring PTY (#239)
- Disable Xdebug for queued child processes unless coverage requires it without PCOV, reducing repeated Composer Xdebug warnings in orchestrated commands (#239)
- Keep the reports workflow permission warning loop shell-safe for paths containing backslashes (#244)
- Keep required PHPUnit matrix checks reporting after workflow-managed `.github/wiki` pointer commits by running the pull-request test workflow without top-level path filters and aligning the packaged consumer test wrapper (#230)
- Publish pending and per-version required PHPUnit statuses for workflow-dispatched test runs so wiki pointer commits do not wait for an all-matrix aggregate status (#230)
Expand Down
9 changes: 7 additions & 2 deletions docs/commands/metrics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Options
Comma-separated directories that should be excluded from analysis.

Default:
``vendor,test,tests,tmp,cache,spec,build,.dev-tools,backup,resources``.
``vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures``.

``--target=<directory>``
Output directory for the generated metrics reports.
Expand Down Expand Up @@ -93,4 +93,9 @@ Behavior
- ``--json`` and ``--pretty-json`` keep DevTools itself structured while
running PhpMetrics in a quieter mode to avoid polluting the captured payload;
- it runs PhpMetrics through the active PHP binary and suppresses PhpMetrics
deprecation notices emitted by the dependency itself.
deprecation notices emitted by the dependency itself;
- it keeps PhpMetrics' Composer analysis enabled so the reports include package
metadata from the root ``composer.json`` and ``composer.lock``, while the
default exclusions keep nested fixture projects out of that Composer scan;
- it limits PhpMetrics' per-package Packagist socket wait so package freshness
enrichment cannot leave metrics generation stuck indefinitely.
4 changes: 3 additions & 1 deletion docs/internals/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ command list:
- Services
* - ``FastForward\DevTools\Process\ProcessBuilderInterface``
- ``ProcessBuilderInterface`` and ``ProcessQueueInterface`` build and
execute subprocess pipelines.
execute subprocess pipelines, while process environment and output
Symfony-style sections keep nested command output readable without PTY
and suppress unnecessary Xdebug overhead in child processes.
* - ``Filesystem and metadata``
- ``FilesystemInterface``, ``ComposerJsonInterface``, and
``FileLocatorInterface`` resolve local files, project metadata, and
Expand Down
13 changes: 12 additions & 1 deletion docs/running/specialized-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ Important details:
structured while forwarding JSON or quieter modes to the wrapped tools where
available;
- it suppresses deprecation notices emitted by the PhpMetrics dependency
itself so the command output stays readable.
itself so the command output stays readable;
- it keeps PhpMetrics' Composer analysis enabled so report generation includes
root package metadata, while default exclusions keep nested fixture projects
out of that Composer scan;
- it limits PhpMetrics' per-package Packagist socket wait so package freshness
enrichment cannot leave report generation stuck indefinitely.

``code-style``
--------------
Expand Down Expand Up @@ -270,6 +275,12 @@ Important details:
``--progress`` re-enables it for human-readable runs;
- ``--json`` and ``--pretty-json`` are propagated to each subprocess, while
their progress output is suppressed where supported;
- human-readable runs keep nested command output grouped with concise local
section boundaries, pass color-friendly environment variables to
subprocesses, and forward explicit ANSI flags to Symfony Console tools;
- queued subprocesses run with ``XDEBUG_MODE=off`` when Xdebug is loaded but
the command does not need Xdebug for coverage, or when PCOV can provide
coverage instead;
- it is the reporting stage used by ``standards``.

``skills``
Expand Down
20 changes: 20 additions & 0 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,26 @@ Recovery:
When calling lower-level tools directly, use their non-interactive flags and
provide required values through environment variables or workflow inputs.

Repeated Composer Xdebug Warnings
---------------------------------

Scope: local orchestration commands.

Symptoms:

- Composer repeatedly prints that it is operating slower than normal because
Xdebug is enabled;
- aggregate commands emit the warning once per nested Composer subprocess.

Behavior:

DevTools sets ``XDEBUG_MODE=off`` for queued child processes when Xdebug is
loaded but the child command does not need Xdebug for coverage. Coverage runs
keep Xdebug available when PCOV is not loaded, and use PCOV when it is
available. A warning printed by the top-level ``composer dev-tools`` process
can still appear before DevTools itself starts; run that command with
``XDEBUG_MODE=off`` when Xdebug is not needed for the top-level process.

GitHub Actions Error Annotations
--------------------------------

Expand Down
10 changes: 7 additions & 3 deletions src/Console/Command/CodeStyleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
->build('composer normalize');

$processBuilder = $this->processBuilder
->withArgument('--ansi')
->withArgument('--config', $this->fileLocator->locate(self::CONFIG));

if (! $progress) {
Expand All @@ -148,9 +149,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$ecs = $processBuilder->build('vendor/bin/ecs');

$this->processQueue->add($composerUpdate);
$this->processQueue->add($composerNormalize);
$this->processQueue->add($ecs);
$this->processQueue->add(process: $composerUpdate, label: 'Refreshing Composer Lock');
$this->processQueue->add(
process: $composerNormalize,
label: 'Normalizing composer.json with Composer Normalize'
);
$this->processQueue->add(process: $ecs, label: 'Checking Code Style with Easy Coding Standard');

$result = $this->processQueue->run($processOutput);

Expand Down
34 changes: 26 additions & 8 deletions src/Console/Command/DependenciesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $this->failure($invalidArgumentException->getMessage(), $input);
}

$this->processQueue->add($this->getRaiseToInstalledCommand($input));
$this->processQueue->add($this->getOpenVersionsCommand($input));
$this->processQueue->add(
process: $this->getRaiseToInstalledCommand($input),
label: 'Raising Dependency Constraints with Jack',
);
$this->processQueue->add(
process: $this->getOpenVersionsCommand($input),
label: 'Opening Dependency Constraints with Jack',
);

if ($input->getOption('upgrade')) {
$this->processQueue->add($this->getComposerUpdateCommand());
$this->processQueue->add($this->getComposerNormalizeCommand());
$this->processQueue->add(
process: $this->getComposerUpdateCommand(),
label: 'Updating Dependencies with Composer'
);
$this->processQueue->add(
process: $this->getComposerNormalizeCommand(),
label: 'Normalizing composer.json with Composer Normalize',
);
}

if (! $jsonOutput) {
Expand All @@ -143,10 +155,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
]);
}

$this->processQueue->add($this->getComposerDependencyAnalyserCommand($input));
$this->processQueue->add(
$this->getJackBreakpointCommand($input, $maximumOutdated),
$this->shouldIgnoreOutdatedFailures($maximumOutdated),
process: $this->getComposerDependencyAnalyserCommand($input),
label: 'Analyzing Dependencies with Composer Dependency Analyser',
);
$this->processQueue->add(
process: $this->getJackBreakpointCommand($input, $maximumOutdated),
ignoreFailure: $this->shouldIgnoreOutdatedFailures($maximumOutdated),
label: 'Checking Outdated Dependencies with Jack',
);

$result = $this->processQueue->run($processOutput);
Expand Down Expand Up @@ -279,7 +295,9 @@ private function getComposerUpdateCommand(): Process
*/
private function getComposerNormalizeCommand(): Process
{
return $this->processBuilder->build('composer normalize');
return $this->processBuilder
->withArgument('--ansi')
->build('composer normalize');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Command/DocsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$phpdoc = $processBuilder->build('vendor/bin/phpdoc');

$this->processQueue->add($phpdoc);
$this->processQueue->add(process: $phpdoc, label: 'Generating API Docs with phpDocumentor');

$result = $this->processQueue->run($processOutput);

Expand Down
5 changes: 4 additions & 1 deletion src/Console/Command/FundingCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,10 @@ private function normalizeComposerFile(string $composerFile, OutputInterface $ou
$processBuilder = $processBuilder->withArgument('--file', $composerBasename);
}

$this->processQueue->add($processBuilder->build('composer normalize'));
$this->processQueue->add(
process: $processBuilder->build('composer normalize'),
label: 'Normalizing composer.json with Composer Normalize',
);

return $this->processQueue->run($output);
}
Expand Down
17 changes: 14 additions & 3 deletions src/Console/Command/MetricsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ final class MetricsCommand extends BaseCommand implements LoggerAwareCommandInte
*/
private const int PHP_ERROR_REPORTING = \E_ALL & ~\E_DEPRECATED;

/**
* @var int the maximum seconds PhpMetrics may wait on each Packagist package lookup
*/
private const int PHP_DEFAULT_SOCKET_TIMEOUT = 1;

/**
* @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process
* @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process
Expand Down Expand Up @@ -80,7 +85,7 @@ protected function configure(): void
name: 'exclude',
mode: InputOption::VALUE_OPTIONAL,
description: 'Comma-separated directories that SHOULD be excluded from analysis.',
default: 'vendor,test,tests,tmp,cache,spec,build,.dev-tools,backup,resources',
default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures',
)
->addOption(
name: 'target',
Expand Down Expand Up @@ -132,9 +137,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$this->processQueue->add(
$processBuilder
process: $processBuilder
->withArgument('.')
->build([\PHP_BINARY, '-derror_reporting=' . self::PHP_ERROR_REPORTING, self::BINARY])
->build([
\PHP_BINARY,
'-derror_reporting=' . self::PHP_ERROR_REPORTING,
'-ddefault_socket_timeout=' . self::PHP_DEFAULT_SOCKET_TIMEOUT,
self::BINARY,
]),
label: 'Generating Metrics with PhpMetrics',
);

$result = $this->processQueue->run($processOutput);
Expand Down
5 changes: 3 additions & 2 deletions src/Console/Command/PhpDocCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$phpCsFixer = $processBuilder->build('vendor/bin/php-cs-fixer fix');

$processBuilder = $this->processBuilder
->withArgument('--ansi')
->withArgument('--config', $this->fileLocator->locate(RefactorCommand::CONFIG))
->withArgument('--autoload-file', 'vendor/autoload.php')
->withArgument('--only', AddMissingMethodPhpDocRector::class);
Expand All @@ -203,8 +204,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$rector = $processBuilder->build('vendor/bin/rector process');

$this->processQueue->add($phpCsFixer);
$this->processQueue->add($rector);
$this->processQueue->add(process: $phpCsFixer, label: 'Fixing PHPDoc File Headers with PHP-CS-Fixer');
$this->processQueue->add(process: $rector, label: 'Adding Missing PHPDoc with Rector');

$result = $this->processQueue->run($processOutput);

Expand Down
6 changes: 5 additions & 1 deletion src/Console/Command/RefactorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$processBuilder = $this->processBuilder
->withArgument('process')
->withArgument('--ansi')
->withArgument('--config')
->withArgument($this->fileLocator->locate(self::CONFIG));

Expand All @@ -137,7 +138,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--dry-run');
}

$this->processQueue->add($processBuilder->build('vendor/bin/rector'));
$this->processQueue->add(
process: $processBuilder->build('vendor/bin/rector'),
label: 'Refactoring Code with Rector',
);

$result = $this->processQueue->run($processOutput);

Expand Down
28 changes: 17 additions & 11 deletions src/Console/Command/ReportsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$docsBuilder = $this->processBuilder
->withArgument('--target', $target);

$coverageBuilder = $this->processBuilder
->withArgument('--coverage-summary')
->withArgument('--coverage', $coveragePath);

$metricsBuilder = $this->processBuilder
->withArgument('--junit', $coveragePath . '/junit.xml')
->withArgument('--target', $metricsPath);

if (! $jsonOutput) {
$docsBuilder = $docsBuilder->withArgument('--ansi');
$coverageBuilder = $coverageBuilder->withArgument('--ansi');
$metricsBuilder = $metricsBuilder->withArgument('--ansi');
}

if (null !== $cacheArgument) {
$docsBuilder = $docsBuilder->withArgument($cacheArgument);
}
Expand All @@ -153,10 +167,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$docs = $docsBuilder->build('composer dev-tools docs --');

$coverageBuilder = $this->processBuilder
->withArgument('--coverage-summary')
->withArgument('--coverage', $coveragePath);

if (null !== $cacheArgument) {
$coverageBuilder = $coverageBuilder->withArgument($cacheArgument);
}
Expand All @@ -179,10 +189,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$coverage = $coverageBuilder->build('composer dev-tools tests --');

$metricsBuilder = $this->processBuilder
->withArgument('--junit', $coveragePath . '/junit.xml')
->withArgument('--target', $metricsPath);

if ($progress) {
$metricsBuilder = $metricsBuilder->withArgument('--progress');
}
Expand All @@ -197,9 +203,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$metrics = $metricsBuilder->build('composer dev-tools metrics --');

$this->processQueue->add(process: $docs, detached: true);
$this->processQueue->add(process: $coverage);
$this->processQueue->add(process: $metrics);
$this->processQueue->add(process: $docs, detached: true, label: 'Generating API Docs Report');
$this->processQueue->add(process: $coverage, label: 'Generating Coverage Report');
$this->processQueue->add(process: $metrics, label: 'Generating Metrics Report');

$result = $this->processQueue->run($processOutput);

Expand Down
Loading
Loading