diff --git a/.github/wiki b/.github/wiki index 13a815246..005922a42 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 13a8152468bab7f9a0cbc1c44bcfb6a5b5acdf4c +Subproject commit 005922a42075e4fb639cde3ec51c525f5ab55fd1 diff --git a/.github/workflows/wiki-preview.yml b/.github/workflows/wiki-preview.yml index bc2c61d86..c3ef160c3 100644 --- a/.github/workflows/wiki-preview.yml +++ b/.github/workflows/wiki-preview.yml @@ -37,6 +37,7 @@ jobs: name: Update Wiki Preview runs-on: ubuntu-latest permissions: + actions: write contents: write pull-requests: read @@ -110,6 +111,13 @@ jobs: pull: "--rebase --autostash" push: true + - name: Dispatch tests for wiki pointer commit + if: steps.submodule_status.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: gh workflow run tests.yml --ref "${HEAD_REF}" -f max-outdated=-1 + - uses: ./.dev-tools-actions/.github/actions/summary/write with: markdown: | @@ -118,3 +126,4 @@ jobs: - Preview branch: `${{ env.WIKI_PREVIEW_BRANCH }}` - Submodule pointer changed: `${{ steps.submodule_status.outputs.changed }}` - Parent repository pointer commit result: `${{ steps.submodule_status.outputs.changed == 'true' && 'updated' || 'unchanged' }}` + - Tests dispatch result: `${{ steps.submodule_status.outputs.changed == 'true' && 'requested' || 'not needed' }}` diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index 9e05b6cd1..48995515a 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -11,6 +11,7 @@ concurrency: jobs: preview: permissions: + actions: write contents: write pull-requests: read uses: ./.github/workflows/wiki-preview.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c74aa65..e95201b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 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) +- Ignore intentional Composer Dependency Analyser shadow dependency findings by default while adding `dependencies --show-shadow-dependencies` for audits (#233) +- Dispatch the required test workflow after wiki preview automation updates a pull-request `.github/wiki` pointer, avoiding permanently pending required checks on bot-authored pointer commits (#230) ## [1.21.0] - 2026-04-24 diff --git a/docs/advanced/branch-protection-and-bot-commits.rst b/docs/advanced/branch-protection-and-bot-commits.rst index 73d05faf5..4e02a20e1 100644 --- a/docs/advanced/branch-protection-and-bot-commits.rst +++ b/docs/advanced/branch-protection-and-bot-commits.rst @@ -99,9 +99,15 @@ updates on PR branches while keeping ``main`` protected. Required test checks must still report for workflow-managed pointer commits. The tests workflow therefore triggers on every pull request update without top-level path filters. This ensures GitHub always creates the required -``Run Tests`` matrix checks for the latest pull request head, including bot -commits that only refresh ``.github/wiki``. Test workflow concurrency cancels -older in-progress runs for the same pull request so the newest commit owns the +``Run Tests`` matrix checks for ordinary pull request updates. + +Workflow-managed ``.github/wiki`` pointer commits need one extra step. GitHub +does not start another ``pull_request`` or ``push`` workflow run for commits +pushed with the built-in workflow token. After the wiki preview workflow commits +a parent-repository pointer update, it explicitly dispatches ``tests.yml`` for +the pull request head branch so the newest bot-authored commit receives the +required ``Run Tests`` matrix checks. Test workflow concurrency cancels older +in-progress runs for the same pull request so the newest commit owns the required check contexts. At a high level, the workflows need permission to read repository contents, @@ -128,8 +134,9 @@ distinguish open pull requests from closed or merged ones before deleting ``wiki.yml`` is now preview-only, so its called workflow keeps ``contents: write`` for wiki preview commits and parent-repository submodule -pointer updates, while retaining ``pull-requests: read`` to inspect pull -request metadata safely. +pointer updates, ``actions: write`` to dispatch ``tests.yml`` after bot-authored +pointer commits, and ``pull-requests: read`` to inspect pull request metadata +safely. ``wiki-maintenance-entry.yml`` and ``wiki-maintenance.yml`` keep ``contents: write`` for wiki publication and cleanup tasks, and diff --git a/docs/commands/dependencies.rst b/docs/commands/dependencies.rst index 1bbcc45e2..e390b3f85 100644 --- a/docs/commands/dependencies.rst +++ b/docs/commands/dependencies.rst @@ -55,6 +55,16 @@ Options Asks ``composer-dependency-analyser`` to dump usages for the given package or wildcard pattern and enables ``--show-all-usages`` automatically. +``--show-shadow-dependencies`` (optional) + Reports shadow dependencies instead of applying the Fast Forward default + ignore for intentional dependency groups. + + By default, DevTools hides ``SHADOW_DEPENDENCY`` findings because Fast + Forward packages may intentionally require ecosystem bundles, meta packages, + or convenience packages that install related dependencies for consumers. + Use this flag when auditing whether a package has accidental shadow + dependencies that should be removed or documented more precisely. + ``--json`` Emit a structured machine-readable payload instead of the normal terminal output. @@ -95,6 +105,12 @@ Dump all matched usages for one package: composer dependencies --dump-usage=symfony/console +Audit shadow dependencies: + +.. code-block:: bash + + composer dependencies --show-shadow-dependencies + Apply the upgrade workflow and then analyze dependencies: .. code-block:: bash @@ -135,6 +151,8 @@ Behavior consumer repositories can extend the baseline instead of copying it whole - ``--dump-usages `` and ``--show-all-usages`` when ``--dump-usage`` is passed to the DevTools command + - the ``FAST_FORWARD_DEV_TOOLS_SHOW_SHADOW_DEPENDENCIES`` process environment + flag, which is enabled when ``--show-shadow-dependencies`` is passed - ``jack breakpoint`` maps ``--max-outdated`` to Jack's ``--limit`` option. - ``--max-outdated=-1`` keeps ``jack breakpoint`` in the workflow for reporting, but its failure is ignored so only missing or unused dependency findings fail diff --git a/docs/configuration/overriding-defaults.rst b/docs/configuration/overriding-defaults.rst index d971b0246..dfe50aaef 100644 --- a/docs/configuration/overriding-defaults.rst +++ b/docs/configuration/overriding-defaults.rst @@ -141,6 +141,13 @@ consumers can extend the default configuration using the This approach keeps the Fast Forward baseline while letting consumer repositories add project-specific ignores or scan rules. +The baseline ignores ``SHADOW_DEPENDENCY`` findings by default because Fast +Forward packages may intentionally require dependency groups, ecosystem bundles, +or meta packages that install related dependencies for consumers. Run +``composer dependencies --show-shadow-dependencies`` when you want to audit +those findings and decide whether a package should keep, document, or remove a +direct dependency. + What Is Not Overwritten Automatically -------------------------------------- diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index 7807ffbf1..5e0f3630f 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -77,6 +77,7 @@ Analyzes missing, unused, misplaced, and outdated Composer dependencies. composer dependencies --max-outdated=-1 composer dependencies --dev composer dependencies --dump-usage=symfony/console + composer dependencies --show-shadow-dependencies composer dependencies --upgrade --dev Important details: @@ -88,6 +89,10 @@ Important details: override locally; - ``--dump-usage=`` forwards to ``composer-dependency-analyser --dump-usages --show-all-usages``; +- ``--show-shadow-dependencies`` keeps shadow dependency findings visible for + audits; without it, DevTools hides intentional Fast Forward dependency-group + shadows so CI does not fail on ecosystem or meta packages that deliberately + install related dependencies for consumers; - it uses ``jack breakpoint --limit=`` to fail when too many outdated dependencies accumulate; - ``--max-outdated=-1`` keeps the Jack outdated report in the output but diff --git a/resources/github-actions/wiki.yml b/resources/github-actions/wiki.yml index d54c27adc..cdf27268f 100644 --- a/resources/github-actions/wiki.yml +++ b/resources/github-actions/wiki.yml @@ -5,6 +5,7 @@ on: types: [opened, synchronize, reopened] permissions: + actions: write contents: write pull-requests: read @@ -15,6 +16,7 @@ concurrency: jobs: preview: permissions: + actions: write contents: write pull-requests: read # Pull-request wiki previews live here. Publication and preview cleanup are diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index 548bbcd0a..a0f88b476 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -41,6 +41,8 @@ */ final class ComposerDependencyAnalyserConfig { + public const string ENV_SHOW_SHADOW_DEPENDENCIES = 'FAST_FORWARD_DEV_TOOLS_SHOW_SHADOW_DEPENDENCIES'; + /** * Dependencies that are only required by the packaged DevTools distribution itself. * @@ -90,6 +92,10 @@ public static function configure(?callable $customize = null): Configuration { $configuration = new Configuration(); + if (! self::shouldShowShadowDependencies()) { + self::applyIgnoresShadowDependencies($configuration); + } + if (DevToolsPathResolver::isRepositoryCheckout()) { self::applyPackagedRepositoryIgnores($configuration); } @@ -101,12 +107,39 @@ public static function configure(?callable $customize = null): Configuration return $configuration; } + /** + * The default configuration ignores shadow dependencies because Fast + * Forward packages MAY intentionally require dependency groups. For example, + * ecosystem or meta packages can require related PSR or framework packages + * so consumers do not need to install every package one by one. + * + * @param Configuration $configuration the analyser configuration to customize + * + * @return Configuration the modified configuration with shadow dependencies ignored + */ + public static function applyIgnoresShadowDependencies(Configuration $configuration): Configuration + { + $configuration->ignoreErrors([ErrorType::SHADOW_DEPENDENCY]); + + return $configuration; + } + + /** + * Determines whether shadow dependency reports SHOULD remain visible. + * + * @return bool + */ + public static function shouldShowShadowDependencies(): bool + { + return '1' === getenv(self::ENV_SHOW_SHADOW_DEPENDENCIES); + } + /** * Applies the ignores required only by the packaged DevTools repository. * * @param Configuration $configuration the analyser configuration to customize * - * @return void + * @return Configuration the modified configuration with packaged repository ignores applied */ public static function applyPackagedRepositoryIgnores(Configuration $configuration): Configuration { diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index cbecf1820..4cd40494d 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use InvalidArgumentException; @@ -101,6 +102,11 @@ protected function configure(): void name: 'dump-usage', mode: InputOption::VALUE_REQUIRED, description: 'Dump usages for the given package pattern and show all matched usages.', + ) + ->addOption( + name: 'show-shadow-dependencies', + mode: InputOption::VALUE_NONE, + description: 'Report shadow dependencies instead of applying Fast Forward intentional-shadow ignores.', ); } @@ -176,7 +182,13 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr ->withArgument('--show-all-usages'); } - return $processBuilder->build('vendor/bin/composer-dependency-analyser'); + $showShadowDependencies = (bool) $input->getOption('show-shadow-dependencies'); + $process = $processBuilder->build('vendor/bin/composer-dependency-analyser'); + $process->setEnv([ + ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $showShadowDependencies ? '1' : '0', + ]); + + return $process; } /** diff --git a/tests/Config/ComposerDependencyAnalyserConfigTest.php b/tests/Config/ComposerDependencyAnalyserConfigTest.php index 7267a40b7..83b8e3c7b 100644 --- a/tests/Config/ComposerDependencyAnalyserConfigTest.php +++ b/tests/Config/ComposerDependencyAnalyserConfigTest.php @@ -28,6 +28,8 @@ use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; +use function Safe\putenv; + #[CoversClass(ComposerDependencyAnalyserConfig::class)] #[UsesClass(DevToolsPathResolver::class)] final class ComposerDependencyAnalyserConfigTest extends TestCase @@ -43,6 +45,48 @@ public function configureWillReturnConfiguration(): void self::assertInstanceOf(Configuration::class, $configuration); } + /** + * @return void + */ + #[Test] + public function configureWillIgnoreShadowDependenciesByDefault(): void + { + $originalValue = getenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES); + + try { + putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES); + $configuration = ComposerDependencyAnalyserConfig::configure(); + + self::assertTrue( + $configuration->getIgnoreList() + ->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, null, 'vendor/shadow-package') + ); + } finally { + self::restoreShadowDependenciesEnvironment($originalValue); + } + } + + /** + * @return void + */ + #[Test] + public function configureWillKeepShadowDependenciesVisibleWhenRequested(): void + { + $originalValue = getenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES); + + try { + putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES . '=1'); + $configuration = ComposerDependencyAnalyserConfig::configure(); + + self::assertFalse( + $configuration->getIgnoreList() + ->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, null, 'vendor/shadow-package') + ); + } finally { + self::restoreShadowDependenciesEnvironment($originalValue); + } + } + /** * @return void */ @@ -102,4 +146,34 @@ public function applyPackagedRepositoryIgnoresWillReturnTheSameConfigurationInst ComposerDependencyAnalyserConfig::applyPackagedRepositoryIgnores($configuration) ); } + + /** + * @return void + */ + #[Test] + public function applyIgnoresShadowDependenciesWillReturnTheSameConfigurationInstance(): void + { + $configuration = new Configuration(); + + self::assertSame( + $configuration, + ComposerDependencyAnalyserConfig::applyIgnoresShadowDependencies($configuration) + ); + } + + /** + * @param false|string $value + * + * @return void + */ + private static function restoreShadowDependenciesEnvironment(false|string $value): void + { + if (false === $value) { + putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES); + + return; + } + + putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES . '=' . $value); + } } diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index a6399ff8c..9bf384d74 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -21,7 +21,9 @@ use FastForward\DevTools\Console\Command\DependenciesCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; +use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; use FastForward\DevTools\Process\ProcessBuilder; +use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -79,6 +81,8 @@ protected function setUp(): void ->willReturn(false); $this->input->getOption('dump-usage') ->willReturn(null); + $this->input->getOption('show-shadow-dependencies') + ->willReturn(false); $this->input->getOption('json') ->willReturn(false); $this->input->getOption('pretty-json') @@ -167,6 +171,30 @@ public function executeWillIgnoreJackFailuresWhenMaxOutdatedIsDisabled(): void self::assertSame(DependenciesCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function composerDependencyAnalyserProcessWillHideShadowDependenciesByDefault(): void + { + $this->input->getOption('show-shadow-dependencies') + ->willReturn(false); + + $this->assertComposerDependencyAnalyserEnvironment('0'); + } + + /** + * @return void + */ + #[Test] + public function composerDependencyAnalyserProcessCanReportShadowDependencies(): void + { + $this->input->getOption('show-shadow-dependencies') + ->willReturn(true); + + $this->assertComposerDependencyAnalyserEnvironment('1'); + } + /** * @return int */ @@ -175,4 +203,36 @@ private function executeCommand(): int return (new ReflectionMethod($this->command, 'execute')) ->invoke($this->command, $this->input->reveal(), $this->output->reveal()); } + + /** + * @param string $expectedValue + * + * @return void + */ + private function assertComposerDependencyAnalyserEnvironment(string $expectedValue): void + { + $processBuilder = $this->prophesize(ProcessBuilderInterface::class); + $configuredProcessBuilder = $this->prophesize(ProcessBuilderInterface::class); + $process = $this->prophesize(Process::class); + $command = new DependenciesCommand( + $processBuilder->reveal(), + $this->processQueue->reveal(), + $this->fileLocator->reveal(), + $this->logger->reveal(), + ); + + $processBuilder->withArgument('--config', '/app/composer-dependency-analyser.php') + ->willReturn($configuredProcessBuilder->reveal()) + ->shouldBeCalledOnce(); + $configuredProcessBuilder->build('vendor/bin/composer-dependency-analyser') + ->willReturn($process->reveal()) + ->shouldBeCalledOnce(); + $process->setEnv([ + ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $expectedValue, + ])->willReturn($process->reveal()) + ->shouldBeCalledOnce(); + + (new ReflectionMethod($command, 'getComposerDependencyAnalyserCommand')) + ->invoke($command, $this->input->reveal()); + } }