From 0acd83359a6b8a32993ccecc6220e2c5e22e29aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 20 Oct 2026 16:44:25 -0300 Subject: [PATCH 1/5] feat!: distribute DevTools through CPX and portable skills --- .agents/agents/README.md | 7 +- .agents/agents/agents-maintainer.md | 4 +- .agents/agents/changelog-maintainer.md | 2 +- .agents/agents/consumer-sync-auditor.md | 14 +- .agents/agents/docs-writer.md | 4 +- .agents/agents/issue-editor.md | 4 +- .agents/agents/issue-implementer.md | 20 +- .agents/agents/php-style-curator.md | 4 +- .agents/agents/quality-pipeline-auditor.md | 16 +- .agents/agents/readme-maintainer.md | 4 +- .agents/agents/review-guardian.md | 16 +- .agents/agents/test-guardian.md | 4 +- .../skills/create-agentsmd/agents/openai.yaml | 4 - .../skills/github-issues/agents/openai.yaml | 4 - .../github-pull-request/agents/openai.yaml | 4 - .../phpdoc-code-style/agents/openai.yaml | 4 - .../skills/phpunit-tests/agents/openai.yaml | 4 - .agents/skills/sphinx-docs/agents/openai.yaml | 4 - .changelog/dev-tools-2-external-runtime.md | 12 + .gitattributes | 2 + .github/actions/php/setup-composer/action.yml | 22 +- .../detect-dev-tools-runtime.sh | 8 - .../setup-composer/dev-tools-runtime-lib.sh | 97 ++-- .../expose-dev-tools-runtime.sh | 3 + .../resolve-report-template.php | 54 +++ .github/actions/php/setup-phpdoc/action.yml | 25 + .github/workflows/auto-assign.yml | 10 +- .github/workflows/label-sync.yml | 2 +- .github/workflows/reports-native.yml | 45 +- .github/workflows/reports.yml | 25 +- .github/workflows/review.yml | 2 +- .github/workflows/tests.yml | 20 +- .github/workflows/wiki-direct.yml | 32 +- .github/workflows/wiki-preview.yml | 11 +- .php-cs-fixer.dist.php | 2 - AGENTS.md | 109 +++-- README.md | 439 ++++-------------- composer.json | 18 +- docs/advanced/consumer-automation.rst | 19 +- docs/api/commands.rst | 12 +- docs/api/composer-integration.rst | 64 +-- docs/commands/agents.rst | 65 +-- docs/commands/code-style.rst | 20 +- docs/commands/codeowners.rst | 12 +- docs/commands/copy-resource.rst | 10 +- docs/commands/dependencies.rst | 26 +- docs/commands/docs.rst | 16 +- docs/commands/funding.rst | 17 +- docs/commands/git-hooks.rst | 20 +- docs/commands/gitattributes.rst | 6 +- docs/commands/gitignore.rst | 10 +- docs/commands/index.rst | 5 +- docs/commands/license.rst | 10 +- docs/commands/metrics.rst | 12 +- docs/commands/phpdoc.rst | 14 +- docs/commands/refactor.rst | 12 +- docs/commands/reports.rst | 12 +- docs/commands/self-update.rst | 16 +- docs/commands/skills.rst | 63 +-- docs/commands/standards.rst | 12 +- docs/commands/sync.rst | 26 +- docs/commands/tests.rst | 28 +- docs/commands/update-composer-json.rst | 8 +- docs/commands/wiki.rst | 12 +- docs/configuration/overriding-defaults.rst | 12 +- docs/configuration/tooling-defaults.rst | 19 +- docs/faq.rst | 70 +-- docs/getting-started/index.rst | 8 +- docs/getting-started/installation.rst | 115 +---- docs/getting-started/quickstart.rst | 85 +--- docs/index.rst | 2 +- docs/internals/architecture.rst | 124 +---- docs/internals/release-publishing.rst | 2 +- docs/links/dependencies.rst | 10 +- docs/links/live-reports.rst | 4 +- docs/running/specialized-commands.rst | 124 ++--- docs/running/unified-command.rst | 4 +- docs/troubleshooting.rst | 29 +- docs/usage/common-workflows.rst | 44 +- docs/usage/dev-tools-2-migration.rst | 189 ++++++++ docs/usage/documentation-workflows.rst | 6 +- docs/usage/github-actions.rst | 39 +- docs/usage/index.rst | 1 + .../usage/migrating-consumer-repositories.rst | 165 +------ docs/usage/syncing-consumer-projects.rst | 136 +----- docs/usage/syncing-packaged-agents.rst | 83 +--- docs/usage/syncing-packaged-skills.rst | 150 +++--- docs/usage/testing-and-coverage.rst | 12 +- resources/docblock/.docheader | 3 +- resources/git-hooks/commit-msg | 25 +- resources/git-hooks/pre-commit | 27 +- resources/github-actions/auto-assign.yml | 2 +- resources/github-actions/label-sync.yml | 2 +- resources/github-actions/reports.yml | 3 +- resources/github-actions/review.yml | 2 +- resources/github-actions/tests.yml | 3 +- resources/github-actions/wiki.yml | 4 +- .../fast-forward-create-agentsmd/LICENSE | 19 + .../fast-forward-create-agentsmd}/SKILL.md | 6 +- .../agents/openai.yaml | 4 + .../references/content-outline.md | 0 .../skills/fast-forward-github-issues/LICENSE | 19 + .../fast-forward-github-issues}/SKILL.md | 6 +- .../agents/openai.yaml | 4 + .../references/architectural-criteria.md | 0 .../references/context.md | 0 .../references/metadata.md | 2 +- .../references/operations.md | 0 .../references/review-checklist.md | 0 .../references/templates.md | 0 .../references/triage.md | 0 .../fast-forward-github-pull-request/LICENSE | 19 + .../SKILL.md | 8 +- .../agents/openai.yaml | 4 + .../references/context-routing.md | 0 .../references/implementation-loop.md | 12 +- .../references/issue-selection.md | 0 .../references/pr-drafting.md | 0 .../references/review-checklist.md | 4 +- .../fast-forward-package-readme/LICENSE | 19 + .../fast-forward-package-readme}/SKILL.md | 6 +- .../agents/openai.yaml | 2 +- .../references/badges.md | 0 .../references/examples.md | 20 +- .../references/update-triggers.md | 0 .../fast-forward-phpdoc-code-style/LICENSE | 19 + .../fast-forward-phpdoc-code-style}/SKILL.md | 12 +- .../agents/openai.yaml | 4 + .../references/anti-patterns.md | 0 .../references/checklist.md | 2 +- .../references/examples.md | 0 .../references/fast-forward-conventions.md | 6 +- .../references/interface-contract-example.md | 0 .../skills/fast-forward-phpunit-tests/LICENSE | 19 + .../fast-forward-phpunit-tests}/SKILL.md | 67 +-- .../agents/openai.yaml | 4 + .../references/generation-checklist.md | 0 .../references/project-discovery.md | 0 .../fast-forward-pull-request-review/LICENSE | 19 + .../SKILL.md | 8 +- .../agents/openai.yaml | 2 +- .../references/review-contract.md | 0 .../references/surface-priorities.md | 2 +- .../references/workflow-action-validation.md | 0 .../skills/fast-forward-sphinx-docs/LICENSE | 19 + .../skills/fast-forward-sphinx-docs}/SKILL.md | 6 +- .../agents/openai.yaml | 4 + .../references/rst-examples.md | 0 .../references/structure.md | 0 .../references/update-triggers.md | 2 +- skills.json | 29 ++ src/CodeOwners/CodeOwnersGenerator.php | 1 + .../Capability/DevToolsCommandProvider.php | 106 ----- src/Composer/Command/ProxyCommand.php | 62 --- src/Composer/DevToolsPluginInterface.php | 93 ---- src/Composer/Json/ComposerJson.php | 5 +- src/Composer/Plugin.php | 179 ------- .../ComposerDependencyAnalyserConfig.php | 1 - src/Config/ECSConfig.php | 1 + src/Config/RectorConfig.php | 1 + src/Console/Command/AgentsCommand.php | 126 ----- src/Console/Command/CodeStyleCommand.php | 17 +- src/Console/Command/DependenciesCommand.php | 10 +- src/Console/Command/DocsCommand.php | 1 + src/Console/Command/FundingCommand.php | 21 +- src/Console/Command/GitAttributesCommand.php | 1 + src/Console/Command/GitHooksCommand.php | 4 +- src/Console/Command/MetricsCommand.php | 1 + src/Console/Command/PhpDocCommand.php | 9 +- src/Console/Command/RuntimeCommand.php | 78 ++++ src/Console/Command/SkillsCommand.php | 158 ------- src/Console/Command/SyncCommand.php | 18 +- src/Console/Command/TestsCommand.php | 1 + .../Command/UpdateComposerJsonCommand.php | 21 +- src/Console/Command/WikiCommand.php | 1 + src/Console/DevTools.php | 4 + src/Console/Logger/OutputFormatLogger.php | 1 + .../Processor/CommandOutputProcessor.php | 1 + .../Output/OutputCapabilityDetector.php | 1 + .../DevToolsServiceProvider.php | 3 - src/Filesystem/Filesystem.php | 1 + src/Funding/FundingYamlCodec.php | 1 + src/Git/GitClient.php | 1 + src/GitAttributes/Writer.php | 1 + src/GitHooks/HookContentRenderer.php | 15 +- src/Path/WorkingProjectPathResolver.php | 1 + .../Coverage/CoverageSummaryLoader.php | 1 + ...isablingProcessEnvironmentConfigurator.php | 1 + src/Project/ProjectCapabilitiesResolver.php | 1 + src/Rector/RemoveEmptyDocBlockRector.php | 1 + src/Resource/FileDiffer.php | 1 + src/Resource/UnifiedDiffer.php | 1 + src/SelfUpdate/ComposerSelfUpdateRunner.php | 6 +- .../ComposerSelfUpdateScopeResolver.php | 1 + src/SelfUpdate/ComposerVersionChecker.php | 1 + src/SelfUpdate/WorkingDirectorySwitcher.php | 1 + src/Sync/PackagedDirectorySynchronizer.php | 240 ---------- src/Sync/SynchronizeResult.php | 147 ------ .../DevToolsCommandProviderTest.php | 240 ---------- tests/Composer/Json/ComposerJsonTest.php | 17 + tests/Composer/PluginTest.php | 183 -------- tests/Config/ECSConfigTest.php | 1 + tests/Config/RectorConfigTest.php | 1 + tests/Console/Command/AgentsCommandTest.php | 205 -------- .../Console/Command/CodeStyleCommandTest.php | 2 +- .../CommandAttributeCompatibilityTest.php | 1 + .../Command/CopyResourceCommandTest.php | 1 + tests/Console/Command/FundingCommandTest.php | 26 +- .../Command/GitAttributesCommandTest.php | 1 + tests/Console/Command/GitHooksCommandTest.php | 6 +- tests/Console/Command/LicenseCommandTest.php | 1 + tests/Console/Command/MetricsCommandTest.php | 1 + tests/Console/Command/PhpDocCommandTest.php | 28 ++ tests/Console/Command/RuntimeCommandTest.php | 128 +++++ tests/Console/Command/SkillsCommandTest.php | 205 -------- tests/Console/Command/SyncCommandTest.php | 8 +- tests/Console/Command/TestsCommandTest.php | 1 + .../Command/UpdateComposerJsonCommandTest.php | 39 +- tests/Console/Command/WikiCommandTest.php | 1 + .../DevToolsCommandLoaderTest.php | 18 +- .../Console/Logger/OutputFormatLoggerTest.php | 1 + .../Output/OutputCapabilityDetectorTest.php | 1 + tests/Distribution/ExternalExecutionTest.php | 193 ++++++++ tests/Distribution/SkillsDistributionTest.php | 90 ++++ tests/Environment/EnvironmentTest.php | 1 + tests/Filesystem/FilesystemTest.php | 1 + .../Command/FixtureDuplicateCommandName.php | 2 +- tests/Funding/ComposerFundingCodecTest.php | 1 + tests/GitAttributes/ReaderTest.php | 1 + tests/GitHooks/HookContentRendererTest.php | 9 +- tests/GitHooks/PackagedHooksTest.php | 258 +++++++++- tests/GitHubActions/ChangelogWorkflowTest.php | 4 +- .../RefreshPreviewPointerActionTest.php | 1 + .../ReportTemplateRuntimeTest.php | 160 +++++++ tests/GitHubActions/ReportsWorkflowTest.php | 44 ++ .../RetryTransientFailuresActionTest.php | 1 + .../GitHubActions/SetupComposerActionTest.php | 328 +++++-------- tests/GitHubActions/WikiWorkflowsTest.php | 23 + tests/GitIgnore/GitIgnoreTest.php | 1 + tests/GitIgnore/ReaderTest.php | 1 + tests/Path/DevToolsPathResolverTest.php | 1 + tests/Path/WorkingProjectPathResolverTest.php | 1 + .../Coverage/CoverageSummaryLoaderTest.php | 1 + tests/Process/ProcessQueueTest.php | 1 + .../ProjectCapabilitiesResolverTest.php | 1 + .../ComposerSelfUpdateRunnerTest.php | 6 +- .../DevToolsServiceProviderTest.php | 1 + .../PackagedDirectorySynchronizerTest.php | 334 ------------- tests/Sync/SynchronizeResultTest.php | 83 ---- 249 files changed, 2775 insertions(+), 4707 deletions(-) delete mode 100644 .agents/skills/create-agentsmd/agents/openai.yaml delete mode 100644 .agents/skills/github-issues/agents/openai.yaml delete mode 100644 .agents/skills/github-pull-request/agents/openai.yaml delete mode 100644 .agents/skills/phpdoc-code-style/agents/openai.yaml delete mode 100644 .agents/skills/phpunit-tests/agents/openai.yaml delete mode 100644 .agents/skills/sphinx-docs/agents/openai.yaml create mode 100644 .changelog/dev-tools-2-external-runtime.md mode change 100755 => 100644 .github/actions/php/setup-composer/dev-tools-runtime-lib.sh create mode 100644 .github/actions/php/setup-composer/resolve-report-template.php create mode 100644 .github/actions/php/setup-phpdoc/action.yml create mode 100644 docs/usage/dev-tools-2-migration.rst create mode 100644 resources/skills/fast-forward-create-agentsmd/LICENSE rename {.agents/skills/create-agentsmd => resources/skills/fast-forward-create-agentsmd}/SKILL.md (92%) create mode 100644 resources/skills/fast-forward-create-agentsmd/agents/openai.yaml rename {.agents/skills/create-agentsmd => resources/skills/fast-forward-create-agentsmd}/references/content-outline.md (100%) create mode 100644 resources/skills/fast-forward-github-issues/LICENSE rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/SKILL.md (96%) create mode 100644 resources/skills/fast-forward-github-issues/agents/openai.yaml rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/architectural-criteria.md (100%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/context.md (100%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/metadata.md (97%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/operations.md (100%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/review-checklist.md (100%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/templates.md (100%) rename {.agents/skills/github-issues => resources/skills/fast-forward-github-issues}/references/triage.md (100%) create mode 100644 resources/skills/fast-forward-github-pull-request/LICENSE rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/SKILL.md (90%) create mode 100644 resources/skills/fast-forward-github-pull-request/agents/openai.yaml rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/references/context-routing.md (100%) rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/references/implementation-loop.md (71%) rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/references/issue-selection.md (100%) rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/references/pr-drafting.md (100%) rename {.agents/skills/github-pull-request => resources/skills/fast-forward-github-pull-request}/references/review-checklist.md (78%) create mode 100644 resources/skills/fast-forward-package-readme/LICENSE rename {.agents/skills/package-readme => resources/skills/fast-forward-package-readme}/SKILL.md (97%) rename {.agents/skills/package-readme => resources/skills/fast-forward-package-readme}/agents/openai.yaml (52%) rename {.agents/skills/package-readme => resources/skills/fast-forward-package-readme}/references/badges.md (100%) rename {.agents/skills/package-readme => resources/skills/fast-forward-package-readme}/references/examples.md (90%) rename {.agents/skills/package-readme => resources/skills/fast-forward-package-readme}/references/update-triggers.md (100%) create mode 100644 resources/skills/fast-forward-phpdoc-code-style/LICENSE rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/SKILL.md (78%) create mode 100644 resources/skills/fast-forward-phpdoc-code-style/agents/openai.yaml rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/references/anti-patterns.md (100%) rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/references/checklist.md (95%) rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/references/examples.md (100%) rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/references/fast-forward-conventions.md (88%) rename {.agents/skills/phpdoc-code-style => resources/skills/fast-forward-phpdoc-code-style}/references/interface-contract-example.md (100%) create mode 100644 resources/skills/fast-forward-phpunit-tests/LICENSE rename {.agents/skills/phpunit-tests => resources/skills/fast-forward-phpunit-tests}/SKILL.md (80%) create mode 100644 resources/skills/fast-forward-phpunit-tests/agents/openai.yaml rename {.agents/skills/phpunit-tests => resources/skills/fast-forward-phpunit-tests}/references/generation-checklist.md (100%) rename {.agents/skills/phpunit-tests => resources/skills/fast-forward-phpunit-tests}/references/project-discovery.md (100%) create mode 100644 resources/skills/fast-forward-pull-request-review/LICENSE rename {.agents/skills/pull-request-review => resources/skills/fast-forward-pull-request-review}/SKILL.md (92%) rename {.agents/skills/pull-request-review => resources/skills/fast-forward-pull-request-review}/agents/openai.yaml (51%) rename {.agents/skills/pull-request-review => resources/skills/fast-forward-pull-request-review}/references/review-contract.md (100%) rename {.agents/skills/pull-request-review => resources/skills/fast-forward-pull-request-review}/references/surface-priorities.md (96%) rename {.agents/skills/pull-request-review => resources/skills/fast-forward-pull-request-review}/references/workflow-action-validation.md (100%) create mode 100644 resources/skills/fast-forward-sphinx-docs/LICENSE rename {.agents/skills/sphinx-docs => resources/skills/fast-forward-sphinx-docs}/SKILL.md (97%) create mode 100644 resources/skills/fast-forward-sphinx-docs/agents/openai.yaml rename {.agents/skills/sphinx-docs => resources/skills/fast-forward-sphinx-docs}/references/rst-examples.md (100%) rename {.agents/skills/sphinx-docs => resources/skills/fast-forward-sphinx-docs}/references/structure.md (100%) rename {.agents/skills/sphinx-docs => resources/skills/fast-forward-sphinx-docs}/references/update-triggers.md (97%) create mode 100644 skills.json delete mode 100644 src/Composer/Capability/DevToolsCommandProvider.php delete mode 100644 src/Composer/Command/ProxyCommand.php delete mode 100644 src/Composer/DevToolsPluginInterface.php delete mode 100644 src/Composer/Plugin.php delete mode 100644 src/Console/Command/AgentsCommand.php create mode 100644 src/Console/Command/RuntimeCommand.php delete mode 100644 src/Console/Command/SkillsCommand.php delete mode 100644 src/Sync/PackagedDirectorySynchronizer.php delete mode 100644 src/Sync/SynchronizeResult.php delete mode 100644 tests/Composer/Capability/DevToolsCommandProviderTest.php delete mode 100644 tests/Composer/PluginTest.php delete mode 100644 tests/Console/Command/AgentsCommandTest.php create mode 100644 tests/Console/Command/RuntimeCommandTest.php delete mode 100644 tests/Console/Command/SkillsCommandTest.php create mode 100644 tests/Distribution/ExternalExecutionTest.php create mode 100644 tests/Distribution/SkillsDistributionTest.php create mode 100644 tests/GitHubActions/ReportTemplateRuntimeTest.php delete mode 100644 tests/Sync/PackagedDirectorySynchronizerTest.php delete mode 100644 tests/Sync/SynchronizeResultTest.php diff --git a/.agents/agents/README.md b/.agents/agents/README.md index 133e6cbd36..511c403853 100644 --- a/.agents/agents/README.md +++ b/.agents/agents/README.md @@ -24,6 +24,7 @@ Each agent file uses: ## Scope -These prompts are packaged for Fast Forward repositories. They define durable -role behavior and delegation boundaries that can be reused in consumer -repositories, while `.agents/skills` remains the procedural source of truth. +These repository prompts define durable role behavior and delegation boundaries. +Portable procedures are published under `resources/skills`; consumers install +those skills through their chosen installer. DevTools does not synchronize these +role prompts into consumer repositories. diff --git a/.agents/agents/agents-maintainer.md b/.agents/agents/agents-maintainer.md index 90ebd5b549..bba00115eb 100644 --- a/.agents/agents/agents-maintainer.md +++ b/.agents/agents/agents-maintainer.md @@ -1,7 +1,7 @@ --- name: agents-maintainer description: Keep repository AGENTS.md guidance aligned with the current Fast Forward DevTools workflows and packaged agent surfaces. -primary-skill: create-agentsmd +primary-skill: fast-forward-create-agentsmd supporting-skills: [] --- @@ -48,7 +48,7 @@ accurate setup, workflow, testing, and contribution instructions. ## Primary Skill -- `create-agentsmd` +- `fast-forward-create-agentsmd` ## Supporting Skills diff --git a/.agents/agents/changelog-maintainer.md b/.agents/agents/changelog-maintainer.md index 0a17cf4d52..84768ea1d7 100644 --- a/.agents/agents/changelog-maintainer.md +++ b/.agents/agents/changelog-maintainer.md @@ -3,7 +3,7 @@ name: changelog-maintainer description: Maintain fragment-based changelog contributions and verified release history through the external Fast Forward Changelog package. primary-skill: fast-forward-changelog supporting-skills: - - github-issues + - fast-forward-github-issues --- # changelog-maintainer diff --git a/.agents/agents/consumer-sync-auditor.md b/.agents/agents/consumer-sync-auditor.md index ab5a65df87..bbf7215c52 100644 --- a/.agents/agents/consumer-sync-auditor.md +++ b/.agents/agents/consumer-sync-auditor.md @@ -1,10 +1,10 @@ --- name: consumer-sync-auditor description: Audit downstream sync and consumer bootstrap impacts for packaged skills, packaged agents, workflows, wiki, and repository defaults. -primary-skill: github-pull-request +primary-skill: fast-forward-github-pull-request supporting-skills: - - package-readme - - sphinx-docs + - fast-forward-package-readme + - fast-forward-sphinx-docs --- # consumer-sync-auditor @@ -28,7 +28,7 @@ wiki/bootstrap assets. ## Use When -- A change touches `resources/`, `.github/workflows/`, `.agents/skills`, +- A change touches `resources/`, `.github/workflows/`, `resources/skills`, `.agents/agents`, `.editorconfig`, wiki automation, `.github/actions/`, or `dev-tools:sync`. - A PR may affect how consumer repositories adopt or refresh DevTools assets. @@ -44,9 +44,9 @@ wiki/bootstrap assets. ## Primary Skill -- `github-pull-request` +- `fast-forward-github-pull-request` ## Supporting Skills -- `package-readme` -- `sphinx-docs` +- `fast-forward-package-readme` +- `fast-forward-sphinx-docs` diff --git a/.agents/agents/docs-writer.md b/.agents/agents/docs-writer.md index 7bc9ebc73a..f46fd49c27 100644 --- a/.agents/agents/docs-writer.md +++ b/.agents/agents/docs-writer.md @@ -1,7 +1,7 @@ --- name: docs-writer description: Create and refresh Sphinx documentation for Fast Forward repositories under docs/. -primary-skill: sphinx-docs +primary-skill: fast-forward-sphinx-docs supporting-skills: [] --- @@ -34,7 +34,7 @@ for Fast Forward repository users and contributors. ## Primary Skill -- `sphinx-docs` +- `fast-forward-sphinx-docs` ## Supporting Skills diff --git a/.agents/agents/issue-editor.md b/.agents/agents/issue-editor.md index c9568e9d40..bf12d89b86 100644 --- a/.agents/agents/issue-editor.md +++ b/.agents/agents/issue-editor.md @@ -1,7 +1,7 @@ --- name: issue-editor description: Turn short Fast Forward requests into implementation-ready GitHub issues and handle issue lifecycle updates. -primary-skill: github-issues +primary-skill: fast-forward-github-issues supporting-skills: [] --- @@ -43,7 +43,7 @@ issues for the current repository. ## Primary Skill -- `github-issues` +- `fast-forward-github-issues` ## Supporting Skills diff --git a/.agents/agents/issue-implementer.md b/.agents/agents/issue-implementer.md index d1eee51a7a..16242d8bee 100644 --- a/.agents/agents/issue-implementer.md +++ b/.agents/agents/issue-implementer.md @@ -1,12 +1,12 @@ --- name: issue-implementer description: Execute a ready repository issue from branch creation through verification and pull request publication. -primary-skill: github-pull-request +primary-skill: fast-forward-github-pull-request supporting-skills: - - phpunit-tests - - package-readme - - sphinx-docs - - phpdoc-code-style + - fast-forward-phpunit-tests + - fast-forward-package-readme + - fast-forward-sphinx-docs + - fast-forward-phpdoc-code-style --- # issue-implementer @@ -47,11 +47,11 @@ pull request. ## Primary Skill -- `github-pull-request` +- `fast-forward-github-pull-request` ## Supporting Skills -- `phpunit-tests` -- `package-readme` -- `sphinx-docs` -- `phpdoc-code-style` +- `fast-forward-phpunit-tests` +- `fast-forward-package-readme` +- `fast-forward-sphinx-docs` +- `fast-forward-phpdoc-code-style` diff --git a/.agents/agents/php-style-curator.md b/.agents/agents/php-style-curator.md index 376304f9e4..3397a9f628 100644 --- a/.agents/agents/php-style-curator.md +++ b/.agents/agents/php-style-curator.md @@ -1,7 +1,7 @@ --- name: php-style-curator description: Normalize Fast Forward PHP style, PHPDoc, headers, and wording without changing behavior. -primary-skill: phpdoc-code-style +primary-skill: fast-forward-phpdoc-code-style supporting-skills: [] --- @@ -32,7 +32,7 @@ conventions without changing runtime behavior. ## Primary Skill -- `phpdoc-code-style` +- `fast-forward-phpdoc-code-style` ## Supporting Skills diff --git a/.agents/agents/quality-pipeline-auditor.md b/.agents/agents/quality-pipeline-auditor.md index 126bba6ae9..eaed6c433a 100644 --- a/.agents/agents/quality-pipeline-auditor.md +++ b/.agents/agents/quality-pipeline-auditor.md @@ -1,11 +1,11 @@ --- name: quality-pipeline-auditor description: Evaluate cross-tool impacts across tests, style, PHPDoc, docs, reports, and dependency analysis. -primary-skill: github-pull-request +primary-skill: fast-forward-github-pull-request supporting-skills: - - phpunit-tests - - phpdoc-code-style - - sphinx-docs + - fast-forward-phpunit-tests + - fast-forward-phpdoc-code-style + - fast-forward-sphinx-docs --- # quality-pipeline-auditor @@ -41,10 +41,10 @@ single command in isolation. ## Primary Skill -- `github-pull-request` +- `fast-forward-github-pull-request` ## Supporting Skills -- `phpunit-tests` -- `phpdoc-code-style` -- `sphinx-docs` +- `fast-forward-phpunit-tests` +- `fast-forward-phpdoc-code-style` +- `fast-forward-sphinx-docs` diff --git a/.agents/agents/readme-maintainer.md b/.agents/agents/readme-maintainer.md index d659180567..4f7ca89035 100644 --- a/.agents/agents/readme-maintainer.md +++ b/.agents/agents/readme-maintainer.md @@ -1,7 +1,7 @@ --- name: readme-maintainer description: Keep the repository README aligned with commands, onboarding, links, badges, and public package guidance. -primary-skill: package-readme +primary-skill: fast-forward-package-readme supporting-skills: [] --- @@ -34,7 +34,7 @@ discovery, links, badges, and contributor orientation. ## Primary Skill -- `package-readme` +- `fast-forward-package-readme` ## Supporting Skills diff --git a/.agents/agents/review-guardian.md b/.agents/agents/review-guardian.md index dfca28e403..2a53e88122 100644 --- a/.agents/agents/review-guardian.md +++ b/.agents/agents/review-guardian.md @@ -1,11 +1,11 @@ --- name: review-guardian description: Review Fast Forward pull requests with a rigorous, findings-first contract before or during human review. -primary-skill: pull-request-review +primary-skill: fast-forward-pull-request-review supporting-skills: - - phpunit-tests - - sphinx-docs - - package-readme + - fast-forward-phpunit-tests + - fast-forward-sphinx-docs + - fast-forward-package-readme --- # review-guardian @@ -57,10 +57,10 @@ and generated-output drift before human review time is spent. ## Primary Skill -- `pull-request-review` +- `fast-forward-pull-request-review` ## Supporting Skills -- `phpunit-tests` -- `sphinx-docs` -- `package-readme` +- `fast-forward-phpunit-tests` +- `fast-forward-sphinx-docs` +- `fast-forward-package-readme` diff --git a/.agents/agents/test-guardian.md b/.agents/agents/test-guardian.md index 69f0dd2ff9..822380a45a 100644 --- a/.agents/agents/test-guardian.md +++ b/.agents/agents/test-guardian.md @@ -1,7 +1,7 @@ --- name: test-guardian description: Extend and repair PHPUnit and Prophecy coverage in Fast Forward repositories. -primary-skill: phpunit-tests +primary-skill: fast-forward-phpunit-tests supporting-skills: [] --- @@ -34,7 +34,7 @@ existing suite conventions. ## Primary Skill -- `phpunit-tests` +- `fast-forward-phpunit-tests` ## Supporting Skills diff --git a/.agents/skills/create-agentsmd/agents/openai.yaml b/.agents/skills/create-agentsmd/agents/openai.yaml deleted file mode 100644 index 34ab721fd8..0000000000 --- a/.agents/skills/create-agentsmd/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward AGENTS.md" - short_description: "Create AGENTS.md files for repositories" - default_prompt: "Use $create-agentsmd to analyze this repository and write or refresh a focused AGENTS.md file at the project root." diff --git a/.agents/skills/github-issues/agents/openai.yaml b/.agents/skills/github-issues/agents/openai.yaml deleted file mode 100644 index a0729ece4f..0000000000 --- a/.agents/skills/github-issues/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward GitHub Issues" - short_description: "Draft and manage Fast Forward GitHub issues with existing repository metadata" - default_prompt: "Use $github-issues to draft, create, or update this Fast Forward GitHub issue and apply the most appropriate existing repository metadata." diff --git a/.agents/skills/github-pull-request/agents/openai.yaml b/.agents/skills/github-pull-request/agents/openai.yaml deleted file mode 100644 index fe3fbe0871..0000000000 --- a/.agents/skills/github-pull-request/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward GitHub Pull Request" - short_description: "Implement ready issues through branch and PR workflow" - default_prompt: "Use $github-pull-request to implement this ready Fast Forward issue on an isolated branch and open or update the matching PR." diff --git a/.agents/skills/phpdoc-code-style/agents/openai.yaml b/.agents/skills/phpdoc-code-style/agents/openai.yaml deleted file mode 100644 index 53f156c776..0000000000 --- a/.agents/skills/phpdoc-code-style/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward PHPDoc Code Style" - short_description: "Format PHP and PHPDoc for Fast Forward packages" - default_prompt: "Use $phpdoc-code-style to normalize this Fast Forward PHP code and write conservative English PHPDoc that matches repository conventions." diff --git a/.agents/skills/phpunit-tests/agents/openai.yaml b/.agents/skills/phpunit-tests/agents/openai.yaml deleted file mode 100644 index 84fb1cfbbc..0000000000 --- a/.agents/skills/phpunit-tests/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward PHPUnit Tests" - short_description: "Generate PHPUnit tests using local Prophecy patterns" - default_prompt: "Use $phpunit-tests to create or update PHPUnit tests for this PHP codebase using local conventions and Prophecy when applicable." diff --git a/.agents/skills/sphinx-docs/agents/openai.yaml b/.agents/skills/sphinx-docs/agents/openai.yaml deleted file mode 100644 index 5dcedc87c8..0000000000 --- a/.agents/skills/sphinx-docs/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Fast Forward Sphinx Docs" - short_description: "Generate Sphinx docs for Fast Forward packages" - default_prompt: "Use $sphinx-docs to generate or revise Sphinx documentation for this Fast Forward PHP package." diff --git a/.changelog/dev-tools-2-external-runtime.md b/.changelog/dev-tools-2-external-runtime.md new file mode 100644 index 0000000000..08f7a82eed --- /dev/null +++ b/.changelog/dev-tools-2-external-runtime.md @@ -0,0 +1,12 @@ +--- +category: changed +type: major +--- + +Runs DevTools as a standalone CLI through an isolated CPX installation, without +requiring consumers to add DevTools to their Composer development dependencies. +Removes Composer plugin command registration, automatic install/update sync and +the skills/agents symlink commands. Exports namespaced fast-forward-* skills in +resources/skills for independent LLM Skills installation; repository metadata, +workflow defaults and Git hooks retain explicit dev-tools:sync. Documents the +version 2 migration and gradual transfer of responsibilities to released owners. diff --git a/.gitattributes b/.gitattributes index 1e59f6fe42..763208aca2 100755 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ * text=auto +/.agents/ export-ignore /.github/ export-ignore /.vscode/ export-ignore /docs/ export-ignore @@ -14,3 +15,4 @@ /README.md export-ignore /SECURITY.md export-ignore /SUPPORT.md export-ignore +/skills.json export-ignore diff --git a/.github/actions/php/setup-composer/action.yml b/.github/actions/php/setup-composer/action.yml index a84857a77b..97196bafae 100644 --- a/.github/actions/php/setup-composer/action.yml +++ b/.github/actions/php/setup-composer/action.yml @@ -30,9 +30,13 @@ inputs: required: false default: '' dev-tools-source-directory: - description: Checked-out DevTools workflow source used to resolve the fallback runtime when the consumer does not install DevTools locally. + description: Checked-out DevTools action source directory (helpers only). required: false default: .dev-tools-actions + dev-tools-version: + description: Published DevTools version executed in an isolated CPX environment for consumers. + required: false + default: '2.0.0' outputs: dev-tools-binary: @@ -42,7 +46,7 @@ outputs: description: Absolute path to the runtime autoload file used by packaged workflow helpers. value: ${{ steps.expose-dev-tools.outputs.autoload }} dev-tools-source: - description: Runtime source selected for this job, either `local` or `workflow`. + description: Runtime source selected for this job, either `local` (DevTools self checkout) or `cpx`. value: ${{ steps.expose-dev-tools.outputs.source }} runs: @@ -54,6 +58,7 @@ runs: php-version: ${{ inputs.php-version }} extensions: ${{ inputs.extensions }} coverage: ${{ inputs.coverage }} + tools: cpx - name: Mark workspace as safe for git shell: bash @@ -97,21 +102,10 @@ runs: INPUT_DEV_TOOLS_SOURCE_DIRECTORY: ${{ inputs.dev-tools-source-directory }} run: ${{ github.action_path }}/detect-dev-tools-runtime.sh - - name: Install fallback DevTools runtime - if: steps.resolve-dev-tools.outputs.needs-fallback == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - env: - COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"} }' - COMPOSER_CACHE_DIR: ${{ inputs.cache-dir }} - with: - working-directory: ${{ inputs.dev-tools-source-directory }} - composer-options: --prefer-dist --no-plugins --no-scripts - require-lock-file: 'false' - custom-cache-suffix: dev-tools-runtime - - name: Expose DevTools runtime id: expose-dev-tools shell: bash env: INPUT_DEV_TOOLS_SOURCE_DIRECTORY: ${{ inputs.dev-tools-source-directory }} + INPUT_DEV_TOOLS_VERSION: ${{ inputs.dev-tools-version }} run: ${{ github.action_path }}/expose-dev-tools-runtime.sh diff --git a/.github/actions/php/setup-composer/detect-dev-tools-runtime.sh b/.github/actions/php/setup-composer/detect-dev-tools-runtime.sh index 13666f1e31..7d14f50409 100755 --- a/.github/actions/php/setup-composer/detect-dev-tools-runtime.sh +++ b/.github/actions/php/setup-composer/detect-dev-tools-runtime.sh @@ -5,16 +5,8 @@ source "$(dirname "$0")/dev-tools-runtime-lib.sh" resolve_dev_tools_runtime -needs_fallback='false' - -if runtime_requires_workflow_fallback; then - needs_fallback='true' -fi - { printf 'source=%s\n' "${DEV_TOOLS_RUNTIME_SOURCE}" - printf 'needs-fallback=%s\n' "${needs_fallback}" printf 'binary=%s\n' "${DEV_TOOLS_RUNTIME_BINARY}" printf 'autoload=%s\n' "${DEV_TOOLS_RUNTIME_AUTOLOAD}" - printf 'source-directory=%s\n' "${DEV_TOOLS_SOURCE_DIRECTORY}" } >> "${GITHUB_OUTPUT}" diff --git a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh old mode 100755 new mode 100644 index 3a137fdc78..da44f980e5 --- a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh +++ b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh @@ -1,94 +1,51 @@ #!/usr/bin/env bash -resolve_dev_tools_workspace_path() { - local input_path="${1:-.}" - - if [[ "${input_path}" = /* ]]; then - printf '%s\n' "${input_path}" - - return - fi - - printf '%s/%s\n' "$(pwd)" "${input_path#./}" -} - workspace_is_dev_tools_repository() { - local workspace_root="${1:?Workspace root is required}" - local composer_json="${workspace_root}/composer.json" - - if [ ! -f "${composer_json}" ]; then - return 1 - fi - - php -r ' - $composer = json_decode((string) file_get_contents($argv[1]), true); - - if (! is_array($composer)) { - exit(1); - } - - exit(($composer["name"] ?? null) === "fast-forward/dev-tools" ? 0 : 1); - ' "${composer_json}" + [ -f composer.json ] || return 1 + php -r '$manifest = json_decode(file_get_contents("composer.json"), true, 512, JSON_THROW_ON_ERROR); exit(($manifest["name"] ?? null) === "fast-forward/dev-tools" ? 0 : 1);' } resolve_dev_tools_runtime() { - local source_directory_input="${INPUT_DEV_TOOLS_SOURCE_DIRECTORY:-.dev-tools-actions}" - - DEV_TOOLS_WORKSPACE_ROOT="$(pwd)" - DEV_TOOLS_SOURCE_DIRECTORY="$(resolve_dev_tools_workspace_path "${source_directory_input}")" - DEV_TOOLS_LOCAL_AUTOLOAD="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/autoload.php" - DEV_TOOLS_LOCAL_INSTALLED_BINARY="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/bin/dev-tools" - DEV_TOOLS_LOCAL_INSTALLED_PACKAGE_ROOT="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/fast-forward/dev-tools" - DEV_TOOLS_LOCAL_REPOSITORY_BINARY="${DEV_TOOLS_WORKSPACE_ROOT}/bin/dev-tools" - - if [ -x "${DEV_TOOLS_LOCAL_INSTALLED_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ] && workspace_is_dev_tools_repository "${DEV_TOOLS_LOCAL_INSTALLED_PACKAGE_ROOT}"; then - DEV_TOOLS_RUNTIME_SOURCE='local' - DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_INSTALLED_BINARY}" - DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_LOCAL_AUTOLOAD}" + DEV_TOOLS_RUNTIME_SOURCE='cpx' + DEV_TOOLS_RUNTIME_BINARY='' + DEV_TOOLS_RUNTIME_AUTOLOAD='' - return 0 - fi - - if [ -x "${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ] && workspace_is_dev_tools_repository "${DEV_TOOLS_WORKSPACE_ROOT}"; then + if workspace_is_dev_tools_repository; then DEV_TOOLS_RUNTIME_SOURCE='local' - DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" - DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_LOCAL_AUTOLOAD}" - - return 0 - fi - - if [ ! -d "${DEV_TOOLS_SOURCE_DIRECTORY}" ]; then - echo "The DevTools workflow source directory was not found: ${DEV_TOOLS_SOURCE_DIRECTORY}" >&2 - - return 1 + DEV_TOOLS_RUNTIME_BINARY="$(pwd)/bin/dev-tools" + DEV_TOOLS_RUNTIME_AUTOLOAD="$(pwd)/vendor/autoload.php" fi +} - if ! workspace_is_dev_tools_repository "${DEV_TOOLS_SOURCE_DIRECTORY}"; then - echo "The DevTools workflow source directory does not point to the fast-forward/dev-tools package: ${DEV_TOOLS_SOURCE_DIRECTORY}" >&2 - echo "Checkout the full php-fast-forward/dev-tools source into ${source_directory_input} before using this action." >&2 - +resolve_cpx_runtime() { + local metadata paths version + version="${INPUT_DEV_TOOLS_VERSION:-2.0.0}" + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.+-]+)?$ ]]; then + echo 'dev-tools-version must be an exact published version, such as 2.0.0.' >&2 return 1 fi - - DEV_TOOLS_RUNTIME_SOURCE='workflow' - DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_SOURCE_DIRECTORY}/bin/dev-tools" - DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_SOURCE_DIRECTORY}/vendor/autoload.php" -} - -runtime_requires_workflow_fallback() { - [ "${DEV_TOOLS_RUNTIME_SOURCE}" = 'workflow' ] + metadata="$(cpx --skip-local "fast-forward/dev-tools:${version}" runtime --json --no-interaction --no-ansi)" + paths="$(printf '%s' "${metadata}" | php -r ' + $runtime = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR); + foreach (["binary", "autoload"] as $key) { + $path = $runtime[$key] ?? null; + if (!is_string($path) || !str_starts_with($path, "/") || preg_match("/[\\x00-\\x1f\\x7f]/", $path)) { + throw new RuntimeException("Invalid DevTools runtime path: " . $key); + } + echo $path, PHP_EOL; + } + ')" + DEV_TOOLS_RUNTIME_BINARY="${paths%%$'\n'*}" + DEV_TOOLS_RUNTIME_AUTOLOAD="${paths#*$'\n'}" } ensure_resolved_runtime_is_available() { if [ ! -x "${DEV_TOOLS_RUNTIME_BINARY}" ]; then echo "Resolved DevTools binary is not executable: ${DEV_TOOLS_RUNTIME_BINARY}" >&2 - return 1 fi - if [ ! -f "${DEV_TOOLS_RUNTIME_AUTOLOAD}" ]; then echo "Resolved DevTools autoload file was not found: ${DEV_TOOLS_RUNTIME_AUTOLOAD}" >&2 - return 1 fi } diff --git a/.github/actions/php/setup-composer/expose-dev-tools-runtime.sh b/.github/actions/php/setup-composer/expose-dev-tools-runtime.sh index 164e26c405..4b33661dbf 100755 --- a/.github/actions/php/setup-composer/expose-dev-tools-runtime.sh +++ b/.github/actions/php/setup-composer/expose-dev-tools-runtime.sh @@ -4,6 +4,9 @@ set -euo pipefail source "$(dirname "$0")/dev-tools-runtime-lib.sh" resolve_dev_tools_runtime +if [ "${DEV_TOOLS_RUNTIME_SOURCE}" = 'cpx' ]; then + resolve_cpx_runtime +fi ensure_resolved_runtime_is_available runtime_directory="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/dev-tools-runtime/bin" diff --git a/.github/actions/php/setup-composer/resolve-report-template.php b/.github/actions/php/setup-composer/resolve-report-template.php new file mode 100644 index 0000000000..068757e67a --- /dev/null +++ b/.github/actions/php/setup-composer/resolve-report-template.php @@ -0,0 +1,54 @@ + $template, + 'DEV_TOOLS_REPORTS_TEMPLATE_VERSION' => $version, + 'DEV_TOOLS_REPORTS_TEMPLATE_REFERENCE' => $selected['reference'] ?? '', +]; +$lines = []; +foreach ($environment as $key => $value) { + if (!is_string($value) || preg_match('/[\x00-\x1f\x7f]/', $value)) { + throw new RuntimeException('Invalid documentation template metadata: ' . $key); + } + $lines[] = $key . '=' . $value; +} +if (file_put_contents(getenv('GITHUB_ENV'), implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND) === false) { + throw new RuntimeException('Cannot export selected documentation template metadata.'); +} +echo json_encode($environment, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) . PHP_EOL; diff --git a/.github/actions/php/setup-phpdoc/action.yml b/.github/actions/php/setup-phpdoc/action.yml new file mode 100644 index 0000000000..5e80a22e43 --- /dev/null +++ b/.github/actions/php/setup-phpdoc/action.yml @@ -0,0 +1,25 @@ +name: Setup signed phpDocumentor +description: Install and verify the standalone documentation executable without enabling consumer Composer plugins. + +runs: + using: composite + steps: + - name: Install signed documentation generator in isolation + shell: bash + run: | + command -v gpg + mkdir -p "$RUNNER_TEMP/phpdoc-tool" vendor/bin + php <<'PHP' + ['phpdocumentor/shim' => '^3.9'], + 'config' => ['allow-plugins' => ['phpdocumentor/shim' => true]], + ]; + $json = json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + if (file_put_contents(getenv('RUNNER_TEMP') . '/phpdoc-tool/composer.json', $json) === false) { + throw new RuntimeException('Cannot write the documentation tool manifest.'); + } + PHP + COMPOSER_HOME="$RUNNER_TEMP/phpdoc-home" composer install --working-dir="$RUNNER_TEMP/phpdoc-tool" --no-scripts --no-interaction --prefer-dist --no-progress 2>&1 | tee "$RUNNER_TEMP/phpdoc-install.log" + grep -F 'PHAR signature successfully verified' "$RUNNER_TEMP/phpdoc-install.log" + install -m 0755 "$RUNNER_TEMP/phpdoc-tool/vendor/bin/phpdoc" vendor/bin/phpdoc diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 93d74635cf..00561a284a 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -81,7 +81,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -101,7 +101,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -133,7 +133,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml index 34fc409625..30ef2a6622 100644 --- a/.github/workflows/label-sync.yml +++ b/.github/workflows/label-sync.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions diff --git a/.github/workflows/reports-native.yml b/.github/workflows/reports-native.yml index 4350b2c810..9259b8e86a 100644 --- a/.github/workflows/reports-native.yml +++ b/.github/workflows/reports-native.yml @@ -2,6 +2,12 @@ name: Native GitHub Pages Reports on: workflow_call: + inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' permissions: contents: read @@ -26,7 +32,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: php-fast-forward/dev-tools - ref: main + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions persist-credentials: false - name: Resolve PHP version @@ -35,45 +41,16 @@ jobs: - name: Setup PHP and plugin-free dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version }} php-version: ${{ steps.php.outputs.php-version }} extensions: pcov, pcntl, dom, intl, zip coverage: pcov root-version: dev-main install-options: --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts - name: Install signed documentation generator in isolation - shell: bash - run: | - command -v gpg - mkdir -p "$RUNNER_TEMP/phpdoc-tool" - php <<'PHP' - ['phpdocumentor/shim' => '^3.9'], - 'config' => ['allow-plugins' => ['phpdocumentor/shim' => true]], - ]; - $json = json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); - if (file_put_contents(getenv('RUNNER_TEMP') . '/phpdoc-tool/composer.json', $json) === false) { - throw new RuntimeException('Cannot write the documentation tool manifest.'); - } - PHP - COMPOSER_HOME="$RUNNER_TEMP/phpdoc-home" composer install --working-dir="$RUNNER_TEMP/phpdoc-tool" --no-scripts --no-interaction --prefer-dist --no-progress 2>&1 | tee "$RUNNER_TEMP/phpdoc-install.log" - grep -F 'PHAR signature successfully verified' "$RUNNER_TEMP/phpdoc-install.log" - install -m 0755 "$RUNNER_TEMP/phpdoc-tool/vendor/bin/phpdoc" vendor/bin/phpdoc + uses: ./.dev-tools-actions/.github/actions/php/setup-phpdoc - name: Verify selected template version - run: | - php <<'PHP' - $version, - 'template_reference' => Composer\InstalledVersions::getReference($name), - ], JSON_THROW_ON_ERROR) . PHP_EOL; - PHP + run: php .dev-tools-actions/.github/actions/php/setup-composer/resolve-report-template.php - name: Generate documentation, coverage and metrics env: COMPOSER_ROOT_VERSION: dev-main @@ -89,7 +66,7 @@ jobs: run: | php <<'PHP' 'dash_sha', 'fast-forward-logo-dark.svg' => 'logo_sha'] as $name => $key) { $generated = '.dev-tools/reports/images/' . $name; diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 5386c7d154..3e1d48b31e 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -3,6 +3,11 @@ name: Generate Reports and Deploy to GitHub Pages on: workflow_call: inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' cleanup-previews: description: Remove stale pull request previews without publishing production reports. required: false @@ -10,6 +15,11 @@ on: default: false workflow_dispatch: inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' cleanup-previews: description: Remove stale pull request previews without publishing production reports. required: false @@ -46,7 +56,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -80,12 +90,13 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version || '2.0.0' }} php-version: ${{ needs.resolve_php.outputs.php-version }} extensions: pcov, pcntl coverage: pcov @@ -189,7 +200,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -244,7 +255,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -353,7 +364,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -392,7 +403,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -430,7 +441,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 2c0a4b7b3f..0fb9f8a4e8 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'v2.0.0' }} path: .dev-tools-actions sparse-checkout: | .github/actions diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e207cbd4a7..e1f553ad18 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,6 +3,11 @@ name: Run PHPUnit Tests on: workflow_call: inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' min-coverage: description: Minimum line coverage percentage enforced by dev-tools tests. required: false @@ -20,6 +25,11 @@ on: default: false workflow_dispatch: inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' min-coverage: description: Minimum line coverage percentage enforced by dev-tools tests. required: false @@ -74,7 +84,7 @@ jobs: with: persist-credentials: false repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -198,12 +208,13 @@ jobs: with: persist-credentials: false repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version || '2.0.0' }} php-version: ${{ matrix.php-version }} extensions: pcov, pcntl coverage: pcov @@ -245,12 +256,13 @@ jobs: with: persist-credentials: false repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version || '2.0.0' }} php-version: ${{ needs.resolve_php.outputs.php-version }} root-version: ${{ env.TESTS_ROOT_VERSION }} install-options: --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts @@ -290,7 +302,7 @@ jobs: with: persist-credentials: false repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions diff --git a/.github/workflows/wiki-direct.yml b/.github/workflows/wiki-direct.yml index fdde4e2a99..6a96fffb6a 100644 --- a/.github/workflows/wiki-direct.yml +++ b/.github/workflows/wiki-direct.yml @@ -2,6 +2,12 @@ name: Publish Wiki Directly on: workflow_call: + inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' permissions: contents: read @@ -20,7 +26,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: php-fast-forward/dev-tools - ref: d5a3b7eec9896f9ea50974ff09b23c32887acbc0 + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions persist-credentials: false - name: Resolve PHP version @@ -29,36 +35,22 @@ jobs: - name: Setup PHP and dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version }} php-version: ${{ steps.php.outputs.php-version }} root-version: dev-main install-options: --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts - - name: Install the pinned Markdown renderer - working-directory: .dev-tools-actions - env: - COMPOSER_ROOT_VERSION: dev-main - run: composer install --prefer-dist --no-progress --no-interaction --no-scripts + - name: Install signed documentation generator in isolation + uses: ./.dev-tools-actions/.github/actions/php/setup-phpdoc - name: Generate API pages id: generate run: | - has_api=$(php -r 'require ".dev-tools-actions/vendor/autoload.php"; echo FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface::class)->resolve()->canGenerateApiDocumentation() ? "true" : "false";') + has_api=$(php -r 'require getenv("DEV_TOOLS_AUTOLOAD"); echo FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface::class)->resolve()->canGenerateApiDocumentation() ? "true" : "false";') echo "generated=${has_api}" >> "$GITHUB_OUTPUT" if [ "$has_api" = false ]; then echo 'No autoloaded PHP API directories; skip Wiki generation and publication.' exit 0 fi - mkdir -p vendor/bin vendor/saggre/phpdocumentor-markdown/themes/markdown - cp .dev-tools-actions/vendor/bin/phpdoc vendor/bin/phpdoc - chmod +x vendor/bin/phpdoc - rsync -a --delete .dev-tools-actions/vendor/saggre/phpdocumentor-markdown/themes/markdown/ vendor/saggre/phpdocumentor-markdown/themes/markdown/ - if [ "$GITHUB_REPOSITORY" = php-fast-forward/dev-tools ]; then - ./bin/dev-tools wiki --target=.dev-tools/wiki-generated --cache-dir="${RUNNER_TEMP}/wiki-phpdoc-cache" - else - export DEV_TOOLS_BINARY="$GITHUB_WORKSPACE/.dev-tools-actions/bin/dev-tools" - export DEV_TOOLS_AUTOLOAD="$GITHUB_WORKSPACE/.dev-tools-actions/vendor/autoload.php" - export DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD="$DEV_TOOLS_AUTOLOAD" - export DEV_TOOLS_RUNTIME_SOURCE=workflow - ./.dev-tools-actions/bin/dev-tools wiki --target=.dev-tools/wiki-generated --cache-dir="${RUNNER_TEMP}/wiki-phpdoc-cache" - fi + dev-tools wiki --target=.dev-tools/wiki-generated --cache-dir="${RUNNER_TEMP}/wiki-phpdoc-cache" test -s .dev-tools/wiki-generated/Home.md - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: steps.generate.outputs.generated == 'true' diff --git a/.github/workflows/wiki-preview.yml b/.github/workflows/wiki-preview.yml index bac8906b57..cc64b620f3 100644 --- a/.github/workflows/wiki-preview.yml +++ b/.github/workflows/wiki-preview.yml @@ -2,6 +2,12 @@ name: Update Wiki Preview on: workflow_call: + inputs: + dev-tools-version: + description: Published isolated DevTools runtime version for consumer repositories. + required: false + type: string + default: '2.0.0' permissions: contents: read @@ -23,7 +29,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions sparse-checkout: | .github/actions @@ -58,7 +64,7 @@ jobs: uses: actions/checkout@v7 with: repository: php-fast-forward/dev-tools - ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || 'main' }} + ref: ${{ github.repository == 'php-fast-forward/dev-tools' && (github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha) || format('v{0}', inputs.dev-tools-version || '2.0.0') }} path: .dev-tools-actions - name: Detect wiki repository @@ -74,6 +80,7 @@ jobs: if: steps.detect_wiki.outputs.exists == 'true' uses: ./.dev-tools-actions/.github/actions/php/setup-composer with: + dev-tools-version: ${{ inputs.dev-tools-version }} php-version: ${{ needs.resolve_php.outputs.php-version }} root-version: dev-${{ github.event.pull_request.head.ref }} safe-directories: | diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 01a76f7b71..dc1783f75b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,8 +2,6 @@ declare(strict_types=1); -require __DIR__ . '/vendor/autoload.php'; - use FastForward\DevTools\Path\WorkingProjectPathResolver; $rules = [ diff --git a/AGENTS.md b/AGENTS.md index 763b54937e..7b35bbb1c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,12 @@ ## Project Overview -FastForward\DevTools is a Composer plugin that aggregates multiple PHP development tools into a single unified command. It provides automated execution of tests, static analysis, code styling, refactoring, and documentation generation. +FastForward\DevTools is a standalone PHP CLI that aggregates multiple PHP development tools into a single unified command. It provides automated execution of tests, static analysis, code styling, refactoring, and documentation generation. **Key Technologies:** - PHP 8.3+ -- Composer plugin +- Symfony Console CLI with isolated CPX execution - PHPUnit 12.5 (testing) - Rector 2.x (automated refactoring) - Easy Coding Standard (code style) @@ -18,11 +18,12 @@ FastForward\DevTools is a Composer plugin that aggregates multiple PHP developme ## Setup Commands ```bash -# Install as development dependency -composer require --dev fast-forward/dev-tools:dev-main - -# Install dependencies +# Develop this repository composer install +php bin/dev-tools list + +# Consume a verified published version without require-dev +cpx --skip-local fast-forward/dev-tools:2.0.0 tests ``` ## Development Workflow @@ -42,23 +43,21 @@ composer dev-tools phpdoc # Check and fix PHPDoc comments composer dev-tools docs # Generate HTML API documentation composer dev-tools wiki # Generate Markdown documentation for wiki composer dev-tools reports # Generate docs frontpage and reports -composer dev-tools agents # Sync packaged project agents into .agents/agents -composer dev-tools:sync # Sync managed repository assets and packaged agent surfaces +php bin/dev-tools dev-tools:sync # Explicitly sync managed metadata, workflows, defaults and hooks ``` **Notable Specialized Commands:** -- `composer skills`: Synchronize packaged skills into consumer `.agents/skills` -- `composer funding`: Sync funding metadata between `composer.json` and `.github/FUNDING.yml` -- `composer codeowners`: Generate `.github/CODEOWNERS` from repository metadata -- `composer gitattributes`: Manage export-ignore rules in `.gitattributes` -- `composer gitignore`: Synchronize managed `.gitignore` content -- `composer license`: Generate or refresh repository `LICENSE` files -- `composer dependencies`: Run dependency analysis workflows -- `composer metrics`: Generate PhpMetrics reports and related artifacts -- `composer update-composer-json`: Normalize managed `composer.json` settings +- `php bin/dev-tools funding`: Sync funding metadata between `composer.json` and `.github/FUNDING.yml` +- `php bin/dev-tools codeowners`: Generate `.github/CODEOWNERS` from repository metadata +- `php bin/dev-tools gitattributes`: Manage export-ignore rules in `.gitattributes` +- `php bin/dev-tools gitignore`: Synchronize managed `.gitignore` content +- `php bin/dev-tools license`: Generate or refresh repository `LICENSE` files +- `php bin/dev-tools dependencies`: Run dependency analysis workflows +- `php bin/dev-tools metrics`: Generate PhpMetrics reports and related artifacts +- `php bin/dev-tools update-composer-json`: Normalize managed `composer.json` settings - `cpx --skip-local fast-forward/changelog `: Run the independent changelog CLI with PHP 8.5, outside the consumer Composer dependency graph -- `composer dev-tools:sync --dry-run|--check|--interactive`: Preview managed-file drift while intentionally skipping wiki, skills, and agents +- `php bin/dev-tools dev-tools:sync --dry-run|--check|--interactive`: Preview managed-file drift while intentionally skipping wiki ## Testing Instructions @@ -67,10 +66,10 @@ composer dev-tools:sync # Sync managed repository assets and packaged a composer dev-tools tests # Run tests matching a pattern -composer dev-tools tests -- --filter=CodeStyle +php bin/dev-tools tests --filter=CodeStyle # Run with coverage report -composer dev-tools tests -- --coverage=.dev-tools/coverage +php bin/dev-tools tests --coverage=.dev-tools/coverage ``` **Test Configuration:** @@ -88,7 +87,7 @@ composer dev-tools tests -- --coverage=.dev-tools/coverage **Creating/Updating Tests:** -- Use skill `phpunit-tests` in `.agents/skills/phpunit-tests/` for creating or updating PHPUnit tests with Prophecy +- Use skill `fast-forward-phpunit-tests` in `resources/skills/fast-forward-phpunit-tests/` for creating or updating PHPUnit tests with Prophecy - Run skill when: creating new test classes, adding test methods, or fixing existing tests ## Code Style @@ -109,13 +108,12 @@ composer dev-tools tests -- --coverage=.dev-tools/coverage - Commands: `src/Console/Command/` - PHPUnit events: `src/PhpUnit/Event/` - 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. + legacy surfaces already awaiting decoupling. Do not restore a Composer plugin, + command provider, or automatic install/update sync hooks. - 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. @@ -147,14 +145,14 @@ composer dev-tools tests -- --coverage=.dev-tools/coverage - All classes require docblocks with `@copyright` and `@license` - Use RFC 2119 keywords (MUST, SHOULD, MAY, etc.) - PHPDoc checked via dev-tools phpdoc command -- Use skill `phpdoc-code-style` in `.agents/skills/phpdoc-code-style/` for PHPDoc cleanup and repository-specific PHP formatting +- Use skill `fast-forward-phpdoc-code-style` in `resources/skills/fast-forward-phpdoc-code-style/` for PHPDoc cleanup and repository-specific PHP formatting ## Build and Deployment -This repository ships a Composer plugin plus the local `bin/dev-tools` binary, +This repository ships a standalone `bin/dev-tools` binary, so there is no separate frontend or asset build step. The package is published -to Packagist, while consumer repositories adopt workflows, packaged skills, and -other managed assets through `composer dev-tools:sync`. +to Packagist. Consumers adopt metadata and workflow defaults through explicit +`dev-tools:sync`; LLM Skills installs procedural packages independently. Release and publishing behavior is driven primarily through `.github/workflows/tests.yml`, `changelog.yml`, `reports.yml`, `wiki.yml`, @@ -164,8 +162,8 @@ Release and publishing behavior is driven primarily through `resources/github-actions/`. Contract-specific templates in `resources/github-actions-optional/` require explicit adoption and are not installed by `dev-tools:sync`; read each companion guide before copying one -into a consumer repository. Packaged skills live under `.agents/skills/` -alongside mirrored project-agent prompts under `.agents/agents/`. +into a consumer repository. Packaged skills live under `resources/skills/fast-forward-*`. LLM Skills owns +installation; `.agents/agents/` prompts remain local repository guidance. The active `wiki.yml` entry and packaged wrapper call `wiki-direct.yml` for read-only generation on pull requests and direct publication from `main`. @@ -193,8 +191,9 @@ history-writing or fragment-consuming operations only in disposable checkouts. **Package Details:** -- Type: `composer-plugin` -- Class: `FastForward\DevTools\Composer\Plugin` +- Type: `library` +- Consumer execution: `cpx --skip-local fast-forward/dev-tools:` +- Runtime discovery: `runtime --json --no-ansi --no-interaction` - Minimum stability: `stable` ## Pull Request Guidelines @@ -228,41 +227,51 @@ composer dev-tools - `resources/github-actions/`: consumer-facing workflow wrappers synchronized by `dev-tools:sync` - [resources/github-actions-optional/test-statuses.md](resources/github-actions-optional/test-statuses.md): explicit opt-in guide for Dependabot status aliases; verify source workflow identity, PHP matrix and protection contexts before adopting the checkout-free template - `.github/pull_request_template.md`: expected PR structure and reviewer checklist -- `src/Sync/`: shared packaged-directory synchronization primitives used by `skills` and `agents` -- `.agents/skills/`: packaged procedural skills shipped to consumer repositories -- `.agents/agents/`: repository-specific role prompts mirrored through `.github/agents` +- `resources/skills/fast-forward-*`: canonical packaged skills; installed via LLM Skills +- `.agents/agents/`: local repository role prompts mirrored through `.github/agents` - `.dev-tools/wiki-generated`: local Markdown output from normal sync; wiki publication operates on a separate artifact and Wiki checkout ## Additional Notes -- **GrumPHP**: Automatically runs on pre-commit (configured in `grumphp.yml`) +- **GrumPHP**: Hooks prefer local GrumPHP, otherwise CPX `phpro/grumphp-shim:2.26.0`. + Explicit sync copies a durable local `grumphp.yml`; do not persist runtime/cache paths. - **Rector**: Custom rules in `src/Rector/` for automated refactoring - **Documentation**: Sphinx-based docs in `docs/` directory - **Wiki**: `.github/workflows/wiki.yml` delegates to `wiki-direct.yml`; pull requests generate without write credentials and only `main` publishes to the separate Wiki repository. `Home.md`, `classes/` and `functions/` are generated; preserve manually maintained pages outside those surfaces. Normal sync does not initialize a Wiki submodule; `wiki --init` remains an explicit legacy option. - **GitHub Actions**: Reusable workflows live in `.github/workflows/`, local workflow actions live in `.github/actions/`, and consumer wrappers are synchronized from `resources/github-actions/` - **Dependency Health CI**: `.github/workflows/tests.yml` always runs the dependency-health job, and its default `max-outdated` input is `-1` so outdated packages are reported without failing CI on count alone -- **Sync Preview Modes**: `dev-tools:sync --dry-run`, `--check`, and `--interactive` intentionally skip wiki, skills, and agents because those flows do not yet expose non-destructive verification -- **Project Agents**: Packaged role prompts synchronized via `composer agents` and `dev-tools:sync` +- **Sync Preview Modes**: `dev-tools:sync --dry-run`, `--check`, and `--interactive` intentionally skip Wiki generation because it does not yet expose non-destructive verification +- **Skills**: LLM Skills installs exported `fast-forward-*` skills; DevTools has no `skills` or `agents` command + +## Version 2 Consumer Contract + +- Read `docs/usage/dev-tools-2-migration.rst` before changing consumption or extraction. +- Keep the DevTools runtime separate from the consumer Composer graph. Use a verified + exact CPX version; the 2.0.0 example requires publication verification before adoption. +- Use Composer Trusted skills only from installed libraries' resolved payloads. + External tools use a pinned GitHub source. Keep one owner per skill and fail on + missing donor payloads rather than silently fetching a newer fallback. +- Explicit sync still owns generic metadata/workflows/hooks. LLM Skills does not. +- Do not migrate other repositories, merge or publish as an incidental local change. ## Skills Usage - **Changelog**: Use `.agents/skills/fast-forward-changelog/SKILL.md` for external fragment authoring, contribution checks, version-PR review and authorized release maintenance, following the selected release's documentation -- **Creating/Updating Tests**: Use skill `phpunit-tests` in `.agents/skills/phpunit-tests/` for PHPUnit tests with Prophecy -- **Creating/Refreshing AGENTS.md**: Use skill `create-agentsmd` in `.agents/skills/create-agentsmd/` to generate or update repository-root AGENTS instructions for coding agents -- **Generating Documentation**: Use skill `sphinx-docs` in `.agents/skills/sphinx-docs/` for Sphinx documentation in `docs/` -- **Updating README**: Use skill `package-readme` in `.agents/skills/package-readme/` for generating README.md files -- **Updating PHPDoc / PHP Style**: Use skill `phpdoc-code-style` in `.agents/skills/phpdoc-code-style/` for PHPDoc cleanup and repository-specific PHP formatting -- **Drafting / Publishing GitHub Issues**: Use skill `github-issues` in `.agents/skills/github-issues/` to transform a short feature description into a complete, production-ready GitHub issue and create or update it on GitHub when needed -- **Implementing Issues & PRs**: Use skill `github-pull-request` in `.agents/skills/github-pull-request/` to iterate through open GitHub issues and implement them one by one with branching, testing, documentation, and pull requests -- **Reviewing Pull Requests**: Use skill `pull-request-review` in `.agents/skills/pull-request-review/` for findings-first Fast Forward pull-request review focused on regressions, missing tests, missing docs, workflow risk, and generated-output drift +- **Creating/Updating Tests**: Use skill `fast-forward-phpunit-tests` in `resources/skills/fast-forward-phpunit-tests/` for PHPUnit tests with Prophecy +- **Creating/Refreshing AGENTS.md**: Use skill `fast-forward-create-agentsmd` in `resources/skills/fast-forward-create-agentsmd/` to generate or update repository-root AGENTS instructions for coding agents +- **Generating Documentation**: Use skill `fast-forward-sphinx-docs` in `resources/skills/fast-forward-sphinx-docs/` for Sphinx documentation in `docs/` +- **Updating README**: Use skill `fast-forward-package-readme` in `resources/skills/fast-forward-package-readme/` for generating README.md files +- **Updating PHPDoc / PHP Style**: Use skill `fast-forward-phpdoc-code-style` in `resources/skills/fast-forward-phpdoc-code-style/` for PHPDoc cleanup and repository-specific PHP formatting +- **Drafting / Publishing GitHub Issues**: Use skill `fast-forward-github-issues` in `resources/skills/fast-forward-github-issues/` to transform a short feature description into a complete, production-ready GitHub issue and create or update it on GitHub when needed +- **Implementing Issues & PRs**: Use skill `fast-forward-github-pull-request` in `resources/skills/fast-forward-github-pull-request/` to iterate through open GitHub issues and implement them one by one with branching, testing, documentation, and pull requests +- **Reviewing Pull Requests**: Use skill `fast-forward-pull-request-review` in `resources/skills/fast-forward-pull-request-review/` for findings-first Fast Forward pull-request review focused on regressions, missing tests, missing docs, workflow risk, and generated-output drift ## Project Agents -Packaged project-agent prompts live in `.agents/agents/` for both this -repository and consumer repositories that synchronize DevTools assets. These -role prompts define behavior and ownership boundaries, while `.agents/skills/` -remains the procedural source of truth. +Local project-agent prompts live in `.agents/agents/`. They define repository +roles; procedural packages live under `resources/skills/fast-forward-*`. +Do not restore vendor-relative skill/agent symlink installation. - Use `issue-editor` for issue drafting, refinement, comments, updates, and closure workflows. - Use `issue-implementer` for issue-to-branch-to-PR execution. diff --git a/README.md b/README.md index 71b34c15ea..6c63686815 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # FastForward\DevTools -FastForward DevTools is a Composer plugin that standardizes quality checks, -documentation builds, consumer repository bootstrap, and packaged agent skills -across Fast Forward libraries. +Fast Forward DevTools is a standalone PHP quality and repository automation CLI. +Run it through CPX to share tooling without adding it to every library's +Composer dependency graph. [![PHP Version](https://img.shields.io/badge/php-^8.3-777BB4?logo=php&logoColor=white)](https://www.php.net/releases/) [![Composer Package](https://img.shields.io/badge/composer-fast--forward%2Fdev--tools-F28D1A.svg?logo=composer&logoColor=white)](https://packagist.org/packages/fast-forward/dev-tools) @@ -19,375 +19,132 @@ across Fast Forward libraries. ## ✨ Features -- Aggregates refactoring, PHPDoc, code style, tests, and reporting under a - single Composer-facing command vocabulary -- Adds dependency analysis for missing and unused Composer packages through a - single report entrypoint -- Delegates fragment authoring and Keep a Changelog release automation to - the independent Fast Forward Changelog package -- Ships shared workflow stubs, `.editorconfig`, Dependabot configuration, and - other onboarding defaults for consumer repositories -- Packages a rigorous pull-request review skill, matching `review-guardian` - project agent, and ready-for-review workflow brief for high-signal review - intake -- Generates `.github/CODEOWNERS` files from local project metadata instead of - shipping repository-specific owners into consumers -- Synchronizes packaged skills and project-agent prompts into consumer - `.agents/skills` and `.agents/agents` directories using safe link-based - updates -- Supports guide-only and automation-only repositories by skipping PHPUnit or - wiki generation gracefully when no runnable PHP surface exists -- Works both as a Composer plugin and as a local binary -- Preserves local overrides through consumer-first configuration resolution - -## šŸš€ Installation +- Runs refactoring, PHPDoc, code style, tests, dependency analysis and reporting. +- Uses consumer configuration first and packaged defaults when it is absent. +- Executes with its own runtime and an explicit consumer working directory. +- Exports `fast-forward-*` skills under `resources/skills` for LLM Skills. +- Provides explicit repository synchronization for metadata, workflow callers, + editor defaults, Git hooks, ignore rules, licensing and local Wiki Markdown. +- Delegates changelog and release policy to the independent Fast Forward Changelog. -```bash -composer require --dev fast-forward/dev-tools -``` +## šŸš€ Run with CPX -## šŸ› ļø Usage - -Once installed, the plugin automatically exposes the aggregated `dev-tools` -command and the individual Composer commands described below. +Install [CPX](https://github.com/laravel/cpx) and choose an exact published +DevTools 2.x version. Set `DEV_TOOLS_VERSION` to its numeric version. These +examples describe the version 2 interface; verify release availability before +migrating a consumer. Install the consumer's own Composer dependencies normally. ```bash -# Run all standard checks (refactoring, code styling, docs, tests, and reports) -composer dev-tools - -# Automatically fix code standards issues where applicable -composer dev-tools:fix - -# Run the standalone binary from another project directory -vendor/bin/dev-tools --working-dir=/path/to/project tests - -# Store generated reports and caches outside the default .dev-tools workspace -vendor/bin/dev-tools --workspace-dir=.artifacts reports - -# Update the installed DevTools package -vendor/bin/dev-tools self-update +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix ``` -You can also run individual commands for specific development tasks: - -```bash -# Run PHPUnit tests -composer dev-tools tests -composer dev-tools tests --json -composer dev-tools tests --pretty-json - -# Analyze missing, unused, misplaced, and outdated Composer dependencies -composer dependencies -composer dependencies --max-outdated=8 -composer dependencies --max-outdated=-1 -composer dependencies --dev -composer dependencies --dump-usage=symfony/console -composer dependencies --upgrade --dev - -# Analyze code metrics with PhpMetrics -composer metrics -composer metrics --target=.dev-tools/metrics -composer --working-dir=packages/example metrics - -# Create and validate an independent change fragment (CPX and PHP 8.5 required) -cpx --skip-local fast-forward/changelog add "Fixes release-note rendering." --category=fixed --no-interaction -cpx --skip-local fast-forward/changelog check --no-interaction - -# Check and fix code style using ECS and Composer Normalize -composer code-style - -# Refactor code using Rector -composer refactor - -# Check and fix PHPDoc comments -composer phpdoc - -# Generate HTML API documentation using phpDocumentor -composer docs -composer docs --source=docs/user-guide - -# Generate Markdown documentation for the wiki -composer wiki +`--skip-local` chooses CPX's separate installation. DevTools does not need to +appear in the consumer's `require-dev`. Its tool runtime PHP requirements are +separate from the consumer library's compatibility matrix. -# Generate documentation frontpage and related reports -composer reports -composer reports --target=.dev-tools --coverage=.dev-tools/coverage -FAST_FORWARD_WORKSPACE_DIR=.artifacts composer reports - -# Synchronize packaged agent skills into .agents/skills -composer skills - -# Synchronize packaged project agents into .agents/agents -composer agents - -# Synchronize Composer funding metadata with .github/FUNDING.yml -composer funding -composer funding --dry-run - -# Generate .github/CODEOWNERS from composer.json metadata -composer codeowners -composer codeowners --interactive - -# Merges and synchronizes .gitignore files -composer gitignore - -# Manages .gitattributes export-ignore rules for leaner package archives -composer gitattributes - -# Generates a LICENSE file from composer.json license information -composer license - -# Copies packaged or local resources into the consumer repository -composer copy-resource --source resources/docblock --target .docheader +## šŸ› ļø Usage -# Installs Fast Forward Git hooks -composer git-hooks +```bash +# Inspect the selected CLI and runtime metadata +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" list +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" runtime --json --no-ansi --no-interaction -# Updates the composer.json file to match the packaged schema -composer update-composer-json --force +# Run against a different checkout and select an artifact workspace +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" --working-dir=/path/to/project tests +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" --workspace-dir=.artifacts reports -# Installs and synchronizes dev-tools scripts, GitHub Actions workflows, -# .editorconfig, .gitignore rules, packaged skills, packaged agents, and the -# local wiki Markdown in .dev-tools/wiki-generated -composer dev-tools:sync +# Preview and explicitly apply repository synchronization +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --dry-run +cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync ``` -The `dependencies` command ships with -`shipmonk/composer-dependency-analyser` and `rector/swiss-knife` as direct -dependencies of `fast-forward/dev-tools`, so it works without extra -installation in the consumer project. - -The packaged `tests.yml` workflow runs dependency health as a required job and -defaults to `--max-outdated=-1`, which keeps outdated-package findings visible -in CI without failing the workflow for their count alone. - -The `metrics` command ships with `phpmetrics/phpmetrics` as a direct -dependency of `fast-forward/dev-tools`, so consumer repositories can generate -metrics reports without extra setup. - -Guide-only repositories and workflow-only repositories can still use the -packaged command surface. When no runnable PHPUnit surface exists, `tests` -returns a controlled warning instead of failing. When a repository ships -guides without PSR-4 source paths, `docs` builds the guide site without trying -to synthesize API pages. When `.github/wiki` is absent and no wiki has been -initialized yet, `wiki` now skips generation with a warning instead of failing -the whole automation run. - -Normal `dev-tools:sync` generates local Markdown in `.dev-tools/wiki-generated` -without creating a Git submodule. The packaged `wiki.yml` workflow publishes -generated API pages directly to the GitHub Wiki from `main`; pull requests -generate a read-only artifact. Existing workflow wrappers require an explicit -migration or `composer dev-tools:sync --overwrite` to adopt these changes. See -[the workflow guide](docs/usage/github-actions.rst) for publication and page -ownership details. - -Run the independent CLI with `cpx --skip-local fast-forward/changelog`. -Install CPX separately and run it with PHP 8.5; the consumer package still supports PHP -8.3. Changelog is isolated from the consumer Composer dependency graph. -Contributions add `.changelog/*.md`; reviewed version PRs consolidate them -into `CHANGELOG.md`. Do not add an `Unreleased` section. See the -[changelog guide](docs/commands/changelog.rst) for prerequisites and previews. -Require the external workflow's exact `Fast Forward Changelog` commit status -on the PR head, after verifying it in a repository pilot. - -Structured output is available across the DevTools command surface through -`--json`, which returns deterministic `message` / `level` / `context` payloads -for CI, bots, and AI-agent workflows while preserving the normal -human-readable terminal output by default. Use `--pretty-json` when you want -the same structured payload indented for manual inspection in a terminal. -`--pretty-json` also implies JSON output, so there is no need to pass both -flags together. In agent environments, DevTools can also switch to JSON -automatically when the runtime is detected as agent-driven. The external -Changelog CLI has its own output and option contracts; consult its command help. - -`--pretty-json` intentionally remains valid JSON. DevTools does not inject ANSI -escape sequences into that mode today because preserving a parseable payload -takes precedence over terminal-only color. Where orchestrated tools can expose -structured subprocess results safely, DevTools prefers adding stable fields to -the JSON context rather than coloring otherwise strict JSON output. - -For the `tests` command, structured runs now capture the bundled PHPUnit -agent-reporter payload inside `context.output` while preserving the normal -DevTools JSON envelope. That means the top-level command still emits one final -document, and consumers can inspect stable nested keys such as -`context.output.result`, `context.output.summary`, optional -`context.output.details`, and `context.output.coverage` when minimum-coverage -validation is active. - -Progress output is disabled by default on the commands that support transient -rendering, and `--progress` re-enables it for human-readable terminal runs. -When `--json` or `--pretty-json` is active on commands that orchestrate other -tools, DevTools keeps progress suppressed, forwards JSON flags where the -underlying tool supports structured output, and otherwise falls back to -quieter subprocess modes so the captured payload stays machine-readable. The -`tests` command now captures the bundled PHPUnit agent-reporter payload in -structured runs and stores it in `context.output`, preserving `result`, -`summary`, optional `details`, and a `raw_output` fallback when coverage or -other PHPUnit text is emitted before the final reporter JSON. In GitHub -Actions, queued subprocess output is -grouped into collapsible sections, and logged failures emit native workflow -error annotations, including file and line metadata when commands provide it. -The packaged tests, reports, wiki, and changelog workflows also append concise -Markdown outcomes to -`GITHUB_STEP_SUMMARY` so maintainers can scan versions, URLs, preview refs, -verification status, and release results without expanding full logs. This -repository also keeps a bounded retry workflow that reruns failed jobs once -when failed job logs match transient GitHub-side checkout or transport errors -such as HTTP 500 fetch failures, while leaving genuine logic and quality -failures untouched. - -The packaged changelog caller uses -`php-fast-forward/changelog/.github/workflows/changelog.yml@v0`. -The `v0` channel follows published 0.x releases and can include breaking minor -updates. This repository selects that channel explicitly; local CPX examples -use the latest published CLI without a version constraint. Check its help and -compatibility with the workflow when performing release maintenance. -GitHub automation uses its own Docker/PHP runtime and requires Linux with -Docker, Bash and Git, independently of the consumer PHP version. - -The `funding` command keeps supported `composer.json` funding entries aligned -with `.github/FUNDING.yml`, including GitHub Sponsors handles and `custom` -URLs, while preserving unsupported providers in place and re-running -`composer normalize` after manifest updates. - -The `codeowners` command generates `.github/CODEOWNERS` from local -`composer.json` metadata. It prefers explicit GitHub profile URLs from author -metadata, falls back to commented suggestions from support metadata, and can -prompt for owners when `--interactive` is used in a terminal. - -The `skills` command keeps `.agents/skills` aligned with the packaged Fast -Forward skill set. It creates missing links, repairs broken links, and -preserves existing non-symlink directories. The `dev-tools:sync` command calls -`skills` automatically after refreshing the rest of the consumer-facing -automation assets. The packaged skills include reusable flows for GitHub issue -authoring, issue implementation, changelog maintenance, and rigorous pull -request review. - -The `agents` command keeps `.agents/agents` aligned with the packaged Fast -Forward project-agent prompt set. It follows the same symlink-preservation and -broken-link-repair safety model as the packaged skill synchronization flow, and -`dev-tools:sync` calls it automatically in normal synchronization mode. The -packaged agent set includes role prompts such as `issue-implementer`, -`docs-writer`, `test-guardian`, `readme-maintainer`, `review-guardian`, and -`changelog-maintainer`. - -The packaged workflow stubs synchronized by `dev-tools:sync` now also include -changelog automation for pull-request validation and release preparation, so -consumer repositories can adopt the same changelog-driven release flow without -copying workflow logic by hand. They also include a rigorous review intake -workflow that reacts when a pull request becomes ready for review and posts a -deterministic brief for the dedicated review agent. - -The external workflow checks fragments and prepares one managed version PR -on `changelog/version`, with the configured title -`chore(release): fast-forward/dev-tools v{current_version} → v{next_version}`. This repository uses -the official README caller with the explicitly approved -`name: Fast Forward Changelog`, `publish: true` and `version-pr-title` settings: `main`, history -from tags, feedback on every PR and release preparation -enabled. The lifecycle uses its default Ubuntu runner and GitHub token. -Merging the reviewed managed version PR automatically publishes its tag and -GitHub Release from the exact approved commit. The workflow never merges that -PR itself; see the -[workflow guide](docs/usage/github-actions.rst). - -This repository also keeps role-based project agents in `.agents/agents`. They -are packaged for consumer repositories alongside `.agents/skills`, so -downstream projects can adopt both reusable role prompts and the procedural -skills they depend on. +Use `--json` or `--pretty-json` for structured results and `--progress` for +human-readable progress. `--pretty-json` remains valid JSON without ANSI escapes. +The runtime command reports `binary`, `autoload`, `package_root`, `project_root` +and `version`, allowing workflows to discover actual runtime paths. ## 🧰 Command Summary | Command | Purpose | |---------|---------| -| `composer dev-tools` | Runs the full `standards` pipeline. | -| `composer tests` | Runs PHPUnit with local-or-packaged configuration. | -| `composer dependencies` | Previews Rector Swiss Knife dependency updates, then reports missing, unused, misplaced, and outdated Composer dependencies. | -| `composer metrics` | Runs PhpMetrics for the current project and generates requested report artifacts. | -| `composer docs` | Builds the HTML documentation site from PSR-4 code and `docs/`. | -| `composer agents` | Creates or repairs packaged project-agent links in `.agents/agents`. | -| `composer skills` | Creates or repairs packaged skill links in `.agents/skills`. | -| `composer funding` | Synchronizes managed funding metadata between `composer.json` and `.github/FUNDING.yml`. | -| `composer codeowners` | Generates managed `.github/CODEOWNERS` content from local repository metadata. | -| `composer gitattributes` | Manages export-ignore rules in `.gitattributes`. | -| `composer dev-tools:sync` | Updates scripts, CODEOWNERS, funding metadata, workflow stubs, `.editorconfig`, `.gitignore`, `.gitattributes`, local wiki Markdown, packaged skills, and packaged agents. | -| `vendor/bin/dev-tools self-update` / `composer dev-tools:self-update` | Updates the local DevTools package, or the Composer global installation when the active binary is globally installed. | - -`--working-dir`/`-d` changes the project root used by the standalone binary. -`--workspace-dir`/`-w` changes where generated DevTools artifacts and caches are -written when command-specific paths are omitted. Composer executions can use -`FAST_FORWARD_WORKSPACE_DIR=.artifacts composer reports` for the same workspace -policy, while explicit options such as `--target`, `--coverage`, `--metrics`, -and `--cache-dir` continue to take precedence. - -## šŸ”Œ Integration - -DevTools integrates with consumer repositories in two ways. The Composer plugin -exposes the command set automatically after installation, and the local binary -keeps the same command vocabulary when you prefer running tools directly from -`vendor/bin/dev-tools`. The consumer sync flow also refreshes `.agents/skills` -and `.agents/agents` so agents can discover the packaged skills and packaged -role prompts shipped with this repository, including workflows for GitHub -issue/PR handling, changelog maintenance, rigorous pull-request review, PHP -quality tasks, Sphinx docs, README generation, and repository `AGENTS.md` -authoring. +| `standards` | Full quality pipeline; default command. | +| `tests` | PHPUnit with consumer-first configuration and optional coverage. | +| `code-style` | Composer Normalize and ECS; checks do not refresh the lockfile. | +| `refactor` / `phpdoc` | Rector and PHPDoc quality checks and fixes. | +| `dependencies` | Missing, unused, misplaced and outdated dependency analysis. | +| `metrics` | PhpMetrics reports. | +| `docs` / `wiki` / `reports` | HTML, Wiki Markdown and combined reports. | +| `dev-tools:sync` | Explicit metadata and repository asset synchronization. | +| `funding` / `codeowners` | Funding and ownership metadata. | +| `gitignore` / `gitattributes` / `license` / `git-hooks` | Repository defaults. | +| `runtime` | Machine-readable paths and version for automation. | + +Run each command after the CPX package selector, or use `php bin/dev-tools` +when developing this repository. `help ` exposes its current options. + +`self-update` refreshes CPX caches for non-global execution. It does not change +the consumer Composer graph or reviewed exact pins. Adopting a newer release +requires updating the selected script, workflow and skill-source versions. + +## šŸ”Œ Skills and Integration + +[LLM Skills](https://github.com/roxblnfk/skills) installs procedural skills +independently from the executable. DevTools exports its owned skill packages +under `resources/skills/fast-forward-*`; names remain stable when responsibilities +move into dedicated repositories. See the [skill distribution guide](docs/usage/syncing-packaged-skills.rst). + +Use Composer Trusted only for packages actually installed in the consumer. +Use a pinned GitHub source for external tools that are absent from its Composer +graph. Each skill has one owner. Missing trusted donor payloads are errors to +resolve, not permission to fetch a newer fallback silently. + +DevTools no longer registers a Composer plugin, install/update hooks, or the +`skills` and `agents` commands. LLM Skills does not synchronize generic workflow +files, metadata or arbitrary project-agent prompts. `dev-tools:sync` remains an +explicit operation for the repository assets still owned here. Existing custom +files and Dependabot policy require review during migration. ## šŸ—ļø Architecture -- `Composer Plugin` - `FastForward\DevTools\Composer\Plugin` exposes the - packaged command set to Composer and runs `dev-tools:sync` after install and - update. -- `DevTools Container` - `FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)` - resolves the shared application from `DevToolsServiceProvider`, which wires - process execution, filesystem access, external tool wrappers, Git helpers, - diffing, reporting, and template loading. -- `Generic Link Synchronization` - - `FastForward\DevTools\Sync\PackagedDirectorySynchronizer` provides the - shared symlink-preserving workflow used by both `skills` and `agents`, - keeping consumer-owned directories intact while repairing broken links. -- `Lazy Command Loading` - `DevToolsCommandLoader` discovers `#[AsCommand]` - classes and resolves most commands only when they are invoked, while - orchestration commands such as `standards` dispatch other commands through - the console application itself. -- `Consumer Sync Pipeline` - `dev-tools:sync` refreshes `composer.json`, - CODEOWNERS, funding metadata, workflow stubs, repository defaults, git - metadata files, packaged Git hooks, and, in normal mode, local wiki Markdown - plus packaged skills and packaged project agents. - -## šŸ¤ Contributing +The CLI loads its own installation's autoloader. The shared service provider +wires lazy Symfony commands, process execution, project metadata, filesystem +services and reports. Consumer source and configuration are selected through +the working directory. CPX separates dependency resolution, so a config or +container library can use DevTools while DevTools depends on its released runtime. +Public test APIs still need compatible consumer dependencies; isolation does not +make incompatible class definitions interchangeable within one process. -Run `composer dev-tools` before opening a pull request. If you change public -commands or consumer onboarding behavior, update `README.md` and `docs/` -together so downstream libraries keep accurate guidance. +## šŸ› ļø Version 2 Migration -Notable pull requests are also expected to add a change fragment before -review is complete. A typical contributor flow looks like this: - -```bash -# Record the user-visible change -cpx --skip-local fast-forward/changelog add "Refines release-note rendering." --category=changed --no-interaction +Follow the [migration and extraction strategy](docs/usage/dev-tools-2-migration.rst). +Publish and verify DevTools 2 first, then migrate each consumer in a separate PR: +pin the external runtime and skill sources, remove DevTools from `require-dev`, +replace plugin commands and local vendor paths, and verify the actual checks. +The guide also proposes issue triage without changing issue state. -# Validate fragments; use --since after committing the contribution -cpx --skip-local fast-forward/changelog check --since=origin/main --no-interaction -``` - -Maintainers can inspect pending fragments without consuming them: +## šŸ¤ Contributing ```bash -cpx --skip-local fast-forward/changelog status --source=tags --no-interaction +composer install +php bin/dev-tools tests +composer dev-tools ``` -Use the managed version PR for consolidation. Test history-writing commands -only in a disposable checkout; they can consume accepted fragments. PR origin -resolution is enabled in automation and does not require editing a fragment -after opening its PR. Unknown PR numbers stay absent. +Update README and docs when commands or consumer behavior change. Add an +independent `.changelog/*.md` fragment and validate it using the selected release +of [Fast Forward Changelog](https://github.com/php-fast-forward/changelog). +Release preparation remains separate from merge and publication authority. -## šŸ“„ License +## šŸ›” License -This package is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. +MIT; see [LICENSE](LICENSE). ## šŸ”— Links - [Repository](https://github.com/php-fast-forward/dev-tools) - [Packagist](https://packagist.org/packages/fast-forward/dev-tools) -- [Documentation](https://php-fast-forward.github.io/dev-tools/index.html) -- [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119) +- [Documentation](docs/index.rst) +- [Workflow guide](docs/usage/github-actions.rst) diff --git a/composer.json b/composer.json index 5cd333ffdc..4e341a9eb0 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "fast-forward/dev-tools", "description": "Fast Forward Development Tools for PHP projects", "license": "MIT", - "type": "composer-plugin", + "type": "library", "keywords": [ "dev-tools", "testing", @@ -40,7 +40,6 @@ ], "require": { "php": "^8.3", - "composer-plugin-api": "^2.0", "container-interop/service-provider": "^0.4.1", "dg/bypass-finals": "^1.9", "ergebnis/agent-detector": "^1.1", @@ -56,7 +55,6 @@ "php-parallel-lint/php-parallel-lint": "^1.4", "phpdocumentor/shim": "^3.9", "phpmetrics/phpmetrics": "^2.9", - "phpro/grumphp-shim": "^2.20", "phpspec/prophecy": "^1.26", "phpspec/prophecy-phpunit": "^2.5", "phpunit/php-code-coverage": "^12.5 || ^13.0", @@ -82,8 +80,8 @@ "thecodingmachine/safe": "^3.4", "twig/twig": "^3.24" }, - "require-dev": { - "composer/composer": "^2.9" + "suggest": { + "llm/skills": "Synchronize bundled skills from Composer dependencies or explicit GitHub sources." }, "minimum-stability": "stable", "autoload": { @@ -114,12 +112,10 @@ }, "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "2.x-dev" }, - "class": "FastForward\\DevTools\\Composer\\Plugin", "gitattributes": { "keep-in-export": [ - "/.agents/", "/.editorconfig", "/.gitignore", "/.php-cs-fixer.dist.php", @@ -129,12 +125,12 @@ "/rector.php" ] }, - "grumphp": { - "config-default-path": "grumphp.yml" + "skills": { + "source": "resources/skills" } }, "scripts": { - "dev-tools": "dev-tools", + "dev-tools": "@php bin/dev-tools", "dev-tools:fix": "@dev-tools --fix" } } diff --git a/docs/advanced/consumer-automation.rst b/docs/advanced/consumer-automation.rst index ab57d8d1a2..d1d08c3078 100644 --- a/docs/advanced/consumer-automation.rst +++ b/docs/advanced/consumer-automation.rst @@ -5,9 +5,8 @@ FastForward DevTools plays two roles at once: - producer: this repository ships reusable workflow templates and default configuration files; -- consumer helper: the ``dev-tools:sync`` command copies those assets and links - packaged skills and packaged project agents into other Fast Forward - libraries. +- consumer helper: the explicit ``dev-tools:sync`` command copies repository assets. + LLM Skills installs procedural packages independently. Reusable Workflows Versus Consumer Stubs ---------------------------------------- @@ -30,7 +29,7 @@ implementation in this repository is increasingly composed from local actions in ``workflow_call``. * - ``.github/actions/php/*`` - Shared PHP helpers such as workflow PHP-version resolution, Composer - setup, and local-vs-workflow DevTools runtime bootstrap. + setup, and isolated CPX or self-source DevTools runtime bootstrap. * - ``php-fast-forward/changelog`` - External fragment, history, version-PR and publication lifecycle called through its reusable workflow at ``@v0``. @@ -56,12 +55,8 @@ implementation in this repository is increasingly composed from local actions in - This repository's own Dependabot configuration. * - ``resources/dependabot.yml`` - Template copied into consumer repositories. - * - ``.agents/skills/*`` - - Packaged agent skills linked into consumer repositories by the - ``skills`` command. - * - ``.agents/agents/*`` - - Packaged project-agent prompts linked into consumer repositories by - the ``agents`` command. + * - ``resources/skills/fast-forward-*`` + - LLM Skills independently installs owned procedural packages. * - ``.dev-tools/wiki-generated`` - Local Markdown API documentation generated by normal sync and the direct Wiki workflow without a repository submodule. @@ -116,7 +111,7 @@ How Changelog and Project Automation Fit In - ``.github/workflows/review.yml`` composes its ready-for-review intake from ``.github/actions/review/*`` and posts a deterministic brief that points maintainers to the packaged ``review-guardian`` agent and - ``pull-request-review`` skill. + ``fast-forward-pull-request-review`` skill. - ``tests.yml``, ``reports.yml``, and legacy wiki preview and maintenance flows delegate final run summaries to ``.github/actions/summary/*`` after their final-state information is known. @@ -131,6 +126,6 @@ Producer Impact --------------- Any change to ``resources/github-actions``, ``resources/dependabot.yml``, -``.agents/skills``, ``.agents/agents``, ``.github/workflows``, or +``resources/skills``, ``.github/workflows``, or ``FastForward\DevTools\Console\Command\SyncCommand`` changes the default onboarding story for every consumer library. diff --git a/docs/api/commands.rst b/docs/api/commands.rst index d8b955bce9..b4f13a7758 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -5,8 +5,7 @@ All public CLI commands are Symfony Console commands resolved lazily through ``DevToolsCommandLoader``. Command classes receive their collaborators from the shared ``DevToolsServiceProvider`` container, while orchestration commands such as ``standards`` dispatch other commands through the console application -itself. Composer integration uses proxy commands to expose that same Symfony -command set without forcing each command to extend Composer's ``BaseCommand``. +itself. The architecture also relies on ``ProcessBuilder`` and ``ProcessQueue`` for fluent process management where subprocess execution is needed. @@ -47,12 +46,6 @@ fluent process management where subprocess execution is needed. * - ``FastForward\DevTools\Console\Command\ReportsCommand`` - ``reports`` - Combines documentation, coverage, and metrics generation. - * - ``FastForward\DevTools\Console\Command\AgentsCommand`` - - ``agents`` - - Synchronizes packaged project agents into ``.agents/agents``. - * - ``FastForward\DevTools\Console\Command\SkillsCommand`` - - ``skills`` - - Synchronizes packaged agent skills into ``.agents/skills``. * - ``FastForward\DevTools\Console\Command\FundingCommand`` - ``funding`` - Synchronizes managed funding metadata between Composer and GitHub files. @@ -61,8 +54,7 @@ fluent process management where subprocess execution is needed. - Generates managed ``.github/CODEOWNERS`` content from project metadata. * - ``FastForward\DevTools\Console\Command\SyncCommand`` - ``dev-tools:sync`` - - Synchronizes consumer-facing scripts, automation assets, and packaged - skills and project agents. + - Synchronizes consumer-facing scripts, metadata, workflows and defaults. * - ``FastForward\DevTools\Console\Command\GitIgnoreCommand`` - ``gitignore`` - Merges and synchronizes .gitignore files. diff --git a/docs/api/composer-integration.rst b/docs/api/composer-integration.rst index 9aa33d7348..1c3c34f536 100644 --- a/docs/api/composer-integration.rst +++ b/docs/api/composer-integration.rst @@ -1,48 +1,16 @@ -Composer Integration -==================== - -FastForward DevTools is exposed through both a Composer plugin and a dedicated -console application. - -Startup Chain -------------- - -1. ``bin/dev-tools`` loads ``bin/dev-tools.php``. -2. ``bin/dev-tools.php`` prefers the consumer project's - ``vendor/autoload.php`` and falls back to the package autoloader. -3. ``bin/dev-tools.php`` starts ``FastForward\DevTools\Console\DevTools`` and appends - ``--no-plugins``. -4. ``FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)`` - resolves the shared application container from - ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. -5. ``FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader`` - lazily discovers ``#[AsCommand]`` classes from the command namespace. -6. ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider`` later - exposes that same command set to Composer. - -Composer Plugin Classes ------------------------ - -.. list-table:: - :header-rows: 1 - - * - Class - - Purpose - * - ``FastForward\DevTools\Composer\Plugin`` - - Registers the command provider and runs ``dev-tools:sync`` after - Composer install and update. - * - ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider`` - - Exposes Symfony command instances to Composer through proxy commands - while filtering names and aliases already registered by Composer or the - root project's scripts. - * - ``FastForward\DevTools\Composer\Command\ProxyCommand`` - - Adapts one Symfony command to Composer's command provider contract. - * - ``FastForward\DevTools\Console\DevTools`` - - Console application used by the local binary. - -Why ``--no-plugins`` Is Appended --------------------------------- - -The local binary already knows which commands it needs. Appending -``--no-plugins`` keeps the standalone application predictable and avoids -pulling unrelated Composer plugins into the command runtime. +Standalone Runtime and Composer Scripts +======================================= + +DevTools is a Composer-distributed library exposing ``bin/dev-tools``. +Version 2 removes the plugin, command provider and proxy commands. +Installing or updating it does not synchronize a repository. + +The binary loads its own installation's autoloader. The shared container and +``DevToolsCommandLoader`` lazily resolve Symfony Console commands. Consumer +autoloading supports tools executing project code; it does not select the CLI. + +Use CPX with ``--skip-local`` and an exact package version. Reviewed Composer +scripts can wrap that invocation without requiring DevTools. The command +``runtime --json --no-ansi --no-interaction`` reports ``binary``, ``autoload``, +``package_root``, ``project_root`` and ``version`` for automation. +See :doc:`../usage/dev-tools-2-migration`. diff --git a/docs/commands/agents.rst b/docs/commands/agents.rst index 860a0b794d..e867a1ea60 100644 --- a/docs/commands/agents.rst +++ b/docs/commands/agents.rst @@ -1,62 +1,5 @@ -agents -====== +Agent Prompt Migration +====================== -Synchronizes Fast Forward project agents into ``.agents/agents`` directory. - -Description ------------ - -The ``agents`` command synchronizes packaged project-agent prompts into the -consumer repository's ``.agents/agents`` directory using repository-relative -symlinks. - -Usage ------ - -.. code-block:: bash - - composer agents - vendor/bin/dev-tools agents - -Options -------- - -This command supports: - -- ``--json`` to emit a structured machine-readable payload instead of the - normal terminal output; -- ``--pretty-json`` to emit the same structured payload with indentation for - terminal inspection. - -Examples --------- - -Synchronize agents: - -.. code-block:: bash - - composer agents - -Exit Codes ----------- - -.. list-table:: - :header-rows: 1 - - * - Code - - Meaning - * - 0 - - Success. Agents synchronized. - * - 1 - - Failure. No packaged agents or sync error. - -Behavior --------- - -- Verifies the packaged ``.agents/agents`` directory before doing any work. -- Creates the consumer ``.agents/agents`` directory when missing. -- Creates missing repository-relative symlinks to packaged project agents. -- Repairs broken symlinks. -- Preserves an existing non-symlink directory instead of overwriting it. -- Reuses the same generic packaged-directory synchronizer as ``skills`` so - both commands follow identical safety rules. +Version 2 removes the DevTools ``agents`` command. Custom role prompts remain +repository-owned. See :doc:`../usage/syncing-packaged-agents`. diff --git a/docs/commands/code-style.rst b/docs/commands/code-style.rst index c06dc0131e..66e65fd04d 100644 --- a/docs/commands/code-style.rst +++ b/docs/commands/code-style.rst @@ -8,8 +8,8 @@ Description The ``code-style`` command orchestrates code style checking and fixing using: -- ``composer update --lock --quiet`` - updates lock file -- ``composer normalize`` - normalizes composer.json format +- ``composer normalize`` - runs the installed plugin in the isolated runtime + root against the consumer's absolute manifest path with ``--no-update-lock`` - ``vendor/bin/ecs`` - EasyCodingStandard for PHP code style Usage @@ -17,11 +17,11 @@ Usage .. code-block:: bash - composer code-style - composer dev-tools code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style - composer code-style --fix - composer dev-tools:fix code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix code-style vendor/bin/dev-tools code-style [options] @@ -48,14 +48,14 @@ Check code style (dry-run): .. code-block:: bash - composer code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style Fix code style automatically: .. code-block:: bash - composer code-style --fix - composer dev-tools:fix code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix code-style Exit Codes --------- @@ -73,7 +73,7 @@ Exit Codes Behavior --------- -- Always runs ``composer update --lock --quiet`` first. +- Does not run Composer dependency updates or refresh the lockfile first. - Composer Normalize runs in ``--dry-run`` mode unless ``--fix`` is specified. - ECS uses local ``ecs.php`` when present, otherwise falls back to packaged default. - Progress output is disabled by default; use ``--progress`` to re-enable it in diff --git a/docs/commands/codeowners.rst b/docs/commands/codeowners.rst index face58314c..a93c3559bf 100644 --- a/docs/commands/codeowners.rst +++ b/docs/commands/codeowners.rst @@ -17,8 +17,8 @@ Usage .. code-block:: bash - composer codeowners - composer codeowners [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners [options] Options ------- @@ -54,25 +54,25 @@ Generate CODEOWNERS from the current repository metadata: .. code-block:: bash - composer codeowners + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners Preview drift without writing: .. code-block:: bash - composer codeowners --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners --dry-run Fail in CI when CODEOWNERS needs an update: .. code-block:: bash - composer codeowners --check + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners --check Prompt for explicit owners when metadata is incomplete: .. code-block:: bash - composer codeowners --interactive + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners --interactive Behavior -------- diff --git a/docs/commands/copy-resource.rst b/docs/commands/copy-resource.rst index 4f9f0f5a9d..e11a22797d 100644 --- a/docs/commands/copy-resource.rst +++ b/docs/commands/copy-resource.rst @@ -21,8 +21,8 @@ Usage .. code-block:: bash - composer copy-resource --source --target - composer dev-tools copy-resource -- --source --target [--overwrite] [--dry-run] [--check] [--interactive] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source --target + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source --target [--overwrite] [--dry-run] [--check] [--interactive] vendor/bin/dev-tools copy-resource --source --target [--overwrite] [--dry-run] [--check] [--interactive] Options @@ -64,19 +64,19 @@ Copy a docblock template: .. code-block:: bash - composer copy-resource --source resources/docblock --target .docheader + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source resources/docblock --target .docheader Copy a directory of resources: .. code-block:: bash - composer copy-resource --source resources/git-hooks --target .git/hooks --overwrite + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source resources/git-hooks --target .git/hooks --overwrite Copy and overwrite an existing file: .. code-block:: bash - composer copy-resource --source .editorconfig --target .editorconfig --overwrite + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source .editorconfig --target .editorconfig --overwrite Exit Codes --------- diff --git a/docs/commands/dependencies.rst b/docs/commands/dependencies.rst index 5e7a10b1f6..bbe85985c6 100644 --- a/docs/commands/dependencies.rst +++ b/docs/commands/dependencies.rst @@ -20,8 +20,8 @@ Usage .. code-block:: bash - composer dependencies - composer dependencies [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies [options] composer deps vendor/bin/dev-tools dependencies [options] @@ -81,43 +81,43 @@ Run dependency analysis: .. code-block:: bash - composer dependencies + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies Allow up to 10 outdated packages: .. code-block:: bash - composer dependencies --max-outdated=10 + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --max-outdated=10 Report outdated packages without failing on their count: .. code-block:: bash - composer dependencies --max-outdated=-1 + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --max-outdated=-1 Preview the upgrade workflow: .. code-block:: bash - composer dependencies --dev + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --dev Dump all matched usages for one package: .. code-block:: bash - composer dependencies --dump-usage=symfony/console + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --dump-usage=symfony/console Audit shadow dependencies: .. code-block:: bash - composer dependencies --show-shadow-dependencies + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --show-shadow-dependencies Apply the upgrade workflow and then analyze dependencies: .. code-block:: bash - composer dependencies --upgrade --dev + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --upgrade --dev Using the alias: @@ -167,3 +167,11 @@ Behavior - Returns a non-zero exit code when missing, unused, misplaced, or too many outdated dependencies are found. - Both tools must be available in ``vendor/bin/``. + +Normalization runtime +--------------------- + +Composer Normalize executes as a plugin from the isolated runtime root against +the consumer's absolute manifest path with ``--no-update-lock``. The consumer +does not need that plugin installed. An explicitly requested dependency upgrade +may still update its Composer lockfile; normalization itself does not. diff --git a/docs/commands/docs.rst b/docs/commands/docs.rst index 351cf4c98f..86a4a61044 100644 --- a/docs/commands/docs.rst +++ b/docs/commands/docs.rst @@ -17,10 +17,10 @@ Usage .. code-block:: bash - composer docs - composer dev-tools docs - composer docs [options] - composer dev-tools docs -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs [options] vendor/bin/dev-tools docs [options] Options @@ -67,25 +67,25 @@ Generate docs with defaults: .. code-block:: bash - composer docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs Generate to custom directory: .. code-block:: bash - composer docs --target=dist/docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs --target=dist/docs Use custom source: .. code-block:: bash - composer docs --source=docs/user-guide + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs --source=docs/user-guide Generate without cache: .. code-block:: bash - composer docs --no-cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs --no-cache Exit Codes --------- diff --git a/docs/commands/funding.rst b/docs/commands/funding.rst index 3910d6e658..f860712e1d 100644 --- a/docs/commands/funding.rst +++ b/docs/commands/funding.rst @@ -23,9 +23,9 @@ Usage .. code-block:: bash - composer funding - composer funding [options] - composer dev-tools funding -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding [options] vendor/bin/dev-tools funding [options] Options @@ -63,25 +63,25 @@ Synchronize the default files: .. code-block:: bash - composer funding + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding Preview funding drift: .. code-block:: bash - composer funding --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding --dry-run Validate funding metadata in CI: .. code-block:: bash - composer funding --check + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding --check Use custom paths: .. code-block:: bash - composer funding --composer-file=packages/example/composer.json --funding-file=packages/example/.github/FUNDING.yml + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding --composer-file=packages/example/composer.json --funding-file=packages/example/.github/FUNDING.yml Exit Codes ---------- @@ -104,7 +104,8 @@ Behavior entries. - Preserves unsupported Composer funding providers and unsupported GitHub funding YAML keys. -- Normalizes ``composer.json`` with ``composer normalize`` after applying +- Normalizes the absolute consumer manifest through the Composer Normalize + plugin in the isolated runtime root with ``--no-update-lock`` after applying funding metadata updates. - Creates ``.github/FUNDING.yml`` when Composer declares supported funding metadata and the file is missing. diff --git a/docs/commands/git-hooks.rst b/docs/commands/git-hooks.rst index 5982b182dc..fac809479a 100644 --- a/docs/commands/git-hooks.rst +++ b/docs/commands/git-hooks.rst @@ -19,9 +19,9 @@ Usage .. code-block:: bash - composer git-hooks - composer git-hooks [options] - composer dev-tools git-hooks -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks [options] vendor/bin/dev-tools git-hooks [options] Options @@ -65,13 +65,13 @@ Install hooks with defaults: .. code-block:: bash - composer git-hooks + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks Install hooks without overwriting existing ones: .. code-block:: bash - composer git-hooks --no-overwrite + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks --no-overwrite Exit Codes --------- @@ -95,3 +95,13 @@ and ``--check`` returns failure without creating files or parent directories, including when overwrite or interactive options are also supplied. When both preview flags are supplied, ``--check`` determines the failure status. Existing targets are compared without replacement or permission changes. + +Runtime and durable configuration +--------------------------------- + +Hooks prefer ``vendor/bin/grumphp`` or ``vendor/bin/grumphp.phar``; otherwise +they run ``cpx --skip-local phpro/grumphp-shim:2.26.0``. CPX must be available +when no local executable exists. GrumPHP is not a DevTools runtime dependency. +Explicit ``dev-tools:sync`` copies a missing ``grumphp.yml`` into the consumer +root. Local overrides remain authoritative; hooks do not persist temporary +runtime/cache paths. diff --git a/docs/commands/gitattributes.rst b/docs/commands/gitattributes.rst index ff1e6a82d7..f2003b3364 100644 --- a/docs/commands/gitattributes.rst +++ b/docs/commands/gitattributes.rst @@ -14,8 +14,8 @@ Usage .. code-block:: bash - composer gitattributes - composer dev-tools gitattributes + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitattributes + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitattributes vendor/bin/dev-tools gitattributes Options @@ -44,7 +44,7 @@ Manage export-ignore rules: .. code-block:: bash - composer gitattributes + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitattributes Exit Codes --------- diff --git a/docs/commands/gitignore.rst b/docs/commands/gitignore.rst index b7aba85f39..bde93537f1 100644 --- a/docs/commands/gitignore.rst +++ b/docs/commands/gitignore.rst @@ -14,9 +14,9 @@ Usage .. code-block:: bash - composer gitignore - composer gitignore [options] - composer dev-tools gitignore -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore [options] vendor/bin/dev-tools gitignore [options] Options @@ -51,13 +51,13 @@ Merge .gitignore files: .. code-block:: bash - composer gitignore + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore Specify custom paths: .. code-block:: bash - composer gitignore --source=/path/to/source/.gitignore --target=/path/to/target/.gitignore + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore --source=/path/to/source/.gitignore --target=/path/to/target/.gitignore Exit Codes --------- diff --git a/docs/commands/index.rst b/docs/commands/index.rst index ae1a63998f..1a47a39fc2 100644 --- a/docs/commands/index.rst +++ b/docs/commands/index.rst @@ -1,7 +1,10 @@ Command Reference ================= -Detailed documentation for each dev-tools command. +Detailed documentation for each DevTools command. CPX examples assume +``DEV_TOOLS_VERSION`` contains an exact published 2.x numeric version selected +as described in :doc:`../getting-started/installation`. The consumer's own +Composer dependencies remain separate from the DevTools runtime. .. toctree:: :maxdepth: 1 diff --git a/docs/commands/license.rst b/docs/commands/license.rst index feee6bbc39..cd0f710ad4 100644 --- a/docs/commands/license.rst +++ b/docs/commands/license.rst @@ -14,9 +14,9 @@ Usage .. code-block:: bash - composer license - composer license [options] - composer dev-tools license -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license [options] vendor/bin/dev-tools license [options] Options @@ -48,13 +48,13 @@ Generate LICENSE file: .. code-block:: bash - composer license + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license Generate to custom path: .. code-block:: bash - composer license --target=LICENCE + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license --target=LICENCE Exit Codes --------- diff --git a/docs/commands/metrics.rst b/docs/commands/metrics.rst index d3cb43d006..4e774a3db1 100644 --- a/docs/commands/metrics.rst +++ b/docs/commands/metrics.rst @@ -15,8 +15,8 @@ Usage .. code-block:: bash - composer metrics - composer metrics [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics [options] vendor/bin/dev-tools metrics [options] Options @@ -66,25 +66,25 @@ Analyze the current repository with defaults: .. code-block:: bash - composer metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics Generate an HTML report for manual inspection: .. code-block:: bash - composer metrics --target=.dev-tools/metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics --target=.dev-tools/metrics Generate the full metrics artifact set for CI previews: .. code-block:: bash - composer metrics --target=.dev-tools/metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics --target=.dev-tools/metrics Analyze another checkout through Composer's inherited working directory: .. code-block:: bash - composer --working-dir=packages/example metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" --working-dir=packages/example metrics Behavior -------- diff --git a/docs/commands/phpdoc.rst b/docs/commands/phpdoc.rst index ab2e5ce30e..ed8e4002d2 100644 --- a/docs/commands/phpdoc.rst +++ b/docs/commands/phpdoc.rst @@ -19,9 +19,9 @@ Usage .. code-block:: bash - composer phpdoc - composer phpdoc [options] - composer dev-tools phpdoc -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc [options] vendor/bin/dev-tools phpdoc [options] Arguments @@ -67,25 +67,25 @@ Check PHPDocs (dry-run): .. code-block:: bash - composer phpdoc + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc Fix PHPDocs automatically: .. code-block:: bash - composer phpdoc --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc --fix Check specific directory: .. code-block:: bash - composer phpdoc ./src + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc ./src Check without cache: .. code-block:: bash - composer phpdoc --no-cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc --no-cache Exit Codes --------- diff --git a/docs/commands/refactor.rst b/docs/commands/refactor.rst index ea621be153..b13d910e61 100644 --- a/docs/commands/refactor.rst +++ b/docs/commands/refactor.rst @@ -14,10 +14,10 @@ Usage .. code-block:: bash - composer refactor - composer refactor --fix - composer dev-tools:fix refactor - composer dev-tools refactor -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix refactor + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor [options] vendor/bin/dev-tools refactor [options] Options @@ -47,13 +47,13 @@ Run Rector in dry-run mode: .. code-block:: bash - composer refactor + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor Apply fixes automatically: .. code-block:: bash - composer refactor --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor --fix Exit Codes --------- diff --git a/docs/commands/reports.rst b/docs/commands/reports.rst index 262c41eced..da9927cef6 100644 --- a/docs/commands/reports.rst +++ b/docs/commands/reports.rst @@ -21,8 +21,8 @@ Usage .. code-block:: bash - composer reports - composer reports [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports [options] vendor/bin/dev-tools reports -- [options] Options @@ -70,25 +70,25 @@ Generate reports with defaults: .. code-block:: bash - composer reports + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports Generate to custom directories: .. code-block:: bash - composer reports --target=.dev-tools --coverage=.dev-tools/coverage + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports --target=.dev-tools --coverage=.dev-tools/coverage Generate reports with a custom metrics directory: .. code-block:: bash - composer reports --metrics=.dev-tools/metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports --metrics=.dev-tools/metrics Generate reports with cache disabled for nested docs and tests: .. code-block:: bash - composer reports --no-cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports --no-cache Exit Codes --------- diff --git a/docs/commands/self-update.rst b/docs/commands/self-update.rst index 1b7a44b8c5..95ce95407c 100644 --- a/docs/commands/self-update.rst +++ b/docs/commands/self-update.rst @@ -1,8 +1,9 @@ self-update =========== -``self-update`` updates the installed ``fast-forward/dev-tools`` package -through Composer. +``self-update`` refreshes DevTools through its owning package manager. +Non-global execution runs ``cpx update fast-forward/dev-tools`` without +changing the consumer's Composer manifest or lockfile. Usage ----- @@ -10,12 +11,15 @@ Usage .. code-block:: bash vendor/bin/dev-tools self-update - composer dev-tools:self-update + +For CPX consumption, update reviewed exact version selectors explicitly. +Cache refresh does not change a pinned script, workflow input or skill-source +ref, so adopting a newer release still requires a reviewed pin change. When the standalone ``dev-tools`` binary is itself loaded from Composer's global installation, ``self-update`` automatically targets -``composer global update fast-forward/dev-tools``. Local project -installations update the current project by default. +``composer global update fast-forward/dev-tools``. Other execution uses the +CPX cache update route, including execution from a project-local binary. Global runtime options ---------------------- @@ -36,7 +40,7 @@ Composer executions can use Composer's own ``--working-dir``/``-d`` option. ``--workspace-dir`` (or ``-w``) changes where generated DevTools artifacts and caches are written when command-specific paths are omitted. It does not change -the project root selected by ``--working-dir``. Composer plugin executions can use +the project root selected by ``--working-dir``. Composer script wrappers can use ``FAST_FORWARD_WORKSPACE_DIR=.artifacts`` to apply the same workspace policy. Explicit command options such as ``--target``, ``--coverage``, ``--metrics``, and ``--cache-dir`` continue to take precedence over the workspace default. diff --git a/docs/commands/skills.rst b/docs/commands/skills.rst index af610a43a6..ffc836d836 100644 --- a/docs/commands/skills.rst +++ b/docs/commands/skills.rst @@ -1,59 +1,6 @@ -skills -====== +Skills Distribution +=================== -Synchronizes Fast Forward skills into .agents/skills directory. - -Description ------------ - -The ``skills`` command synchronizes packaged agent skills into the consumer -repository's ``.agents/skills`` directory using repository-relative symlinks. - -Usage ------ - -.. code-block:: bash - - composer skills - vendor/bin/dev-tools skills - -Options -------- - -This command supports: - -- ``--json`` to emit a structured machine-readable payload instead of the - normal terminal output; -- ``--pretty-json`` to emit the same structured payload with indentation for - terminal inspection. - -Examples --------- - -Synchronize skills: - -.. code-block:: bash - - composer skills - -Exit Codes ---------- - -.. list-table:: - :header-rows: 1 - - * - Code - - Meaning - * - 0 - - Success. Skills synchronized. - * - 1 - - Failure. No packaged skills or sync error. - -Behavior ---------- - -- Verifies the packaged ``.agents/skills`` directory before doing any work. -- Creates the consumer ``.agents/skills`` directory when missing. -- Creates missing repository-relative symlinks to packaged skills. -- Repairs broken symlinks. -- Preserves an existing non-symlink directory instead of overwriting it. +Version 2 removes the DevTools ``skills`` command. LLM Skills owns installation +and updates of ``fast-forward-*`` packages exported under ``resources/skills``. +See :doc:`../usage/syncing-packaged-skills`. diff --git a/docs/commands/standards.rst b/docs/commands/standards.rst index 122b245fea..d5f354553a 100644 --- a/docs/commands/standards.rst +++ b/docs/commands/standards.rst @@ -18,17 +18,17 @@ Usage .. code-block:: bash - composer standards - composer standards --fix - composer dev-tools standards -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards [options] vendor/bin/dev-tools standards [options] Alternatively, you can run the unified fixing variant: .. code-block:: bash - composer dev-tools - composer dev-tools:fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix Options ------- @@ -62,7 +62,7 @@ Exit Codes Behavior --------- -- This is the default command when running ``composer dev-tools`` without args. +- This is the default command when running ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` without args. - Each phase runs in sequence; every nonzero queue status makes the command return failure (1), including invalid invocations and missing executables. The original queue status is retained as ``exit_code`` in the failure log diff --git a/docs/commands/sync.rst b/docs/commands/sync.rst index f6598d7449..d82abae929 100644 --- a/docs/commands/sync.rst +++ b/docs/commands/sync.rst @@ -11,24 +11,22 @@ The ``dev-tools:sync`` command synchronizes consumer-facing automation and defau 1. ``update-composer-json`` - adds dev-tools scripts to composer.json 2. ``copy-resource`` - copies GitHub Actions workflows, including changelog automation 3. ``copy-resource`` - copies .editorconfig -4. ``copy-resource`` - copies dependabot.yml +4. ``copy-resource`` - copies dependabot.yml and missing local grumphp.yml 5. ``codeowners`` - generates ``.github/CODEOWNERS`` from local metadata 6. ``funding`` - synchronizes ``composer.json`` funding metadata with ``.github/FUNDING.yml`` 7. ``wiki --target=.dev-tools/wiki-generated`` - generates local wiki Markdown 8. ``gitignore`` - merges .gitignore files 9. ``gitattributes`` - manages export-ignore rules -10. ``skills`` - synchronizes packaged skills -11. ``agents`` - synchronizes packaged project agents -12. ``license`` - generates LICENSE file -13. ``git-hooks`` - installs Git hooks +10. ``license`` - generates LICENSE file +11. ``git-hooks`` - installs Git hooks Usage ----- .. code-block:: bash - composer dev-tools:sync - composer dev-tools:sync [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync [options] Options ------- @@ -62,25 +60,25 @@ Run sync: .. code-block:: bash - composer dev-tools:sync + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync Sync with overwrite: .. code-block:: bash - composer dev-tools:sync --overwrite + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --overwrite Preview managed-file drift: .. code-block:: bash - composer dev-tools:sync --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --dry-run Fail in CI when managed files drift: .. code-block:: bash - composer dev-tools:sync --check + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --check Exit Codes --------- @@ -121,7 +119,5 @@ Behavior ``wiki-maintenance.yml`` wrappers explicitly or sync with ``--overwrite``. The packaged maintenance wrapper is manual-only compatibility guidance. - Calls other commands in sequence. -- Runs ``skills`` and ``agents`` in normal mode so consumer repositories - receive both packaged procedural skills and packaged role prompts. -- ``wiki``, ``skills``, and ``agents`` are skipped in preview/check modes - until they expose a non-destructive verification path. +- LLM Skills installs skills independently. DevTools does not install agent links. +- ``wiki`` is skipped in preview/check modes until it exposes a non-destructive verification path. diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index b81039d1e7..b073ee29da 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -21,9 +21,9 @@ Usage .. code-block:: bash - composer tests - composer tests [path] [options] - composer dev-tools tests -- [path] [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests [path] [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests [path] [options] vendor/bin/dev-tools tests [path] [options] Arguments @@ -81,62 +81,62 @@ Run all tests: .. code-block:: bash - composer tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests Run tests in a specific directory: .. code-block:: bash - composer tests ./tests/unit + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests ./tests/unit Run with coverage report: .. code-block:: bash - composer tests --coverage=.dev-tools/coverage + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --coverage=.dev-tools/coverage Run with concise coverage text output: .. code-block:: bash - composer tests --coverage=.dev-tools/coverage --coverage-summary + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --coverage=.dev-tools/coverage --coverage-summary Run tests matching a pattern: .. code-block:: bash - composer tests -- --filter=EventTracerTest + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --filter=EventTracerTest Run with minimum coverage enforcement: .. code-block:: bash - composer tests --min-coverage=80 + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --min-coverage=80 Run with structured JSON output: .. code-block:: bash - composer tests --json - composer tests --pretty-json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --pretty-json Run without cache: .. code-block:: bash - composer tests --no-cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --no-cache Force cache on explicitly: .. code-block:: bash - composer tests --cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --cache Run with PHPUnit progress output enabled: .. code-block:: bash - composer tests --progress + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --progress Exit Codes --------- diff --git a/docs/commands/update-composer-json.rst b/docs/commands/update-composer-json.rst index cf96290da2..e27bb4ab61 100644 --- a/docs/commands/update-composer-json.rst +++ b/docs/commands/update-composer-json.rst @@ -19,8 +19,8 @@ Usage .. code-block:: bash - composer update-composer-json - composer dev-tools update-composer-json -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" update-composer-json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" update-composer-json [options] vendor/bin/dev-tools update-composer-json [options] Options @@ -52,13 +52,13 @@ Update the default composer.json: .. code-block:: bash - composer dev-tools update-composer-json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" update-composer-json Update a specific file: .. code-block:: bash - composer dev-tools update-composer-json --file=composer.json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" update-composer-json --file=composer.json Behavior --------- diff --git a/docs/commands/wiki.rst b/docs/commands/wiki.rst index 609088eacc..a700d6bb6b 100644 --- a/docs/commands/wiki.rst +++ b/docs/commands/wiki.rst @@ -14,8 +14,8 @@ Usage .. code-block:: bash - composer wiki - composer dev-tools wiki -- [options] + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki [options] vendor/bin/dev-tools wiki [options] Options @@ -54,25 +54,25 @@ Generate wiki docs: .. code-block:: bash - composer wiki + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki Generate to custom directory: .. code-block:: bash - composer dev-tools wiki -- --target=.dev-tools/wiki-generated + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki --target=.dev-tools/wiki-generated Initialize a legacy wiki submodule explicitly: .. code-block:: bash - composer wiki --init + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki --init Generate without cache: .. code-block:: bash - composer wiki --no-cache + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki --no-cache Exit Codes --------- diff --git a/docs/configuration/overriding-defaults.rst b/docs/configuration/overriding-defaults.rst index dfe50aaeff..dc138a5d81 100644 --- a/docs/configuration/overriding-defaults.rst +++ b/docs/configuration/overriding-defaults.rst @@ -45,14 +45,6 @@ Commands and Their Configuration Files * - ``docs`` - ``docs/`` or another path passed with ``--source`` - The selected guide source must exist locally. - * - ``skills`` - - ``.agents/skills/`` - - Creates missing local links to packaged skills and preserves existing - non-symlink directories. - * - ``agents`` - - ``.agents/agents/`` - - Creates missing local links to packaged project agents and preserves - existing non-symlink directories. * - ``dev-tools:sync`` - Consumer repository files - Works directly against local project files such as ``composer.json`` and @@ -144,7 +136,7 @@ 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 +``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --show-shadow-dependencies`` when you want to audit those findings and decide whether a package should keep, document, or remove a direct dependency. @@ -154,8 +146,6 @@ What Is Not Overwritten Automatically - existing workflow files in ``.github/workflows/``; - an existing ``.editorconfig``; - an existing ``.github/dependabot.yml``; -- an existing non-symlink directory inside ``.agents/skills/``; -- an existing non-symlink directory inside ``.agents/agents/``; - an existing ``.github/wiki`` directory or submodule. .. tip:: diff --git a/docs/configuration/tooling-defaults.rst b/docs/configuration/tooling-defaults.rst index 69ade03233..719228f8ce 100644 --- a/docs/configuration/tooling-defaults.rst +++ b/docs/configuration/tooling-defaults.rst @@ -28,14 +28,12 @@ create them on day one. * - ``.editorconfig`` - ``dev-tools:sync`` - Copied into the consumer root when missing. - * - ``.agents/skills/*`` - - ``skills`` and ``dev-tools:sync`` - - Packaged agent skill directories exposed to consumer repositories - through symlinks. - * - ``.agents/agents/*`` - - ``agents`` and ``dev-tools:sync`` - - Packaged project-agent prompt directories exposed to consumer - repositories through symlinks. + * - ``grumphp.yml`` + - Git hooks and explicit sync + - Copied locally when missing; local GrumPHP takes precedence over CPX. + * - ``resources/skills/fast-forward-*`` + - LLM Skills + - Independently installs owned procedural packages. Generated and Cache Directories ------------------------------- @@ -50,10 +48,7 @@ Generated and Cache Directories from normal sync and the direct Wiki workflow. - ``.github/wiki/`` remains the standalone ``wiki`` command's default target; legacy consumers may also use it as an explicitly initialized Wiki submodule. -- ``.agents/skills/`` contains symlinked packaged skills or consumer-owned - directories kept in place by the ``skills`` command. -- ``.agents/agents/`` contains symlinked packaged project agents or - consumer-owned directories kept in place by the ``agents`` command. +- ``.agents/skills/`` contains skills installed by LLM Skills. - ``.dev-tools/cache/phpdoc``, ``.dev-tools/cache/phpunit``, ``.dev-tools/cache/rector``, and ``.dev-tools/cache/php-cs-fixer/.php-cs-fixer.cache`` store repository-local diff --git a/docs/faq.rst b/docs/faq.rst index 901ef1454a..f2fd6bae2a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,48 +1,32 @@ FAQ === -Why does ``composer dev-tools docs`` fail in a new package? ------------------------------------------------------------ +How can I use DevTools without require-dev? +------------------------------------------ -Because the command requires a ``docs/`` directory. Create at least -``docs/index.rst`` before running ``docs`` or ``reports``. +Use CPX with ``--skip-local`` and an exact published version. Follow +:doc:`getting-started/installation` and :doc:`usage/dev-tools-2-migration`. -Why did my ``composer.json`` change after installing the package? ------------------------------------------------------------------ +Does installation synchronize repository files? +---------------------------------------------- -The Composer plugin runs ``dev-tools:sync`` after install and update. -That command adds the ``dev-tools`` scripts, removes deprecated -DevTools-managed ``extra.grumphp.config-default-path`` values, and refreshes -the packaged Git hooks. +No. Version 2 removes Composer plugin activation and install/update hooks. +Run ``dev-tools:sync`` explicitly after reviewing its preview. -Do I always need to run ``dev-tools:sync`` manually? ----------------------------------------------------- +How do I install skills and role prompts? +---------------------------------------- -Usually no. The plugin already runs it after ``composer install`` and -``composer update``. Manual sync is most useful when plugins were disabled or -after upgrading ``fast-forward/dev-tools`` and wanting to refresh consumer -automation. That flow also runs ``skills`` and ``agents`` so packaged skill -links and packaged project-agent links are kept up to date. +LLM Skills installs the ``fast-forward-*`` procedural packages independently. +See :doc:`usage/syncing-packaged-skills`. Custom role prompts remain +repository-owned; ``skills`` and ``agents`` are removed from DevTools. -When should I run ``composer dev-tools skills`` manually? ---------------------------------------------------------- - -Run it when you want to refresh ``.agents/skills`` without rerunning the full -consumer sync flow, especially after upgrading ``fast-forward/dev-tools`` or -after deleting a packaged skill link locally. - -When should I run ``composer agents`` manually? ------------------------------------------------ - -Run it when you want to refresh ``.agents/agents`` without rerunning the full -consumer sync flow, especially after upgrading ``fast-forward/dev-tools`` or -after deleting a packaged project-agent link locally. - -Why does ``code-style`` touch ``composer.lock``? ------------------------------------------------- +Does code-style update dependencies? +----------------------------------- -Because ``FastForward\DevTools\Console\Command\CodeStyleCommand`` always runs -``composer update --lock --quiet`` before Composer Normalize and ECS. +No. Code-style runs Composer Normalize as a plugin from the isolated runtime +root against the absolute consumer manifest with ``--no-update-lock``, then +runs ECS. It does not run ``composer update --lock``. Use ``--fix`` to permit +formatting changes; no standalone Composer Normalize binary is assumed. Where did ``.docheader`` come from? ----------------------------------- @@ -58,7 +42,7 @@ Use the ``tests`` command with ``--filter``: .. code-block:: bash - composer dev-tools tests -- --filter=SyncCommandTest + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --filter=SyncCommandTest Why can my tests double final classes? -------------------------------------- @@ -91,20 +75,6 @@ gitlink and matching ``.gitmodules`` stanza explicitly, preserving the separate Wiki repository and any other submodules. ``wiki --init`` remains available as an explicit legacy option. -What happens if ``.agents/skills/my-skill`` already exists? ------------------------------------------------------------ - -If that path is a real directory instead of a symlink, the ``skills`` command -preserves it and skips link creation. Broken symlinks are repaired, but -consumer-owned directories are not overwritten automatically. - -What happens if ``.agents/agents/my-agent`` already exists? ------------------------------------------------------------ - -If that path is a real directory instead of a symlink, the ``agents`` command -preserves it and skips link creation. Broken symlinks are repaired, but -consumer-owned directories are not overwritten automatically. - How do I override only one tool without forking the whole package? ------------------------------------------------------------------ diff --git a/docs/getting-started/index.rst b/docs/getting-started/index.rst index 0c0b647265..703b8014b8 100644 --- a/docs/getting-started/index.rst +++ b/docs/getting-started/index.rst @@ -7,13 +7,11 @@ knowing the internal architecture. By the end of this section you will know how to: -- install the Composer plugin safely; -- run the generated commands through Composer or directly through the binary; +- select an isolated CPX runtime; +- run commands through CPX or the checked-out binary; - prepare the minimum ``docs/`` structure required by the documentation pipeline; -- understand which files ``dev-tools:sync`` creates or updates, including - packaged skills under ``.agents/skills`` and packaged project agents under - ``.agents/agents``. +- understand explicit repository sync and independent LLM Skills installation. .. toctree:: :maxdepth: 1 diff --git a/docs/getting-started/installation.rst b/docs/getting-started/installation.rst index ee3fd12a15..9f2e70f66d 100644 --- a/docs/getting-started/installation.rst +++ b/docs/getting-started/installation.rst @@ -1,111 +1,28 @@ Installation ============ -FastForward DevTools is installed as a development dependency and exposed -through Composer as both a plugin and a local binary. +DevTools 2 is a standalone CLI. CPX keeps its runtime outside the consumer's +Composer dependency graph. Install CPX following https://github.com/laravel/cpx +and select PHP compatible with the chosen DevTools release and bundled tools. +Install the consumer's own dependencies before executing tests. -Requirements ------------- - -- PHP 8.3 or newer. -- Composer 2. -- A project with PSR-4 autoload definitions in ``composer.json``. -- Git, for repository metadata and workflow synchronization. - -Install with Composer ---------------------- - -.. code-block:: bash - - composer require --dev fast-forward/dev-tools - -What happens after installation? --------------------------------- - -When Composer finishes the install or update, the package performs the -following steps: - -1. Composer loads ``FastForward\DevTools\Composer\Plugin``. -2. The plugin exposes commands through - ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider``. -3. Composer triggers ``vendor/bin/dev-tools dev-tools:sync`` after install and - update. -4. ``dev-tools:sync`` adds or refreshes the ``dev-tools`` and - ``dev-tools:fix`` scripts in the consumer ``composer.json``. -5. ``dev-tools:sync`` removes deprecated DevTools-managed - ``extra.grumphp.config-default-path`` entries, synchronizes funding - metadata, copies automation assets such as workflow stubs, - ``.editorconfig``, and ``.github/dependabot.yml``, and refreshes - ``.gitignore``, ``.gitattributes``, the project license, and packaged Git - hooks that prefer a project-local ``grumphp.yml`` override and otherwise - use a project-relative reference to the active packaged DevTools - ``grumphp.yml`` path. -6. In normal mode, ``dev-tools:sync`` generates local Markdown documentation - in ``.dev-tools/wiki-generated`` without initializing a Wiki submodule. -7. ``dev-tools:sync`` runs ``gitignore`` to merge canonical ignore rules into - the consumer project. -8. ``dev-tools:sync`` runs ``skills`` and ``agents`` to create or repair - packaged links inside ``.agents/skills`` and ``.agents/agents`` when sync - runs in normal mode rather than preview/check mode. - -Wiki publication is separate from local synchronization. The synchronized -``wiki.yml`` wrapper calls ``wiki-direct.yml`` to generate documentation with -read-only permissions on pull requests and publish directly to the separate -Wiki repository from ``main``. Its isolated, pinned DevTools checkout provides -the renderer without changing the consumer's ``composer.json``, -``composer.lock``, or plugin allowlist. - -The synchronized ``wiki-maintenance.yml`` is a manual-only migration notice. -Existing callers of the legacy reusable preview and maintenance workflows -remain supported until they migrate. The explicit ``wiki --init`` option also -remains available for legacy submodule users; normal sync does not use it. - -First commands to try ---------------------- - -After installation, these are the most useful sanity checks: - -.. code-block:: bash - - composer skills - composer agents - composer tests - composer docs - composer dev-tools - -If Composer argument forwarding becomes awkward, call the binary directly: +Assign an exact published 2.x numeric version to ``DEV_TOOLS_VERSION``. This +variable does not assert that a particular release is already published. .. code-block:: bash - vendor/bin/dev-tools tests --filter=SyncCommandTest + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" list + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests -If you want to verify the packaged skills or project agents on their own, run: +Run from the consumer root or use ``--working-dir``. ``--skip-local`` avoids +a project-local installation. No Composer commands or automatic install/update +hooks are registered. Repository setup is explicit: .. code-block:: bash - vendor/bin/dev-tools skills - vendor/bin/dev-tools agents - -When manual sync is useful --------------------------- - -If the package was installed with Composer plugins disabled, or if you want to -refresh consumer automation after upgrading this package, run: - -.. code-block:: bash - - composer dev-tools:sync - -Or call the binary explicitly: - -.. code-block:: bash - - vendor/bin/dev-tools dev-tools:sync - -.. important:: + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync - The ``docs`` and ``reports`` commands require a ``docs/`` directory. If - your package does not have one yet, create it before running those commands. - The ``skills`` and ``agents`` commands create ``.agents/skills`` and - ``.agents/agents`` when needed, but they do not overwrite existing - non-symlink directories inside those trees. +Install skills separately through :doc:`../usage/syncing-packaged-skills`. +For DevTools development itself use ``composer install`` followed by +``php bin/dev-tools tests`` or the root ``composer dev-tools`` script. diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst index 71240525a4..11f2828c69 100644 --- a/docs/getting-started/quickstart.rst +++ b/docs/getting-started/quickstart.rst @@ -1,82 +1,17 @@ Quickstart ========== -This walkthrough is the fastest way to get a new library into a healthy state. - -1. Install the package. -2. Create a guide directory if the repository will publish guides. -3. Synchronize shared automation, packaged skills, and packaged agents. -4. Run the focused commands once. -5. Run the full suite before opening a pull request. - -Optional Guide Setup --------------------- - -If the repository will publish guides, a tiny starting page is enough for the -first successful ``docs`` run. - -Create the directory: - -.. code-block:: bash - - mkdir -p docs - -Create ``docs/index.rst`` with content such as: - -.. code-block:: rst - - Documentation - ============= - - Welcome to the project documentation. - -Repositories that only ship PHP code can skip this step and still generate API -documentation. Repositories that only ship guides can also use the same -``docs`` command even without PSR-4 source paths. - -Run the First Commands ----------------------- - -Once the package is installed, run: - -.. code-block:: bash - - composer dev-tools:sync - composer skills - composer agents - composer tests - composer docs - composer dev-tools - -What Each Command Proves ------------------------- - -- ``composer dev-tools:sync`` proves the consumer repository can receive the - shared scripts, automation assets, packaged skills, and packaged agents - during onboarding. -- ``composer skills`` proves the packaged skill set can be linked - safely into ``.agents/skills`` without copying files into the consumer - repository. -- ``composer agents`` proves the packaged project-agent prompts can be linked - safely into ``.agents/agents`` without copying files into the consumer - repository. -- ``composer tests`` proves the packaged or local PHPUnit - configuration can execute the current test suite, or skip gracefully when - the repository intentionally has no runnable PHPUnit surface yet. -- ``composer docs`` proves the PSR-4 source paths and the guide - directory are usable by phpDocumentor, whichever of those surfaces the - repository actually provides. -- ``composer dev-tools`` proves the complete pipeline can run in the expected - order. - -When You Want Automatic Fixes ------------------------------ - -If you want the tools to modify files for you, run: +Follow :doc:`installation` to select an exact published runtime version. .. code-block:: bash - composer dev-tools:fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards -That is the quickest way to let Rector, PHPDoc automation, and ECS fix what -they can before you start manual cleanup. +Use ``--fix`` to allow quality tools to modify files, ``--json`` for structured +results and ``--working-dir`` to select a project. Create ``docs/index.rst`` +before building guides. Preview ``dev-tools:sync --dry-run`` and review the +diff before applying synchronization. Skills are installed separately through +:doc:`../usage/syncing-packaged-skills`; version 1 consumers should follow +:doc:`../usage/dev-tools-2-migration`. diff --git a/docs/index.rst b/docs/index.rst index 43af70897e..22367c7480 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Documentation If you maintain Fast Forward libraries, continue with :doc:`advanced/index`, :doc:`api/index`, and :doc:`internals/index` to - understand how the Composer plugin, PHPUnit extension, and reusable + understand how the standalone CLI, PHPUnit extension, and reusable workflows fit together. .. container:: col-lg-5 text-center diff --git a/docs/internals/architecture.rst b/docs/internals/architecture.rst index 1bb0a7f5b0..28c38128ff 100644 --- a/docs/internals/architecture.rst +++ b/docs/internals/architecture.rst @@ -1,103 +1,23 @@ Architecture and Command Lifecycle -================================= - -The package has two connected execution models: local command execution and -consumer repository synchronization. - -Local Command Lifecycle ------------------------ - -1. ``bin/dev-tools`` loads ``bin/dev-tools.php``. -2. ``bin/dev-tools.php`` prefers the consumer ``vendor/autoload.php`` and - falls back to the package autoloader. -3. ``FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)`` - resolves the shared application container from - ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. -4. ``FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader`` - lazily discovers ``#[AsCommand]`` classes and resolves them from that - container. -5. ``standards`` is used as the default command when no explicit command name - is given. -6. ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider`` only - adapts the same application command set for Composer plugin integration. - -Consumer Synchronization Lifecycle ----------------------------------- - -1. A library installs ``fast-forward/dev-tools`` as a development dependency. -2. Composer loads ``FastForward\DevTools\Composer\Plugin``. -3. The plugin exposes commands through - ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider``. -4. After ``composer install`` or ``composer update``, the plugin runs - ``vendor/bin/dev-tools dev-tools:sync``. -5. ``FastForward\DevTools\Console\Command\SyncCommand`` updates - ``composer.json`` scripts, funding metadata, workflow stubs, - ``.editorconfig``, ``dependabot.yml``, ``.gitignore``, - ``.gitattributes``, the project license, and packaged Git hooks. -6. In normal mode, ``dev-tools:sync`` also runs - ``wiki --target=.dev-tools/wiki-generated``, ``skills``, and ``agents`` - to generate local API Markdown without a wiki submodule and - synchronize packaged links into ``.agents/skills`` and - ``.agents/agents``. -7. In ``--dry-run``, ``--check``, and ``--interactive`` modes, ``wiki``, - ``skills``, and ``agents`` are skipped because they do not yet expose - non-destructive verification paths. -8. ``FastForward\DevTools\Sync\PackagedDirectorySynchronizer`` creates - missing links, repairs broken ones, and preserves consumer-owned - directories for both synchronization flows. - -Documentation Pipeline ----------------------- - -1. ``FastForward\DevTools\Console\Command\DocsCommand`` reads PSR-4 paths from - ``composer.json`` via ``ComposerJsonInterface``. -2. It generates a temporary ``phpdocumentor.xml`` file in the configured cache - directory. -3. phpDocumentor builds API pages from those PSR-4 paths. -4. phpDocumentor also builds the guide from the selected ``docs/`` source - directory. -5. ``FastForward\DevTools\Console\Command\ReportsCommand`` combines that - documentation build with PHPUnit coverage generation. - -Dependency Injection --------------------- - -``DevToolsServiceProvider`` builds the shared application container used by -``ContainerFactory::get(DevTools::class)``. Most commands receive collaborators through -constructor injection once resolved by that container, while command discovery -itself stays lazy through ``DevToolsCommandLoader``. - -The provider wires the command runtime by concern rather than by one flat -command list: - -.. list-table:: - :header-rows: 1 - - * - Concern - - Services - * - ``FastForward\DevTools\Process\ProcessBuilderInterface`` - - ``ProcessBuilderInterface`` and ``ProcessQueueInterface`` build and - 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 - packaged resources. - * - ``Console bootstrapping`` - - ``CommandLoaderInterface`` resolves lazy command loading, and - ``Composer\Plugin\Capability\CommandProvider`` exposes the same - command set to Composer. - * - ``External changelog and Git`` - - The independent Changelog package owns fragment parsing, history, - versioning and publication policy, with its own CLI and workflow. - ``GitClientInterface`` remains available for DevTools Git operations. - * - ``Synchronization helpers`` - - Git ignore, Git attributes, license, resource diffing, and coverage - summary services support consumer sync and reporting workflows. - * - ``Shared infrastructure`` - - ``LoggerInterface``, ``ClockInterface``, - ``RuntimeEnvironmentInterface``, and Twig's ``LoaderInterface`` - provide reusable runtime infrastructure, including centralized checks - for GitHub Actions, generic CI, Composer test runs, and truthy - environment flags. +================================== + +CPX installs the chosen DevTools runtime separately. ``bin/dev-tools`` loads +its own autoloader and resolves ``Console\DevTools`` through the shared +container. ``DevToolsCommandLoader`` lazily discovers Symfony commands. +Commands read consumer metadata, local overrides, source and artifacts from +the working directory, while tool binaries and packaged defaults come from +the DevTools installation. ``standards`` orchestrates the default pipeline. + +There is no Composer plugin or automatic install/update hook. +``dev-tools:sync`` explicitly manages repository metadata, workflow callers, +defaults and Git hooks. LLM Skills owns skill installation and provenance; +the old skill/agent link synchronizer is removed. + +DevTools may require extracted config, container or other runtime libraries. +Those libraries run DevTools through CPX for development, avoiding a reverse +Composer dependency. Separate resolution does not guarantee incompatible PHP +APIs can coexist in one process. Preserve actual consumer test dependencies +and validate the test/runtime boundary. + +Keep process policy centralized in ``ProcessQueue`` configurators. See +:doc:`../usage/dev-tools-2-migration` for staged extraction and ownership. diff --git a/docs/internals/release-publishing.rst b/docs/internals/release-publishing.rst index 675809d24d..dc810bfbf3 100644 --- a/docs/internals/release-publishing.rst +++ b/docs/internals/release-publishing.rst @@ -43,7 +43,7 @@ Run the project gate against the reviewed candidate: .. code-block:: bash composer install - composer dev-tools + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards When guides or generated output change, inspect reports and wiki artifacts as well. Test history-writing Changelog commands only in disposable checkouts, diff --git a/docs/links/dependencies.rst b/docs/links/dependencies.rst index e10b7f7b6e..17f1f094d6 100644 --- a/docs/links/dependencies.rst +++ b/docs/links/dependencies.rst @@ -12,11 +12,11 @@ Runtime and Composer Integration * - Package - Why it matters - * - ``composer/composer`` and ``composer-plugin-api`` - - Provide the Composer plugin API, command integration, and script hooks. - * - ``phpro/grumphp-shim`` - - Supplies the default GrumPHP executable and configuration referenced by - ``dev-tools:sync``. + * - ``symfony/console`` + - Provides the standalone command runtime; no Composer plugin is registered. + * - External ``phpro/grumphp-shim:2.26.0`` + - Hooks use CPX when local GrumPHP is absent. It is not a DevTools runtime + dependency; explicit sync supplies durable local configuration. QA and Refactoring ------------------ diff --git a/docs/links/live-reports.rst b/docs/links/live-reports.rst index a4c2030d87..f51dcd67e7 100644 --- a/docs/links/live-reports.rst +++ b/docs/links/live-reports.rst @@ -14,5 +14,5 @@ If you need the same artifacts locally, run: .. code-block:: bash - composer dev-tools reports - composer dev-tools wiki + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index 0afda28b3e..fc248701c8 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -28,8 +28,8 @@ Runs PHPUnit with the resolved ``phpunit.xml``. .. code-block:: bash - composer tests - composer tests -- --filter=EventTracerTest + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --filter=EventTracerTest Important details: @@ -59,13 +59,13 @@ Analyzes missing, unused, misplaced, and outdated Composer dependencies. .. code-block:: bash - composer dependencies - composer dependencies --max-outdated=10 - composer dependencies --max-outdated=-1 - composer dependencies --dev - composer dependencies --dump-usage=symfony/console - composer dependencies --show-shadow-dependencies - composer dependencies --upgrade --dev + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --max-outdated=10 + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --max-outdated=-1 + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --dev + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --dump-usage=symfony/console + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --show-shadow-dependencies + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dependencies --upgrade --dev Important details: @@ -101,10 +101,10 @@ Analyzes code metrics with PhpMetrics. .. code-block:: bash - composer metrics - composer metrics --target=.dev-tools/metrics - FAST_FORWARD_WORKSPACE_DIR=.artifacts composer metrics - composer --working-dir=packages/example metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics --target=.dev-tools/metrics + FAST_FORWARD_WORKSPACE_DIR=.artifacts cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" metrics + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" --working-dir=packages/example metrics Important details: @@ -136,12 +136,12 @@ Runs Composer Normalize and ECS. .. code-block:: bash - composer code-style - composer code-style --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" code-style --fix Important details: -- it always executes ``composer update --lock --quiet`` first; +- it does not refresh the lockfile or run dependency updates first; - without ``--fix``, Composer Normalize runs in ``--dry-run`` mode; - ECS uses local ``ecs.php`` when present, otherwise the packaged fallback; - progress output is disabled by default; ``--progress`` re-enables it for text @@ -156,7 +156,7 @@ Runs Rector against the current project. .. code-block:: bash - composer refactor --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" refactor --fix Important details: @@ -176,8 +176,8 @@ Coordinates PHP-CS-Fixer and a focused Rector pass for missing method PHPDoc. .. code-block:: bash - composer phpdoc - composer phpdoc --fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc --fix Important details: @@ -204,7 +204,7 @@ Builds the HTML documentation site with phpDocumentor. .. code-block:: bash - composer docs --source=docs --target=.dev-tools + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs --source=docs --target=.dev-tools Important details: @@ -228,7 +228,7 @@ Builds Markdown API pages for a GitHub wiki. .. code-block:: bash - composer wiki + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki Important details: @@ -251,7 +251,7 @@ Runs the documentation and test-report pipeline used by GitHub Pages. .. code-block:: bash - composer reports + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports Important details: @@ -278,39 +278,11 @@ Important details: coverage instead; - it is the reporting stage used by ``standards``. -``skills`` ----------- - -Synchronizes packaged agent skills into the consumer repository. - -.. code-block:: bash - - composer skills - -Important details: - -- it verifies the packaged ``.agents/skills`` directory before doing any work; -- it creates the consumer ``.agents/skills`` directory when missing; -- it creates missing symlinks and repairs broken ones; -- it preserves an existing non-symlink directory instead of overwriting it. - -``agents`` ----------- - -Synchronizes packaged project-agent prompts into the consumer repository. - -.. code-block:: bash - - composer agents - -Important details: +Skill installation +------------------ -- it verifies the packaged ``.agents/agents`` directory before doing any work; -- it creates the consumer ``.agents/agents`` directory when missing; -- it creates missing symlinks and repairs broken ones; -- it preserves an existing non-symlink directory instead of overwriting it; -- it uses the same generic synchronization rules as ``skills`` through - ``FastForward\\DevTools\\Sync\\PackagedDirectorySynchronizer``. +Use LLM Skills; see :doc:`../usage/syncing-packaged-skills`. +DevTools no longer installs skill or agent links. ``funding`` ----------- @@ -319,9 +291,9 @@ Synchronizes supported funding metadata between Composer and GitHub formats. .. code-block:: bash - composer funding - composer funding --dry-run - composer funding --check + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" funding --check Important details: @@ -341,9 +313,9 @@ Generates managed CODEOWNERS files from repository metadata. .. code-block:: bash - composer codeowners - composer codeowners --dry-run - composer codeowners --interactive + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" codeowners --interactive Important details: @@ -363,7 +335,7 @@ Synchronizes consumer-facing automation and defaults. .. code-block:: bash - composer dev-tools:sync + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync Important details: @@ -379,13 +351,9 @@ Important details: - it calls ``gitignore`` to merge the canonical .gitignore with the project's .gitignore; - it calls ``gitattributes`` to manage export-ignore rules in .gitattributes; -- it refreshes packaged Git hooks that prefer a local ``grumphp.yml`` - override and otherwise use a project-relative reference to the active - packaged DevTools ``grumphp.yml`` path resolved when sync installs them; -- it calls ``skills`` so ``.agents/skills`` contains links to the packaged - skill set; -- it calls ``agents`` so ``.agents/agents`` contains links to the packaged - project-agent set. +- it copies a missing local ``grumphp.yml`` and refreshes hooks that prefer + local GrumPHP, otherwise CPX ``phpro/grumphp-shim:2.26.0``; temporary + runtime/cache paths are not stored in hooks; ``gitattributes`` ---------------- @@ -394,7 +362,7 @@ Manages .gitattributes export-ignore rules for leaner Composer package archives. .. code-block:: bash - composer gitattributes + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitattributes Important details: @@ -415,7 +383,7 @@ Merges and synchronizes .gitignore files. .. code-block:: bash - composer gitignore --source=/path/to/source/.gitignore --target=/path/to/target/.gitignore + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" gitignore --source=/path/to/source/.gitignore --target=/path/to/target/.gitignore Important details: @@ -434,7 +402,7 @@ Generates a LICENSE file from composer.json license information. .. code-block:: bash - composer license + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" license Important details: @@ -453,7 +421,7 @@ Copies packaged or local resources into the consumer repository. .. code-block:: bash - composer copy-resource --source --target --overwrite + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" copy-resource --source --target --overwrite Important details: @@ -470,7 +438,7 @@ Installs packaged Fast Forward Git hooks. .. code-block:: bash - composer git-hooks + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" git-hooks Important details: @@ -489,7 +457,7 @@ Updates composer.json with Fast Forward dev-tools scripts and metadata. .. code-block:: bash - composer update-composer-json --file=composer.json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" update-composer-json --file=composer.json Important details: @@ -500,3 +468,11 @@ Important details: - if the target file does not exist, exits silently with code 0; - existing scripts with the same name are overwritten; - preserves consumer-owned GrumPHP settings. + +Normalization runtime +--------------------- + +Composer Normalize executes as a plugin from the isolated runtime root against +the consumer's absolute manifest path with ``--no-update-lock``. The consumer +does not need that plugin installed. An explicitly requested dependency upgrade +may still update its Composer lockfile; normalization itself does not. diff --git a/docs/running/unified-command.rst b/docs/running/unified-command.rst index 1de2b07a55..7d788de137 100644 --- a/docs/running/unified-command.rst +++ b/docs/running/unified-command.rst @@ -4,7 +4,7 @@ The Unified Command The default command of the local application is ``standards``. In practice, that means the following entry points all reach the same orchestration flow: -- ``composer dev-tools`` +- ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` - ``vendor/bin/dev-tools`` - ``vendor/bin/dev-tools standards`` @@ -28,7 +28,7 @@ To allow the tools to modify files, use one of the following entry points: .. code-block:: bash - composer dev-tools:fix + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix vendor/bin/dev-tools --fix The flag mainly affects ``refactor``, ``phpdoc``, and ``code-style``. The diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 09836f5ce3..9dca9f5751 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -40,7 +40,7 @@ Scope: local and CI. Symptoms: -- ``composer dev-tools`` fails because a binary is missing; +- ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` fails because a binary is missing; - CI installs a dependency set that differs from the local machine; - Composer reports that ``composer.lock`` is not compatible with ``composer.json``. @@ -67,13 +67,12 @@ review the dependency diff: composer update --lock git diff composer.lock -Reusable workflow consumers have one extra fallback path: the shared -``setup-composer`` action first looks for a consumer-local -``vendor/bin/dev-tools`` and otherwise exposes a ``dev-tools`` wrapper backed -by the checked-out ``.dev-tools-actions`` source. If a workflow-only consumer -fails before a ``dev-tools`` command starts, confirm that the job ran the -shared bootstrap and that the upstream ``.dev-tools-actions`` checkout includes -the DevTools package source needed for the fallback runtime. +Reusable workflow consumers use a separately resolved CPX installation. +If a command fails before startup, check that CPX is available, the selected +exact DevTools version is published, its PHP requirements are satisfied and +``runtime --json --no-ansi --no-interaction`` returns valid binary/autoload +paths. Install the consumer's own dependencies separately. DevTools itself +uses checked-out source during producer CI. Branch Protection or Bot Commit Blocks -------------------------------------- @@ -163,7 +162,7 @@ Recovery: .. code-block:: bash - composer dev-tools -- --no-interaction + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards -- --no-interaction When calling lower-level tools directly, use their non-interactive flags and provide required values through environment variables or workflow inputs. @@ -184,7 +183,7 @@ 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 +available. A warning printed by the top-level ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` 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. @@ -223,8 +222,8 @@ Recovery: .. code-block:: bash - composer dev-tools tests - composer dev-tools tests -- --coverage=.dev-tools/coverage + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --coverage=.dev-tools/coverage Add focused tests for the changed behavior before lowering thresholds. If local coverage differs from CI, compare PHP, Xdebug or PCOV, and PHPUnit versions. @@ -236,7 +235,7 @@ Scope: local and CI. Symptoms: -- ``composer dev-tools docs`` fails during API documentation generation; +- ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs`` fails during API documentation generation; - generated docs are missing classes or links; - PHPDoc validation fails after a source change. @@ -250,8 +249,8 @@ Recovery: .. code-block:: bash - composer dev-tools phpdoc - composer dev-tools docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" phpdoc + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs Fix source PHPDoc first, then regenerate docs. Avoid editing generated API output directly; it should reflect source files and templates. diff --git a/docs/usage/common-workflows.rst b/docs/usage/common-workflows.rst index e78b070365..a735bc31bd 100644 --- a/docs/usage/common-workflows.rst +++ b/docs/usage/common-workflows.rst @@ -10,66 +10,58 @@ Most day-to-day work falls into one of the flows below. - Command - What happens * - Check everything before a pull request - - ``composer dev-tools`` + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` - Runs ``refactor``, ``phpdoc``, ``code-style``, and ``reports`` in order. * - Auto-fix what can be changed safely - - ``composer dev-tools:fix`` + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix`` - Passes ``--fix`` through the supported subcommands. * - Refresh only test results - - ``composer tests`` + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests`` - Runs PHPUnit with the resolved ``phpunit.xml``. * - Refresh only the documentation site - - ``composer docs`` + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs`` - Runs phpDocumentor using PSR-4 namespaces and the ``docs/`` guide. * - Record a notable pull-request change - ``cpx --skip-local fast-forward/changelog add "Describes the change." --no-interaction`` - Creates one independent change fragment using the external CPX runtime. - * - Refresh packaged agent skills only - - ``composer skills`` - - Creates or repairs symlinks in ``.agents/skills``. - * - Refresh packaged project agents only - - ``composer agents`` - - Creates or repairs symlinks in ``.agents/agents``. + * - Refresh procedural skills + - ``composer skills:update --dry-run`` then ``composer skills:update`` + - LLM Skills previews and installs the configured skill sources. * - Trigger a rigorous review pass for a ready pull request - Mark the pull request ready for review or run ``review.yml`` manually - Posts a deterministic review brief that points maintainers to the - ``review-guardian`` agent and ``pull-request-review`` skill. + ``review-guardian`` agent and ``fast-forward-pull-request-review`` skill. * - Inspect the next release - ``cpx --skip-local fast-forward/changelog status --source=tags --no-interaction`` - Plans from pending fragments; the managed version PR consolidates them for review. Merging the reviewed managed version PR automatically publishes its tag and GitHub Release at the approved commit. * - Publish local automation defaults into a consumer repository - - ``composer dev-tools:sync`` - - Updates scripts, copies missing automation assets, and refreshes - packaged skills and packaged agents. + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync`` + - Updates metadata, scripts and managed repository assets explicitly. * - Regenerate wiki pages - - ``composer wiki`` + - ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki`` - Builds Markdown API pages in ``.github/wiki`` by default; normal sync explicitly uses ``.dev-tools/wiki-generated`` without a submodule. Choose the Right Entry Point ---------------------------- -Use ``composer ...`` when you want the same command vocabulary across -repositories. - -Use ``vendor/bin/dev-tools ...`` when you need to pass several options and want -to avoid Composer's ``--`` forwarding rules. +Use pinned CPX commands for consumers. ``php bin/dev-tools`` runs the checked-out +DevTools source. Install instructions independently with LLM Skills. A Safe Beginner Routine ----------------------- -1. Run ``composer tests``. +1. Run ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests``. 2. Run ``cpx --skip-local fast-forward/changelog add`` with a description when the branch introduces a notable change (requires CPX and PHP 8.5); then run ``cpx --skip-local fast-forward/changelog check``. -3. Run ``composer skills`` if you changed packaged consumer skills. -4. Run ``composer agents`` if you changed packaged project-agent prompts. -5. Run ``composer docs`` if you changed guides or public APIs. -6. Run ``composer dev-tools:fix`` when you want automated help. -7. Run ``composer dev-tools`` before pushing. +3. Preview LLM Skills updates when instruction sources change. +5. Run ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs`` if you changed guides or public APIs. +6. Run ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards --fix`` when you want automated help. +7. Run ``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" standards`` before pushing. .. tip:: diff --git a/docs/usage/dev-tools-2-migration.rst b/docs/usage/dev-tools-2-migration.rst new file mode 100644 index 0000000000..8a9444bdc0 --- /dev/null +++ b/docs/usage/dev-tools-2-migration.rst @@ -0,0 +1,189 @@ +DevTools 2 Migration and Extraction Strategy +============================================ + +Dependency model +---------------- + +Version 1 made DevTools a development dependency of libraries that DevTools +might itself need at runtime. A library's ``require-dev`` is not installed +transitively when another project installs that library, so this is not always +a literal Composer solver cycle. It still creates a development feedback loop: +released DevTools constraints, local component versions, plugin activation and +shared QA tooling can constrain or bootstrap each other. + +Version 2 separates three contracts: + +* Composer dependencies provide application/library PHP APIs. +* CPX provides the independently resolved DevTools executable and bundled tools. +* LLM Skills provides procedural instructions from an explicitly chosen owner. + +A released config/container library can therefore be required by DevTools while +its own repository invokes DevTools externally. Do not move DevTools into a +global Composer dependency of the component or install it back into the +consumer in CI. ``--skip-local`` makes the isolated execution policy explicit. + +A CPX process boundary does not isolate every class loaded by a nested tool. +PHPUnit tests, custom extensions, Rector rules and configuration imports can +still share APIs. Keep directly used test libraries in the consumer as needed +and test real compatibility; do not remove every development dependency just +because DevTools no longer needs to be one. + +Version 2 contract +------------------ + +* DevTools is a library with ``bin/dev-tools``, without a Composer plugin, + command provider, proxy commands or automatic post-install/update sync. +* Consumers select an exact published runtime version, for example + ``cpx --skip-local fast-forward/dev-tools:2.0.0 tests`` after that release + has been published and independently verified. +* ``runtime --json --no-ansi --no-interaction`` supplies actual binary, + autoload, package/project root and version paths to automation. +* CI uses the CPX runtime for consumers and the checked-out binary for DevTools + itself. The project's Composer installation stays separate. +* ``code-style`` checks normalize/ECS without a preliminary + ``composer update --lock``. Manifest normalization fixes remain explicit. +* ``dev-tools:sync`` remains an explicit operation for metadata, workflow + callers, repository defaults, hooks and local Wiki generation. +* Non-global ``self-update`` runs ``cpx update fast-forward/dev-tools``; global + installation retains ``composer global update``. Neither route changes a + consumer's exact reviewed runtime or skill-source pin. +* Composer Normalize runs as a plugin from the isolated runtime root against + the absolute consumer manifest with ``--no-update-lock``. +* Hooks prefer local GrumPHP, otherwise pinned CPX GrumPHP; explicit sync copies + local ``grumphp.yml`` without persisting temporary runtime/cache paths. +* LLM Skills replaces DevTools skill installation. ``skills`` and ``agents`` + commands and generic symlink synchronization are removed. Custom project + role prompts remain repository-owned. + +See :doc:`syncing-packaged-skills` for configuration, trust and update examples. +LLM Skills does not replace synchronization of arbitrary repository files. + +Publish first, migrate consumers second +--------------------------------------- + +The packaged workflow callers target ``@v2.0.0`` and pass the exact +``dev-tools-version: '2.0.0'`` runtime pin where supported. These templates are +prepared for the future release; do not copy or synchronize them into consumers +until the tag and package are published and independently verified. When +updating a consumer later, update both the reusable-workflow ref and runtime +version together. + +1. Validate DevTools independently against a clean consumer fixture: no + ``fast-forward/dev-tools`` dependency, independent Composer lockfile, + compatible PHP runtime, local configuration and actual test execution. + Check JSON runtime metadata, packaged resources and skill payloads outside + the development checkout. +2. Review the DevTools 2 implementation and release notes. Merge and publish + through the existing human-controlled release lifecycle. Verify the exact + tag/commit and Packagist availability before adopting a consumer pin. +3. Inventory each consumer's manifest/lockfile, plugin allowlist, Composer + scripts, ``vendor/fast-forward/dev-tools`` imports, PHPUnit extension paths, + hooks, workflow callers and skill/agent links. Refresh the inventory live; + do not assume only Fast Forward repositories consume the public package. +4. Open one migration PR per consumer. Replace DevTools commands with pinned + CPX or reviewed script wrappers. Remove its ``require-dev`` entry and only + its obsolete allowlist/configuration entries; regenerate the consumer lock + through Composer, preserving unrelated dependencies and custom scripts. +5. Configure LLM Skills with a pinned GitHub source for external tools and + Composer Trusted for libraries actually installed. Review the skill preview, + provenance and one-owner rule. Preserve local edited instructions. +6. Preview explicit sync. Compare workflows and hooks deliberately, preserve + Dependabot policy, and avoid broad overwrite adoption. Replace hard-coded + vendor runtime paths with supported runtime discovery. +7. Run focused quality commands and full CI on the exact consumer PR head. + Verify required status identities, coverage, reports and read-only Wiki + artifacts. Merge remains a maintainer decision. + +Begin with a small component such as clock, then config/container, then a +representative repository using coverage, custom configs and report publishing. +Broaden only after those pilots establish the real compatibility contract. +This implementation does not remove dependencies in other repositories or +publish any release. + +Continuous extraction +--------------------- + +Use one domain owner for PHP behavior, CLI, skill and release documentation. +Changelog already demonstrates the independent package/tool route. For each +future extraction: + +1. Define the narrow API/CLI and output contract; avoid a dependency from the + extracted runtime back to DevTools. +2. Move implementation, tests and its existing ``fast-forward-*`` skill to the + new owner. Keep its procedure in ``SKILL.md`` and avoid parallel copies. +3. Publish and verify the donor's archive, executable and skill payload. +4. Update DevTools to consume the released library when it needs PHP APIs, or + an external command when process isolation is preferable. An adapter may + preserve orchestration while responsibility moves out. +5. Update each consumer's source and version in a reviewed PR. Use Composer + Trusted only when the library is installed; otherwise use a pinned GitHub + source. Do not silently fall back to the latest GitHub payload if a trusted + installed package lacks it. +6. Remove obsolete implementation and sync phases only when their new owner + covers the actual asset. Skills alone cannot replace metadata/workflow sync. + +Keep product releases and consumer adoption independently reviewable. Runtime, +workflow and skill pins should refer to compatible verified releases; a +release-channel change is an explicit policy decision. Roll back a failed +consumer migration by reverting that PR's manifest/lock, workflow, hook and +skill configuration together. Preserve the previous pin and provenance so the +rollback does not depend on a moving branch. + +Proposed issue triage +--------------------- + +Snapshot: open DevTools issues read on 2026-10-20. These are proposals for later +maintainer review, not issue edits or closure claims. The #195, #197 and #367 +bodies were read; remaining groupings below use current issue titles and need +acceptance-criteria review before changing them. + +.. list-table:: + :header-rows: 1 + :widths: 18 26 56 + + * - Issues + - Proposed disposition + - Reason and remaining work + * - `#195 `_ + - Replace installation design + - Composer installer-paths and a required agents bundle conflict with the + external-tool model. Separate skill distribution from custom role prompts. + * - `#197 `_ + - Supersede shim prerequisite + - CPX addresses tool dependency isolation without a Box/Scoper release + pipeline. Retain explicit investigation of shared PHPUnit/public APIs. + * - `#367 `_ + - Retain, align adoption contract + - Independent docs/Wiki PHP library, CLI, skill and automation fit this + strategy. Preserve publication/manual-page/report ownership gates. + * - #336 + - Retain extraction, revise bootstrap + - Dedicated git-hooks ownership can remove another sync responsibility + after a real replacement exists; skills do not install hooks. + * - #238, #240 + - Retain, reconcile owners + - Release-safe refs and shared Actions remain necessary. Coordinate docs + automation with #367 instead of relocating the same domain twice. + * - #350, #351, #349, #348, #347, #144 + - Reassess scope and owner + - Dependency guidance, documentation skills, specs and workflow tools may + belong to independent skill/tool owners; avoid automatic new runtime deps. + * - #14, #8, #262, #265, #266, #267, #268, #269 + - Retain capability questions + - Static analysis, parallel tests and optional quality tools still require + evidence and focused design; CPX changes distribution, not their value. + * - #246, #248, #142, #163, #164 + - Revalidate against version 2 + - Matrix discovery, structured output, doctor, paths and process dispatch + need current-code acceptance review before declaring anything complete. + +Instruction hierarchy map +-------------------------- + +The current and target scoped instruction hierarchy both contain only root +``AGENTS.md``. Keep shared runtime, validation and publication boundaries there. +Update its skills navigation to ``resources/skills/fast-forward-*`` while +keeping package procedures in their ``SKILL.md`` entrypoints. No new child +contract or Child DOX Index is justified by moving payload directories alone. +Restore the root contract together with the payload paths if reverting this +migration. diff --git a/docs/usage/documentation-workflows.rst b/docs/usage/documentation-workflows.rst index 8c4d2f5966..d9db14359b 100644 --- a/docs/usage/documentation-workflows.rst +++ b/docs/usage/documentation-workflows.rst @@ -44,10 +44,10 @@ Common Examples .. code-block:: bash - composer docs + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" docs vendor/bin/dev-tools docs --source=docs --target=.dev-tools - composer wiki - composer reports + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" wiki + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" reports What Each Command Is For ------------------------ diff --git a/docs/usage/github-actions.rst b/docs/usage/github-actions.rst index 4ec6320eb8..bf8bb799b1 100644 --- a/docs/usage/github-actions.rst +++ b/docs/usage/github-actions.rst @@ -25,10 +25,9 @@ The automation model separates shared implementations from consumer triggers: * **Workflow action source checkout** inside the reusable workflows when they need local action implementations from ``.github/actions/``. The reusable workflow checks out the upstream ``php-fast-forward/dev-tools`` source into - a dedicated ``.dev-tools-actions`` workspace path. Jobs that use the shared - PHP bootstrap can then prefer the consumer-local ``vendor/bin/dev-tools`` - when it exists and otherwise install a deterministic fallback runtime from - that workflow-source checkout. + a dedicated ``.dev-tools-actions`` workspace path. Jobs using shared PHP bootstrap resolve a pinned CPX runtime for consumers. + DevTools itself executes its checked-out source binary. The action-source + checkout is not a consumer runtime dependency installation. Wrapper Workflows ----------------- @@ -42,19 +41,15 @@ Example of an inherited workflow: .. code-block:: yaml name: "Fast Forward Reports" - uses: php-fast-forward/dev-tools/.github/workflows/reports.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/reports.yml@v2.0.0 secrets: inherit -This approach ensures that all libraries in the ecosystem benefit from infrastructure updates without requiring manual changes to every repository. - -DevTools-owned wrappers intentionally follow the reviewed ``@main`` workflow -contract so centrally deployed fixes reach consumers. This is an explicit -shared-infrastructure update policy, distinct from pinning third-party -actions. A workflow SHA alone would not freeze the existing action-source -checkout, which also follows DevTools ``main``; a fully immutable migration -would need to version both surfaces together. The external Changelog caller -instead selects its published ``@v0`` channel; its update contract is documented -below. +Consumer wrappers select the full DevTools release tag ``@v2.0.0`` and a +compatible exact runtime version. Adopt those refs only after verifying the +published tag, workflow contents and Packagist package. Upgrades are reviewed +caller-ref and runtime-pin changes. Verify nested action-source checkout refs +too: freezing a caller alone does not freeze a moving nested checkout. The +external Changelog caller retains its independently selected ``@v0`` channel. The packaged wrappers currently include: @@ -76,13 +71,15 @@ For the protected-branch-safe preview and publish model, see The standard reusable workflows support workflow-only consumers without a local ``fast-forward/dev-tools`` Composer dependency. The shared -``setup-composer`` action prefers the -consumer ``vendor/bin/dev-tools`` when it exists and otherwise exposes a -``dev-tools`` wrapper backed by the checked-out ``.dev-tools-actions`` source. +``setup-composer`` action uses ``cpx --skip-local`` with the selected exact +DevTools version, then reads ``runtime --json --no-ansi --no-interaction`` +to expose the real binary and autoloader. Consumer dependencies remain in their +own Composer installation. The DevTools producer uses checked-out source. For Pages configured to use GitHub Actions, the optional -``reports-native.yml`` workflow requires consumer-local DevTools and a template -version of at least 2.1.0. Follow the `native Pages operator guide +``reports-native.yml`` workflow has its own adoption contract, including a +template version of at least 2.1.0. Review its current runtime requirements +separately when migrating an existing caller. Follow the `native Pages operator guide `_ before adopting it. The legacy ``reports.yml`` workflow remains unchanged. @@ -237,7 +234,7 @@ Migration from the pointer-based workflows requires updating the consumer's ``wiki.yml`` and removing its automatic maintenance triggers. The packaged ``wiki-maintenance.yml`` is now a manual-only compatibility stub. Normal sync preserves existing files: replace the wrappers explicitly or review -``composer dev-tools:sync --overwrite``. Sync does not delete files missing +``cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --overwrite``. Sync does not delete files missing from its packaged source directory. Remove only the ``.github/wiki`` gitlink and its matching ``.gitmodules`` diff --git a/docs/usage/index.rst b/docs/usage/index.rst index 2e2b55e0c4..5a80d27f48 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -12,6 +12,7 @@ task-oriented guidance instead of class-by-class reference. documentation-workflows github-actions migrating-consumer-repositories + dev-tools-2-migration syncing-packaged-agents syncing-packaged-skills syncing-consumer-projects diff --git a/docs/usage/migrating-consumer-repositories.rst b/docs/usage/migrating-consumer-repositories.rst index 839d5ddda8..8117e1c3e4 100644 --- a/docs/usage/migrating-consumer-repositories.rst +++ b/docs/usage/migrating-consumer-repositories.rst @@ -1,163 +1,10 @@ Migrating Consumer Repositories =============================== -Use this guide when an existing PHP package repository is adopting -``fast-forward/dev-tools`` for the first time. The goal is to introduce the -shared Composer scripts, workflow stubs, wiki setup, skills, and repository -defaults in a reviewable pull request. +Follow :doc:`dev-tools-2-migration`: publish and verify DevTools 2 first, then +prepare one migration PR per consumer. Pin runtime and skill sources, remove +the reverse Composer dependency, preserve customizations and verify required +workflow statuses before merge. -This page complements :doc:`syncing-consumer-projects`; it focuses on the -migration path instead of the full command reference. - -Preflight Checks ----------------- - -Before installing DevTools, confirm the repository has: - -- a committed ``composer.json`` with the package name, PHP constraint, license, - and autoload settings already reviewed; -- a clean working tree; -- a configured ``origin`` remote that points to the GitHub repository; -- agreement on whether the repository should publish GitHub Pages reports and a - GitHub Wiki; -- a maintainer who can review workflow, Dependabot, and branch protection - changes. - -If the repository already has custom workflows or generated documentation, list -the files that must be preserved before running sync. The sync command protects -many existing files by default, but maintainers should still review the outcome -as infrastructure code. - -Install DevTools ----------------- - -Install the package as a development dependency: - -.. code-block:: bash - - composer require --dev fast-forward/dev-tools - -Commit the Composer changes separately only if the repository prefers very small -review steps. For most migrations, it is reasonable to keep the Composer update -and generated sync changes in the same pull request so reviewers can understand -the complete automation model. - -Run Sync Safely ---------------- - -Run the sync command from the repository root: - -.. code-block:: bash - - composer dev-tools:sync - -Then inspect the result before staging anything: - -.. code-block:: bash - - git status --short - git diff - -Expected changes can include: - -.. list-table:: - :header-rows: 1 - - * - Area - - Expected change - - Review focus - * - ``composer.json`` - - Adds shared ``dev-tools`` scripts and GrumPHP defaults. - - Keep existing package metadata, scripts, and project-specific settings. - * - ``.github/workflows/*.yml`` - - Adds thin workflow stubs that call reusable Fast Forward workflows. - - Compare with any existing CI, release, docs, or Pages workflows. - * - ``.editorconfig`` - - Adds the shared editor defaults when missing. - - Confirm indentation and line-ending expectations match the project. - * - ``.github/dependabot.yml`` - - Adds the shared Dependabot template when missing. - - Confirm update cadence and ecosystem coverage. - * - ``.dev-tools/wiki-generated`` - - Generates local API Markdown without creating a submodule. - - Review the generated pages and direct Wiki workflow configuration. - * - ``.agents/skills`` - - Links packaged skills for issue, PR, tests, docs, README, and style work. - - Preserve existing custom skills and non-symlink directories. - * - ``.agents/agents`` - - Links packaged project agents for issue, PR, docs, README, and AGENTS - maintenance work. - - Preserve existing custom agents and non-symlink directories. - * - ``LICENSE`` - - Adds the packaged license file when missing. - - Confirm it matches the package metadata. - * - ``.gitignore`` and ``.gitattributes`` - - Adds repository defaults when missing. - - Keep existing project-specific ignores and export rules. - -Handling Existing Custom Files ------------------------------- - -When a target file already exists, treat the migration as a comparison exercise: - -1. keep the existing file in place; -2. compare it with the corresponding packaged file or workflow stub; -3. copy only the entries that make sense for the repository; -4. leave a pull request note explaining any intentional divergence. - -This is especially important for custom CI workflows, release automation, -repository-specific ``.gitignore`` rules, and existing Dependabot policies. -Shared defaults are meant to reduce repeated maintenance, not erase local -constraints. - -When adopting direct Wiki publication, update an existing ``wiki.yml`` wrapper -explicitly to call ``wiki-direct.yml`` and remove automatic maintenance -triggers. Normal sync preserves existing workflow files by default. The -packaged ``wiki-maintenance.yml`` is a manual-only migration notice; legacy -reusable preview and maintenance workflows remain available for unmigrated -callers. - -Remove only the obsolete ``.github/wiki`` gitlink and its matching -``.gitmodules`` stanza, preserving other submodules and the separate GitHub -Wiki repository. Local sync does not remove an existing pointer automatically. - -Branch Protection and Bot Commits ---------------------------------- - -Consumer repositories commonly protect ``main``. That is compatible with the -Fast Forward model: - -- pull requests receive generated previews and bot updates on the PR branch; -- report previews are published under PR-specific Pages paths; -- the direct Wiki workflow generates a read-only artifact on pull requests, - without publishing a preview branch or committing a submodule pointer; -- post-merge jobs publish final reports and wiki content from ``main``. - -If branch protection blocks bot commits to pull request branches, either adjust -the repository policy to allow workflow updates on PR branches or document that -maintainers must apply those bot-managed changes manually before merge. - -Suggested Pull Request Rollout ------------------------------- - -Use one migration pull request per consumer repository: - -1. install ``fast-forward/dev-tools``; -2. run ``composer dev-tools:sync``; -3. review and adapt generated files; -4. run the smallest relevant local verification command; -5. open the pull request and let GitHub Actions create previews; -6. verify report preview links and the generated Wiki artifact before merge; -7. merge through the protected ``main`` branch. - -After merge, confirm that the final reports and wiki pages reflect the merged -revision. If they do not, inspect the workflow logs before making follow-up -changes. - -Related References ------------------- - -- :doc:`syncing-consumer-projects` -- :doc:`syncing-packaged-agents` -- :doc:`syncing-packaged-skills` -- :doc:`github-actions` +For new adoption use :doc:`../getting-started/installation`, preview +``dev-tools:sync`` and install skills through :doc:`syncing-packaged-skills`. diff --git a/docs/usage/syncing-consumer-projects.rst b/docs/usage/syncing-consumer-projects.rst index 0179abbe90..e3ae71dcda 100644 --- a/docs/usage/syncing-consumer-projects.rst +++ b/docs/usage/syncing-consumer-projects.rst @@ -1,123 +1,19 @@ Syncing Consumer Projects ========================= -The ``dev-tools:sync`` command is the bridge between this repository and the -libraries that consume it. - -For the focused skills-only workflow, see :doc:`syncing-packaged-skills`. -For packaged project agents, see :doc:`syncing-packaged-agents`. -For a first-time adoption path, see -:doc:`migrating-consumer-repositories`. - -What the Command Changes ------------------------- - -.. list-table:: - :header-rows: 1 - - * - Asset - - Behavior - - Overwrite policy - * - ``composer.json`` scripts - - Adds or updates ``dev-tools`` and ``dev-tools:fix``. - - Updated in place. - * - ``composer.json`` extra - - Removes only deprecated DevTools-managed - ``extra.grumphp.config-default-path`` values while preserving - consumer-owned GrumPHP settings. - - Updated in place. - * - ``.github/workflows/*.yml`` - - Copies thin wrapper workflows from ``resources/github-actions`` that - delegate to reusable workflows in ``php-fast-forward/dev-tools``. - - Only when missing by default; replaceable with ``--overwrite``. - * - ``.editorconfig`` - - Copies the packaged file from the repository root. - - Only when missing by default; replaceable with ``--overwrite``. - * - ``.github/dependabot.yml`` - - Copies the packaged Dependabot template. - - Only when missing by default; replaceable with ``--overwrite``. - * - ``.github/CODEOWNERS`` - - Generates managed ownership rules from local ``composer.json`` metadata, - with commented suggestions when direct GitHub owners cannot be inferred. - - Preserved by default; replaceable with ``--overwrite``. - * - ``.agents/skills/`` - - Creates or repairs symlinks to packaged agent skills. - - Creates missing links, repairs broken symlinks, and preserves existing - non-symlink directories. - * - ``.agents/agents/`` - - Creates or repairs symlinks to packaged project agents. - - Creates missing links, repairs broken symlinks, and preserves existing - non-symlink directories. - * - ``.dev-tools/wiki-generated`` - - Generates local Markdown API documentation without creating a Git - submodule. - - Generated in normal mode; skipped in preview/check modes. - * - ``.git/hooks/*`` - - Copies packaged hooks that prefer a local ``grumphp.yml`` override and - otherwise use a project-relative reference to the active packaged - DevTools ``grumphp.yml`` path resolved when sync installs them. - - Replaced when drift is detected. - -When to Run It --------------- - -- right after installing the package with plugins disabled; -- after upgrading ``fast-forward/dev-tools`` and wanting new shared workflow - stubs; -- when onboarding an older repository into the Fast Forward automation model. -- when packaged skills were added or updated and the consumer repository - needs fresh links. - -What It Needs -------------- - -- a writable ``composer.json`` in the consumer project; -- permission to create local ``.github/`` files; -- permission to write local ``.dev-tools/wiki-generated`` documentation; -- permission to create local ``.agents/skills`` and ``.agents/agents`` - entries. - -Workflow Wrapper Notes ----------------------- - -The synchronized workflow files in consumer repositories are wrappers, not the -full implementation. The actual reusable workflows live in the upstream -``php-fast-forward/dev-tools`` repository. When those reusable workflows need -local action implementations, they explicitly check out the upstream -``php-fast-forward/dev-tools`` repository into a dedicated -``.dev-tools-actions`` path in the run workspace first, so the consumer -repository does not need to mirror ``.github/actions`` locally. Workflows that -need only shared actions can use sparse checkout. The direct Wiki workflow -uses a full checkout pinned to a commit, installs the renderer there, and runs -it against the consumer project without changing its ``composer.json``, -``composer.lock``, or plugin allowlist. - -That matters most for these cases: - -* ``.github/workflows/wiki.yml`` calls ``wiki-direct.yml`` for read-only - generation on pull requests and direct publication from ``main`` to the - separate Wiki repository. Only ``Home.md``, ``classes/``, and ``functions/`` - are generated surfaces; manually maintained pages outside them are - preserved. -* The packaged ``.github/workflows/wiki-maintenance.yml`` is a manual-only - migration notice. Legacy reusable ``wiki-preview.yml`` and - ``wiki-maintenance.yml`` remain available for unmigrated callers. -* Normal sync generates ``.dev-tools/wiki-generated`` without ``--init``. - The explicit ``wiki --init`` option remains available for legacy submodule - users. -* ``.github/workflows/auto-assign.yml`` accepts a ``project`` input in the - wrapper and also support the ``PROJECT`` repository variable. -* The effective project-board contract is ``project`` first, then - ``PROJECT``, then any reusable-workflow default that applies to - ``php-fast-forward`` repositories. - -.. important:: - - Workflow stubs, ``.editorconfig``, and ``dependabot.yml`` are copied only - when the target file does not already exist unless ``--overwrite`` is used. - This protects consumer-specific customizations by default while still - allowing explicit replacement during shared automation updates. The - generated ``.github/CODEOWNERS`` file follows the same principle by keeping - an existing file unless you explicitly request replacement. The ``skills`` - and ``agents`` phases follow the same spirit by preserving existing - non-symlink directories inside ``.agents/skills`` and ``.agents/agents``. +``dev-tools:sync`` explicitly manages the remaining DevTools-owned assets: +Composer scripts/metadata, funding, CODEOWNERS, workflow callers, editor +defaults, ignore/export rules, license and Git hooks. Normal sync also generates +Wiki Markdown in ``.dev-tools/wiki-generated`` without creating a submodule. + +.. code-block:: bash + + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --dry-run + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync --check + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" dev-tools:sync + +Inspect generated changes. Copied workflows and existing Dependabot policy +remain preserved by default; replacement requires deliberate adoption. +Wiki generation is skipped in preview modes. ``skills`` and ``agents`` are +no longer phases. LLM Skills handles skills separately and does not synchronize +generic repository files. See :doc:`syncing-packaged-skills`. diff --git a/docs/usage/syncing-packaged-agents.rst b/docs/usage/syncing-packaged-agents.rst index de0d1b8bfd..39a94bccc2 100644 --- a/docs/usage/syncing-packaged-agents.rst +++ b/docs/usage/syncing-packaged-agents.rst @@ -1,76 +1,13 @@ -Syncing Packaged Agents -======================= +Project Agent Prompts +===================== -The ``agents`` command keeps the consumer repository's ``.agents/agents`` -directory aligned with the packaged project-agent prompts shipped inside -``fast-forward/dev-tools``. +DevTools 2 removes ``agents`` and automatic links into ``.agents/agents``. +Custom role prompts remain repository-owned and distinct from procedural +skills installed through LLM Skills. -Why This Command Exists ------------------------ +Inventory old links before removing the Composer dependency. Preserve custom +directories and edited files. Choose a real owner and supported distribution +contract before replacing links. LLM Skills does not synchronize arbitrary +role prompts, workflow files or repository metadata. -Fast Forward libraries can share role-based project agents without copying -prompt files into every consumer repository. The packaged agent directories -live in this repository, while consumer repositories receive lightweight -repository-relative symlinks that point back to the packaged source. - -That approach keeps upgrades simple: - -- updating ``fast-forward/dev-tools`` changes the packaged project-agent source; -- rerunning ``agents`` repairs missing or broken links; -- consumer-specific directories are preserved when they are not symlinks. - -How to Run It -------------- - -.. code-block:: bash - - composer agents - vendor/bin/dev-tools agents - -What the Command Does ---------------------- - -.. list-table:: - :header-rows: 1 - - * - Situation - - Behavior - * - ``.agents/agents`` is missing - - Creates the directory in the consumer repository. - * - A packaged agent is missing locally - - Creates a repository-relative symlink that points to the packaged - agent directory. - * - A valid symlink already exists - - Leaves the link unchanged. - * - A symlink is broken - - Removes it and recreates it with the current packaged target. - * - A real directory already exists at the target path - - Preserves the directory and skips link creation to avoid overwriting - consumer-owned content. - -When to Run It Manually ------------------------ - -Run ``agents`` explicitly when: - -- you upgraded ``fast-forward/dev-tools`` and want to refresh local - project-agent links; -- someone deleted or broke entries inside ``.agents/agents``; -- Composer plugins were disabled during install, so ``dev-tools:sync`` did not - run automatically; -- you are iterating on packaged project agents and want to verify the - consumer-facing links without rerunning the entire repository sync flow. - -Relationship with ``dev-tools:sync`` ------------------------------------- - -``dev-tools:sync`` runs ``skills`` and ``agents`` in normal mode. That means -the full onboarding command refreshes workflow stubs, repository defaults, -packaged skills, and packaged role prompts in one pass. - -What the Command Does Not Overwrite ------------------------------------ - -The command does not replace an existing non-symlink directory inside -``.agents/agents``. This protects local experiments, package-specific custom -agents, or directories managed by another tool. +See :doc:`syncing-packaged-skills` and :doc:`dev-tools-2-migration`. diff --git a/docs/usage/syncing-packaged-skills.rst b/docs/usage/syncing-packaged-skills.rst index aba19c58d2..54b66df902 100644 --- a/docs/usage/syncing-packaged-skills.rst +++ b/docs/usage/syncing-packaged-skills.rst @@ -1,81 +1,87 @@ -Syncing Packaged Skills -======================= - -The ``skills`` command keeps the consumer repository's ``.agents/skills`` -directory aligned with the skills shipped inside -``fast-forward/dev-tools``. - -Why This Command Exists ------------------------ - -Fast Forward libraries can share agent skills without copying them into every -consumer repository. The packaged skill directories live in this repository, -while consumer repositories receive lightweight repository-relative symlinks -that point back to the packaged source. - -That approach keeps upgrades simple: - -- updating ``fast-forward/dev-tools`` changes the packaged skill source; -- rerunning ``skills`` repairs missing or broken links; -- consumer-specific directories are preserved when they are not symlinks. - -How to Run It -------------- +Installing Packaged Skills +========================== + +DevTools exports owned procedural packages under +``resources/skills/fast-forward-*``. LLM Skills owns their installation, +updates and provenance. Installing instructions does not install the CLI. + +Use the supported LLM Skills integration (Composer package ``llm/skills``), +then configure ``skills.json``. When DevTools is only an external tool, use +a pinned GitHub source after verifying that release exists: + +.. code-block:: json + + { + "target": ".agents/skills", + "aliases": [], + "discovery": false, + "vendor-sources": false, + "sources": [ + { + "from": "github", + "package": "php-fast-forward/dev-tools", + "ref": "v2.0.0", + "skills": ["fast-forward-phpunit-tests", "fast-forward-phpdoc-code-style"] + } + ] + } + +The full tag above is the version 2 migration target, not a claim of published +availability. Prefer an immutable verified release commit when adopting it. +Select only the skills needed by the project. .. code-block:: bash - composer dev-tools skills - vendor/bin/dev-tools skills - -What the Command Does ---------------------- - -.. list-table:: - :header-rows: 1 - - * - Situation - - Behavior - * - ``.agents/skills`` is missing - - Creates the directory in the consumer repository. - * - A packaged skill is missing locally - - Creates a repository-relative symlink that points to the packaged skill - directory. - * - A valid symlink already exists - - Leaves the link unchanged. - * - A symlink is broken - - Removes it and recreates it with the current packaged target. - * - A real directory already exists at the target path - - Preserves the directory and skips link creation to avoid overwriting - consumer-owned content. - -When to Run It Manually ------------------------ - -Run ``skills`` explicitly when: + composer skills:show + composer skills:update --dry-run + composer skills:update -- you upgraded ``fast-forward/dev-tools`` and want to refresh local skill - links; -- someone deleted or broke entries inside ``.agents/skills``; -- Composer plugins were disabled during install, so ``dev-tools:sync`` did not - run automatically; -- you are iterating on packaged skills and want to verify the consumer-facing - links without rerunning the entire repository sync flow. +For a focused GitHub-source update, LLM Skills also accepts the source package: -Relationship with ``dev-tools:sync`` ------------------------------------- +.. code-block:: bash -``dev-tools:sync`` ends by running ``gitignore`` and ``skills``. That means -the full onboarding command refreshes workflow stubs, repository defaults, -packaged skills, and packaged role prompts in one pass. + composer skills:update php-fast-forward/dev-tools --dry-run + composer skills:update php-fast-forward/dev-tools -What the Command Does Not Overwrite ------------------------------------ +A positional update explicitly trusts that operation; it is not proof that the +same source is enabled for unattended default synchronization. Review preview, +source configuration and provenance together. Keep LLM Skills' own metadata +and lock state; DevTools must not manufacture substitute records. -The command does not replace an existing non-symlink directory inside -``.agents/skills``. This protects local experiments, package-specific custom -skills, or directories managed by another tool. +Composer Trusted donors +----------------------- -Internally, ``skills`` shares the same generic packaged-directory -synchronization helper used by ``agents``. That keeps link creation, broken -link repair, and consumer-owned-directory preservation consistent across both -flows. +For libraries actually installed through Composer, trust their resolved skill +payload rather than adding a second GitHub owner for the same skill: + +.. code-block:: json + + { + "dependencies": { + "composer": { + "trusted": ["fast-forward/config", "fast-forward/container"], + "trusted-replace": true + } + } + } + +This is an example for projects that really install those packages. Trust does +not install a missing library or fetch a GitHub replacement. A missing donor +payload must fail review until a compatible donor or explicit source migration +is selected. ``trusted-replace`` permits a trusted package's canonical skill +replacement; review ownership changes and remove the prior source explicitly. + +Self development and extraction +------------------------------- + +The DevTools checkout uses a ``dir`` source for its own ``resources/skills``. +Consumers use GitHub when DevTools is absent from their Composer graph. +As code style, quality or another domain moves out, retain the +``fast-forward-`` skill name, publish its new owner first, +then transfer the source in one reviewed consumer change. Keep one owner per +skill. Do not leave duplicated DevTools and donor sources for the same name. + +Version 2 removes ``skills`` and ``agents`` from the DevTools command surface. +Inventory old links and preserve edited/local directories before migrating. +LLM Skills does not synchronize workflow files, Git hooks, metadata or generic +project-agent prompts. See :doc:`dev-tools-2-migration`. diff --git a/docs/usage/testing-and-coverage.rst b/docs/usage/testing-and-coverage.rst index c4623d1d85..08d4298b66 100644 --- a/docs/usage/testing-and-coverage.rst +++ b/docs/usage/testing-and-coverage.rst @@ -27,12 +27,12 @@ Useful Examples .. code-block:: bash - composer tests - composer tests -- --filter=PluginTest - composer tests --coverage=.dev-tools/coverage - composer tests --no-cache --bootstrap=tests/bootstrap.php - composer tests --json - composer tests --pretty-json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --filter=PluginTest + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --coverage=.dev-tools/coverage + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --no-cache --bootstrap=tests/bootstrap.php + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --json + cpx --skip-local "fast-forward/dev-tools:$DEV_TOOLS_VERSION" tests --pretty-json Coverage Outputs ---------------- diff --git a/resources/docblock/.docheader b/resources/docblock/.docheader index c65c82fbd5..4648495a5b 100644 --- a/resources/docblock/.docheader +++ b/resources/docblock/.docheader @@ -4,7 +4,8 @@ {% endif %} * This file is part of {{ package }} project. * - * @author {{ copyright_holder|raw }} +{% if copyright_holder %} * @author {{ copyright_holder|raw }} +{% endif %} {% if license %} * @license https://opensource.org/licenses/{{ license }} {{ license }} License {% endif %} * diff --git a/resources/git-hooks/commit-msg b/resources/git-hooks/commit-msg index aab448834f..99afc2b70b 100755 --- a/resources/git-hooks/commit-msg +++ b/resources/git-hooks/commit-msg @@ -28,18 +28,23 @@ elif [ -f "${DEVTOOLS_GRUMPHP_CONFIG}" ]; then fi # Run GrumPHP -if [ -x "vendor/bin/grumphp" ]; then - GRUMPHP_COMMAND='vendor/bin/grumphp' -elif [ -x "vendor/bin/grumphp.phar" ]; then - GRUMPHP_COMMAND='vendor/bin/grumphp.phar' -else - GRUMPHP_COMMAND='grumphp' -fi +run_grumphp() { + if [ -x "vendor/bin/grumphp" ]; then + vendor/bin/grumphp "$@" + elif [ -x "vendor/bin/grumphp.phar" ]; then + vendor/bin/grumphp.phar "$@" + else + cpx --skip-local phpro/grumphp-shim:2.26.0 "$@" + fi +} -if [ -n "${GRUMPHP_COMMAND}" ]; then +if command -v cpx >/dev/null 2>&1 || [ -x vendor/bin/grumphp ] || [ -x vendor/bin/grumphp.phar ]; then if [ -n "${GRUMPHP_CONFIG_FILE}" ]; then - printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}" '--config' "${GRUMPHP_CONFIG_FILE}" 'git:commit-msg' "--git-user=$GIT_USER" "--git-email=$GIT_EMAIL" "$COMMIT_MSG_FILE" + printf "%s\n" "${DIFF}" | run_grumphp '--config' "${GRUMPHP_CONFIG_FILE}" 'git:commit-msg' "--git-user=$GIT_USER" "--git-email=$GIT_EMAIL" "$COMMIT_MSG_FILE" else - printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}" 'git:commit-msg' "--git-user=$GIT_USER" "--git-email=$GIT_EMAIL" "$COMMIT_MSG_FILE" + printf "%s\n" "${DIFF}" | run_grumphp 'git:commit-msg' "--git-user=$GIT_USER" "--git-email=$GIT_EMAIL" "$COMMIT_MSG_FILE" fi +else + printf '%s\n' 'DevTools hooks require CPX or a project-local GrumPHP binary.' >&2 + exit 1 fi diff --git a/resources/git-hooks/pre-commit b/resources/git-hooks/pre-commit index 82b37a4788..38209c12d5 100755 --- a/resources/git-hooks/pre-commit +++ b/resources/git-hooks/pre-commit @@ -24,18 +24,23 @@ elif [ -f "${DEVTOOLS_GRUMPHP_CONFIG}" ]; then fi # Run GrumPHP -if [ -x "vendor/bin/grumphp" ]; then - GRUMPHP_COMMAND='vendor/bin/grumphp' -elif [ -x "vendor/bin/grumphp.phar" ]; then - GRUMPHP_COMMAND='vendor/bin/grumphp.phar' -else - GRUMPHP_COMMAND='grumphp' -fi +run_grumphp() { + if [ -x "vendor/bin/grumphp" ]; then + vendor/bin/grumphp "$@" + elif [ -x "vendor/bin/grumphp.phar" ]; then + vendor/bin/grumphp.phar "$@" + else + cpx --skip-local phpro/grumphp-shim:2.26.0 "$@" + fi +} -if [ -n "${GRUMPHP_COMMAND}" ]; then +if command -v cpx >/dev/null 2>&1 || [ -x vendor/bin/grumphp ] || [ -x vendor/bin/grumphp.phar ]; then if [ -n "${GRUMPHP_CONFIG_FILE}" ]; then - printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}" '--config' "${GRUMPHP_CONFIG_FILE}" 'git:pre-commit' '--skip-success-output' + printf "%s\n" "${DIFF}" | run_grumphp '--config' "${GRUMPHP_CONFIG_FILE}" 'git:pre-commit' '--skip-success-output' + else + printf "%s\n" "${DIFF}" | run_grumphp 'git:pre-commit' '--skip-success-output' fi - - printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}" 'git:pre-commit' '--skip-success-output' +else + printf '%s\n' 'DevTools hooks require CPX or a project-local GrumPHP binary.' >&2 + exit 1 fi diff --git a/resources/github-actions/auto-assign.yml b/resources/github-actions/auto-assign.yml index cf1d7a4a04..c6bd701555 100644 --- a/resources/github-actions/auto-assign.yml +++ b/resources/github-actions/auto-assign.yml @@ -12,7 +12,7 @@ jobs: issues: write repository-projects: write pull-requests: write - uses: php-fast-forward/dev-tools/.github/workflows/auto-assign.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/auto-assign.yml@v2.0.0 with: # project: 1 # diff --git a/resources/github-actions/label-sync.yml b/resources/github-actions/label-sync.yml index 649aab9d99..e554c3b36c 100644 --- a/resources/github-actions/label-sync.yml +++ b/resources/github-actions/label-sync.yml @@ -12,4 +12,4 @@ jobs: contents: read issues: read pull-requests: write - uses: php-fast-forward/dev-tools/.github/workflows/label-sync.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/label-sync.yml@v2.0.0 diff --git a/resources/github-actions/reports.yml b/resources/github-actions/reports.yml index f0f8c322b7..e86b66047b 100644 --- a/resources/github-actions/reports.yml +++ b/resources/github-actions/reports.yml @@ -22,7 +22,8 @@ permissions: jobs: reports: - uses: php-fast-forward/dev-tools/.github/workflows/reports.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/reports.yml@v2.0.0 secrets: inherit with: + dev-tools-version: '2.0.0' cleanup-previews: ${{ inputs.cleanup-previews || false }} diff --git a/resources/github-actions/review.yml b/resources/github-actions/review.yml index d8c5f9e87e..a69955f649 100644 --- a/resources/github-actions/review.yml +++ b/resources/github-actions/review.yml @@ -15,6 +15,6 @@ jobs: permissions: contents: read pull-requests: write - uses: php-fast-forward/dev-tools/.github/workflows/review.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/review.yml@v2.0.0 with: pull-request-number: ${{ github.event.pull_request.number || inputs.pull-request-number || '' }} diff --git a/resources/github-actions/tests.yml b/resources/github-actions/tests.yml index e0d51d55f3..49c5a86871 100644 --- a/resources/github-actions/tests.yml +++ b/resources/github-actions/tests.yml @@ -27,8 +27,9 @@ permissions: jobs: tests: - uses: php-fast-forward/dev-tools/.github/workflows/tests.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/tests.yml@v2.0.0 with: + dev-tools-version: '2.0.0' max-outdated: ${{ inputs.max-outdated || -1 }} publish-required-statuses: ${{ inputs.publish-required-statuses || false }} secrets: inherit diff --git a/resources/github-actions/wiki.yml b/resources/github-actions/wiki.yml index 0a35d8ac48..b20ede3b5f 100644 --- a/resources/github-actions/wiki.yml +++ b/resources/github-actions/wiki.yml @@ -11,4 +11,6 @@ permissions: jobs: wiki: - uses: php-fast-forward/dev-tools/.github/workflows/wiki-direct.yml@main + uses: php-fast-forward/dev-tools/.github/workflows/wiki-direct.yml@v2.0.0 + with: + dev-tools-version: '2.0.0' diff --git a/resources/skills/fast-forward-create-agentsmd/LICENSE b/resources/skills/fast-forward-create-agentsmd/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-create-agentsmd/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/create-agentsmd/SKILL.md b/resources/skills/fast-forward-create-agentsmd/SKILL.md similarity index 92% rename from .agents/skills/create-agentsmd/SKILL.md rename to resources/skills/fast-forward-create-agentsmd/SKILL.md index 26a72b236d..bcf7cf2f21 100644 --- a/.agents/skills/create-agentsmd/SKILL.md +++ b/resources/skills/fast-forward-create-agentsmd/SKILL.md @@ -1,6 +1,10 @@ --- -name: create-agentsmd +name: fast-forward-create-agentsmd description: Generate or refresh repository-level AGENTS.md files with actionable setup, workflow, testing, style, and contribution guidance for coding agents. Use when a repository is missing AGENTS.md, when an existing AGENTS.md is stale, or when agent-facing project instructions need to be aligned with the current checkout. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward AGENTS.md diff --git a/resources/skills/fast-forward-create-agentsmd/agents/openai.yaml b/resources/skills/fast-forward-create-agentsmd/agents/openai.yaml new file mode 100644 index 0000000000..fcd1c4dbeb --- /dev/null +++ b/resources/skills/fast-forward-create-agentsmd/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward AGENTS.md" + short_description: "Create AGENTS.md files for repositories" + default_prompt: "Use $fast-forward-create-agentsmd to analyze this repository and write or refresh a focused AGENTS.md file at the project root." diff --git a/.agents/skills/create-agentsmd/references/content-outline.md b/resources/skills/fast-forward-create-agentsmd/references/content-outline.md similarity index 100% rename from .agents/skills/create-agentsmd/references/content-outline.md rename to resources/skills/fast-forward-create-agentsmd/references/content-outline.md diff --git a/resources/skills/fast-forward-github-issues/LICENSE b/resources/skills/fast-forward-github-issues/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-github-issues/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/github-issues/SKILL.md b/resources/skills/fast-forward-github-issues/SKILL.md similarity index 96% rename from .agents/skills/github-issues/SKILL.md rename to resources/skills/fast-forward-github-issues/SKILL.md index e9f65d2fd6..8b6809adf5 100644 --- a/.agents/skills/github-issues/SKILL.md +++ b/resources/skills/fast-forward-github-issues/SKILL.md @@ -1,6 +1,10 @@ --- -name: github-issues +name: fast-forward-github-issues description: Draft, create, update, comment on, and close GitHub issues for Fast Forward repositories. Use when an agent needs to turn a brief feature, bug, or task prompt into a production-ready issue, refine existing issue content, publish or update it on GitHub, manage existing metadata such as labels, types, milestones, projects, and project fields, add issue comments, or close tracked work without leaving the local workflow. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward GitHub Issues diff --git a/resources/skills/fast-forward-github-issues/agents/openai.yaml b/resources/skills/fast-forward-github-issues/agents/openai.yaml new file mode 100644 index 0000000000..9558688a16 --- /dev/null +++ b/resources/skills/fast-forward-github-issues/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward GitHub Issues" + short_description: "Draft and manage Fast Forward GitHub issues with existing repository metadata" + default_prompt: "Use $fast-forward-github-issues to draft, create, or update this Fast Forward GitHub issue and apply the most appropriate existing repository metadata." diff --git a/.agents/skills/github-issues/references/architectural-criteria.md b/resources/skills/fast-forward-github-issues/references/architectural-criteria.md similarity index 100% rename from .agents/skills/github-issues/references/architectural-criteria.md rename to resources/skills/fast-forward-github-issues/references/architectural-criteria.md diff --git a/.agents/skills/github-issues/references/context.md b/resources/skills/fast-forward-github-issues/references/context.md similarity index 100% rename from .agents/skills/github-issues/references/context.md rename to resources/skills/fast-forward-github-issues/references/context.md diff --git a/.agents/skills/github-issues/references/metadata.md b/resources/skills/fast-forward-github-issues/references/metadata.md similarity index 97% rename from .agents/skills/github-issues/references/metadata.md rename to resources/skills/fast-forward-github-issues/references/metadata.md index 5ab251ee3e..66d9b7bc62 100644 --- a/.agents/skills/github-issues/references/metadata.md +++ b/resources/skills/fast-forward-github-issues/references/metadata.md @@ -145,5 +145,5 @@ Avoid speculative or noisy links when the relationship is weak. ## Title Guidance - Keep the title specific and actionable. -- Reuse the title drafted by `github-issues` unless the GitHub context reveals a better repository-specific wording. +- Reuse the title drafted by `fast-forward-github-issues` unless the GitHub context reveals a better repository-specific wording. - Avoid redundant prefixes like `[Bug]` when issue types already communicate that information. diff --git a/.agents/skills/github-issues/references/operations.md b/resources/skills/fast-forward-github-issues/references/operations.md similarity index 100% rename from .agents/skills/github-issues/references/operations.md rename to resources/skills/fast-forward-github-issues/references/operations.md diff --git a/.agents/skills/github-issues/references/review-checklist.md b/resources/skills/fast-forward-github-issues/references/review-checklist.md similarity index 100% rename from .agents/skills/github-issues/references/review-checklist.md rename to resources/skills/fast-forward-github-issues/references/review-checklist.md diff --git a/.agents/skills/github-issues/references/templates.md b/resources/skills/fast-forward-github-issues/references/templates.md similarity index 100% rename from .agents/skills/github-issues/references/templates.md rename to resources/skills/fast-forward-github-issues/references/templates.md diff --git a/.agents/skills/github-issues/references/triage.md b/resources/skills/fast-forward-github-issues/references/triage.md similarity index 100% rename from .agents/skills/github-issues/references/triage.md rename to resources/skills/fast-forward-github-issues/references/triage.md diff --git a/resources/skills/fast-forward-github-pull-request/LICENSE b/resources/skills/fast-forward-github-pull-request/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-github-pull-request/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/github-pull-request/SKILL.md b/resources/skills/fast-forward-github-pull-request/SKILL.md similarity index 90% rename from .agents/skills/github-pull-request/SKILL.md rename to resources/skills/fast-forward-github-pull-request/SKILL.md index ed3134be42..29dd0c1dcb 100644 --- a/.agents/skills/github-pull-request/SKILL.md +++ b/resources/skills/fast-forward-github-pull-request/SKILL.md @@ -1,6 +1,10 @@ --- -name: github-pull-request +name: fast-forward-github-pull-request description: Implement a ready Fast Forward GitHub issue on an isolated branch, verify code, tests, and documentation impacts, and open or update the corresponding pull request. Use when an agent needs to move an issue through the GitHub implementation-to-PR workflow, or when the user asks to work through ready issues sequentially. Resolve repository or pull-request context through local git and GitHub data when needed. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward GitHub Pull Request @@ -26,7 +30,7 @@ Use this skill to take a Fast Forward issue from "ready to implement" to an open branch and PR instead of reviving the old branch name. - Prefer local `git` for checkout, commit, and push. - Prefer connector-backed GitHub data for issue and PR context when available. -- Use `phpunit-tests`, `package-readme`, and `sphinx-docs` when the change clearly affects tests or documentation. +- Use `fast-forward-phpunit-tests`, `fast-forward-package-readme`, and `fast-forward-sphinx-docs` when the change clearly affects tests or documentation. - Use `fast-forward-changelog` when the implementation introduces a notable user-facing or automation-facing change that SHOULD appear in release notes. - Never manually close an issue; rely on `Closes #123` style text in the PR body. - Do not block waiting for merge. Open or update the PR, then report status and the next action. diff --git a/resources/skills/fast-forward-github-pull-request/agents/openai.yaml b/resources/skills/fast-forward-github-pull-request/agents/openai.yaml new file mode 100644 index 0000000000..f4eb79408a --- /dev/null +++ b/resources/skills/fast-forward-github-pull-request/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward GitHub Pull Request" + short_description: "Implement ready issues through branch and PR workflow" + default_prompt: "Use $fast-forward-github-pull-request to implement this ready Fast Forward issue on an isolated branch and open or update the matching PR." diff --git a/.agents/skills/github-pull-request/references/context-routing.md b/resources/skills/fast-forward-github-pull-request/references/context-routing.md similarity index 100% rename from .agents/skills/github-pull-request/references/context-routing.md rename to resources/skills/fast-forward-github-pull-request/references/context-routing.md diff --git a/.agents/skills/github-pull-request/references/implementation-loop.md b/resources/skills/fast-forward-github-pull-request/references/implementation-loop.md similarity index 71% rename from .agents/skills/github-pull-request/references/implementation-loop.md rename to resources/skills/fast-forward-github-pull-request/references/implementation-loop.md index 291a399bb8..511e12ffaa 100644 --- a/.agents/skills/github-pull-request/references/implementation-loop.md +++ b/resources/skills/fast-forward-github-pull-request/references/implementation-loop.md @@ -19,10 +19,10 @@ Use this loop for each selected issue. Use these only when the issue clearly needs them: -- `phpunit-tests` for new or updated PHPUnit coverage -- `package-readme` for README changes -- `sphinx-docs` for Sphinx or docs tree changes -- `phpdoc-code-style` for repository-specific PHPDoc cleanup, file-header normalization, and PHP formatting when the issue touches PHP comments or contracts +- `fast-forward-phpunit-tests` for new or updated PHPUnit coverage +- `fast-forward-package-readme` for README changes +- `fast-forward-sphinx-docs` for Sphinx or docs tree changes +- `fast-forward-phpdoc-code-style` for repository-specific PHPDoc cleanup, file-header normalization, and PHP formatting when the issue touches PHP comments or contracts ## Verification Strategy @@ -31,8 +31,8 @@ Choose the smallest command that proves the change first, then run the project g For Fast Forward repositories, prefer: ```bash -composer dev-tools tests -- --filter=RelevantTestName -composer dev-tools +cpx --skip-local fast-forward/dev-tools:2.0.0 tests --filter=RelevantTestName +cpx --skip-local fast-forward/dev-tools:2.0.0 ``` Also verify any touched generated or synchronized outputs when relevant: diff --git a/.agents/skills/github-pull-request/references/issue-selection.md b/resources/skills/fast-forward-github-pull-request/references/issue-selection.md similarity index 100% rename from .agents/skills/github-pull-request/references/issue-selection.md rename to resources/skills/fast-forward-github-pull-request/references/issue-selection.md diff --git a/.agents/skills/github-pull-request/references/pr-drafting.md b/resources/skills/fast-forward-github-pull-request/references/pr-drafting.md similarity index 100% rename from .agents/skills/github-pull-request/references/pr-drafting.md rename to resources/skills/fast-forward-github-pull-request/references/pr-drafting.md diff --git a/.agents/skills/github-pull-request/references/review-checklist.md b/resources/skills/fast-forward-github-pull-request/references/review-checklist.md similarity index 78% rename from .agents/skills/github-pull-request/references/review-checklist.md rename to resources/skills/fast-forward-github-pull-request/references/review-checklist.md index 688b33f43c..c513e975a5 100644 --- a/.agents/skills/github-pull-request/references/review-checklist.md +++ b/resources/skills/fast-forward-github-pull-request/references/review-checklist.md @@ -8,8 +8,8 @@ Use this checklist before reporting completion. - The branch contains only issue-related work. - The branch was created from the correct base branch. - Relevant tests were run, or any failure was reported clearly. -- `composer dev-tools` was run before the PR unless the user explicitly constrained the work. -- If `composer dev-tools` reported auto-fixable issues, `composer dev-tools:fix` was used or explicitly considered before finalizing the branch. +- `cpx --skip-local fast-forward/dev-tools:2.0.0` was run before the PR unless the user explicitly constrained the work. +- If `cpx --skip-local fast-forward/dev-tools:2.0.0` reported auto-fixable issues, `cpx --skip-local fast-forward/dev-tools:2.0.0 --fix` was used or explicitly considered before finalizing the branch. - README, docs, wiki, reports, or sync outputs were updated when the change touched them. - The PR title follows repository conventions. - The PR body includes a clear summary, concrete testing notes, and `Closes #123` style closing text. diff --git a/resources/skills/fast-forward-package-readme/LICENSE b/resources/skills/fast-forward-package-readme/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-package-readme/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/package-readme/SKILL.md b/resources/skills/fast-forward-package-readme/SKILL.md similarity index 97% rename from .agents/skills/package-readme/SKILL.md rename to resources/skills/fast-forward-package-readme/SKILL.md index 44313450e1..de5f28253c 100644 --- a/.agents/skills/package-readme/SKILL.md +++ b/resources/skills/fast-forward-package-readme/SKILL.md @@ -1,6 +1,10 @@ --- -name: package-readme +name: fast-forward-package-readme description: Generate or refresh README.md files for Fast Forward PHP packages. Use when an agent needs to create a new package README, reorganize an existing README, improve onboarding and discoverability, or align README structure with the Fast Forward ecosystem. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward README diff --git a/.agents/skills/package-readme/agents/openai.yaml b/resources/skills/fast-forward-package-readme/agents/openai.yaml similarity index 52% rename from .agents/skills/package-readme/agents/openai.yaml rename to resources/skills/fast-forward-package-readme/agents/openai.yaml index 2ecbedad99..74b627bcdf 100644 --- a/.agents/skills/package-readme/agents/openai.yaml +++ b/resources/skills/fast-forward-package-readme/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Fast Forward README" short_description: "Generate consistent README files for Fast Forward packages" - default_prompt: "Use $package-readme to write or refresh a README.md for this Fast Forward PHP package." + default_prompt: "Use $fast-forward-package-readme to write or refresh a README.md for this Fast Forward PHP package." diff --git a/.agents/skills/package-readme/references/badges.md b/resources/skills/fast-forward-package-readme/references/badges.md similarity index 100% rename from .agents/skills/package-readme/references/badges.md rename to resources/skills/fast-forward-package-readme/references/badges.md diff --git a/.agents/skills/package-readme/references/examples.md b/resources/skills/fast-forward-package-readme/references/examples.md similarity index 90% rename from .agents/skills/package-readme/references/examples.md rename to resources/skills/fast-forward-package-readme/references/examples.md index 7fdc2dd539..6fa071bf23 100644 --- a/.agents/skills/package-readme/references/examples.md +++ b/resources/skills/fast-forward-package-readme/references/examples.md @@ -2,11 +2,11 @@ Real examples from Fast Forward packages showing correct structure and formatting. -## dev-tools (Meta Package) +## dev-tools (Isolated CLI) ```markdown # FastForward\DevTools -A Composer plugin that aggregates multiple PHP development tools into a single unified command. +A standalone CLI that aggregates multiple PHP development tools into a single unified command. [![PHP Version](https://img.shields.io/badge/php-^8.3-777BB4?logo=php&logoColor=white)](https://www.php.net/releases/) [![Composer Package](https://img.shields.io/badge/composer-fast--forward%2Fdev--tools-F28D1A.svg?logo=composer&logoColor=white)](https://packagist.org/packages/fast-forward/dev-tools) @@ -25,23 +25,23 @@ A Composer plugin that aggregates multiple PHP development tools into a single u ## šŸ“¦ Installation ```bash -composer require --dev fast-forward/dev-tools:dev-main +cpx --skip-local fast-forward/dev-tools:2.0.0 --help ``` -Requirements: PHP 8.3+, Composer +Requirements: PHP 8.3+, Composer and CPX ## šŸ› ļø Usage -```php -use FastForward\DevTools\Composer\Plugin; -// Plugin automatically registers commands +```bash +cpx --skip-local fast-forward/dev-tools:2.0.0 tests +cpx --skip-local fast-forward/dev-tools:2.0.0 --fix ``` -Run `composer dev-tools` for full suite. +Run `cpx --skip-local fast-forward/dev-tools:2.0.0` for full suite. ## 🧰 API Summary | Command | Description | |---------|-------------| | `dev-tools` | Run all checks | -| `dev-tools:fix` | Auto-fix style issues | +| `dev-tools --fix` | Auto-fix style issues | | `dev-tools tests` | Run PHPUnit | | `dev-tools code-style` | ECS + normalize | @@ -111,4 +111,4 @@ $request = new Request('GET', 'https://example.com'); 5. **Usage**: Code block with minimal viable example 6. **API Summary**: Table format for classes/methods 7. **Integration**: Key dependencies and standards -8. **Links**: Repository, Packagist, RFCs/PSRs at minimum \ No newline at end of file +8. **Links**: Repository, Packagist, RFCs/PSRs at minimum diff --git a/.agents/skills/package-readme/references/update-triggers.md b/resources/skills/fast-forward-package-readme/references/update-triggers.md similarity index 100% rename from .agents/skills/package-readme/references/update-triggers.md rename to resources/skills/fast-forward-package-readme/references/update-triggers.md diff --git a/resources/skills/fast-forward-phpdoc-code-style/LICENSE b/resources/skills/fast-forward-phpdoc-code-style/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-phpdoc-code-style/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/phpdoc-code-style/SKILL.md b/resources/skills/fast-forward-phpdoc-code-style/SKILL.md similarity index 78% rename from .agents/skills/phpdoc-code-style/SKILL.md rename to resources/skills/fast-forward-phpdoc-code-style/SKILL.md index 58f7b7476e..1b8b92d658 100644 --- a/.agents/skills/phpdoc-code-style/SKILL.md +++ b/resources/skills/fast-forward-phpdoc-code-style/SKILL.md @@ -1,6 +1,10 @@ --- -name: phpdoc-code-style -description: Reformat Fast Forward PHP source and tests to match repository PHP style and PHPDoc conventions while preserving behavior and public APIs. Use when an agent needs to clean up PHP formatting, add or repair English PHPDoc, satisfy `composer dev-tools phpdoc`, standardize file headers or contract wording, or prepare PHP changes for a Fast Forward pull request. Pair with `github-pull-request` when the PHPDoc cleanup is part of a larger implementation branch. +name: fast-forward-phpdoc-code-style +description: Reformat Fast Forward PHP source and tests to match repository PHP style and PHPDoc conventions while preserving behavior and public APIs. Use when an agent needs to clean up PHP formatting, add or repair English PHPDoc, satisfy `cpx --skip-local fast-forward/dev-tools:2.0.0 phpdoc`, standardize file headers or contract wording, or prepare PHP changes for a Fast Forward pull request. Pair with `fast-forward-github-pull-request` when the PHPDoc cleanup is part of a larger implementation branch. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward PHPDoc Code Style @@ -14,7 +18,7 @@ Normalize PHP files for Fast Forward repositories without changing runtime behav 3. Normalize formatting and rewrite or add PHPDoc only where it improves correctness or satisfies repository conventions. 4. Before documenting interfaces, abstract contracts, commands, or orchestration services, read [references/anti-patterns.md](references/anti-patterns.md). 5. Use [references/examples.md](references/examples.md) and [references/interface-contract-example.md](references/interface-contract-example.md) when the surrounding code does not provide enough style guidance. -6. Verify with the smallest relevant command. For repository work, prefer `composer dev-tools phpdoc` and broaden validation only when the touched files require it. +6. Verify with the smallest relevant command. For repository work, prefer `cpx --skip-local fast-forward/dev-tools:2.0.0 phpdoc` and broaden validation only when the touched files require it. ## Fast Forward Defaults @@ -24,7 +28,7 @@ Normalize PHP files for Fast Forward repositories without changing runtime behav - Treat the file header as the canonical place for repository-wide license and RFC references when the file already uses that pattern. - Use RFC 2119 keywords conservatively and only where the visible contract supports them. - Prefer docblocks over speculative signature changes when types are uncertain. -- When PHPDoc work is part of an issue branch, also iterate `github-pull-request` so the PR flow explicitly considers this cleanup. +- When PHPDoc work is part of an issue branch, also iterate `fast-forward-github-pull-request` so the PR flow explicitly considers this cleanup. ## Output Contract diff --git a/resources/skills/fast-forward-phpdoc-code-style/agents/openai.yaml b/resources/skills/fast-forward-phpdoc-code-style/agents/openai.yaml new file mode 100644 index 0000000000..4267f84b5b --- /dev/null +++ b/resources/skills/fast-forward-phpdoc-code-style/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward PHPDoc Code Style" + short_description: "Format PHP and PHPDoc for Fast Forward packages" + default_prompt: "Use $fast-forward-phpdoc-code-style to normalize this Fast Forward PHP code and write conservative English PHPDoc that matches repository conventions." diff --git a/.agents/skills/phpdoc-code-style/references/anti-patterns.md b/resources/skills/fast-forward-phpdoc-code-style/references/anti-patterns.md similarity index 100% rename from .agents/skills/phpdoc-code-style/references/anti-patterns.md rename to resources/skills/fast-forward-phpdoc-code-style/references/anti-patterns.md diff --git a/.agents/skills/phpdoc-code-style/references/checklist.md b/resources/skills/fast-forward-phpdoc-code-style/references/checklist.md similarity index 95% rename from .agents/skills/phpdoc-code-style/references/checklist.md rename to resources/skills/fast-forward-phpdoc-code-style/references/checklist.md index c833abb1e5..aee570a3f8 100644 --- a/.agents/skills/phpdoc-code-style/references/checklist.md +++ b/resources/skills/fast-forward-phpdoc-code-style/references/checklist.md @@ -12,7 +12,7 @@ - Prefer concise summaries plus one or two evidence-based sentences over long filler paragraphs. - Add `@param`, `@return`, and `@throws` tags only when the code or nearby context supports them. - Prefer PHPDoc annotations over speculative signature changes when a type is uncertain. -- Run `composer dev-tools phpdoc` after repository edits when feasible. +- Run `cpx --skip-local fast-forward/dev-tools:2.0.0 phpdoc` after repository edits when feasible. ## Evidence Hierarchy diff --git a/.agents/skills/phpdoc-code-style/references/examples.md b/resources/skills/fast-forward-phpdoc-code-style/references/examples.md similarity index 100% rename from .agents/skills/phpdoc-code-style/references/examples.md rename to resources/skills/fast-forward-phpdoc-code-style/references/examples.md diff --git a/.agents/skills/phpdoc-code-style/references/fast-forward-conventions.md b/resources/skills/fast-forward-phpdoc-code-style/references/fast-forward-conventions.md similarity index 88% rename from .agents/skills/phpdoc-code-style/references/fast-forward-conventions.md rename to resources/skills/fast-forward-phpdoc-code-style/references/fast-forward-conventions.md index cf3882294a..e0ecfeec78 100644 --- a/.agents/skills/phpdoc-code-style/references/fast-forward-conventions.md +++ b/resources/skills/fast-forward-phpdoc-code-style/references/fast-forward-conventions.md @@ -43,15 +43,15 @@ Preserve this shape when it already exists. When adding a missing header, match For PHPDoc-focused changes, prefer: ```bash -composer dev-tools phpdoc +cpx --skip-local fast-forward/dev-tools:2.0.0 phpdoc ``` If the change is part of a larger implementation, it can be reasonable to run: ```bash -composer dev-tools +cpx --skip-local fast-forward/dev-tools:2.0.0 ``` ## Pairing -When a PHPDoc cleanup is part of a branch that will become a pull request, also iterate `github-pull-request` so the PR flow accounts for PHPDoc verification and summary. +When a PHPDoc cleanup is part of a branch that will become a pull request, also iterate `fast-forward-github-pull-request` so the PR flow accounts for PHPDoc verification and summary. diff --git a/.agents/skills/phpdoc-code-style/references/interface-contract-example.md b/resources/skills/fast-forward-phpdoc-code-style/references/interface-contract-example.md similarity index 100% rename from .agents/skills/phpdoc-code-style/references/interface-contract-example.md rename to resources/skills/fast-forward-phpdoc-code-style/references/interface-contract-example.md diff --git a/resources/skills/fast-forward-phpunit-tests/LICENSE b/resources/skills/fast-forward-phpunit-tests/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-phpunit-tests/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/phpunit-tests/SKILL.md b/resources/skills/fast-forward-phpunit-tests/SKILL.md similarity index 80% rename from .agents/skills/phpunit-tests/SKILL.md rename to resources/skills/fast-forward-phpunit-tests/SKILL.md index 57218971f2..c2f750760a 100644 --- a/.agents/skills/phpunit-tests/SKILL.md +++ b/resources/skills/fast-forward-phpunit-tests/SKILL.md @@ -1,6 +1,10 @@ --- -name: phpunit-tests +name: fast-forward-phpunit-tests description: Generate, extend, and repair PHP unit tests with PHPUnit and Prophecy while respecting PSR-12, Composer PHP constraints, project namespaces, and local testing abstractions. Use when an agent needs to create or update PHPUnit test classes, add coverage attributes, isolate dependencies with Prophecy, infer namespaces from composer.json, reuse abstract test cases or data providers, or run focused commands to validate new PHP tests in an existing codebase. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward PHPUnit Tests @@ -76,64 +80,27 @@ Prefer PHPUnit attributes over docblock annotations when the installed PHPUnit v Choose the smallest command that proves the change. -- If `fast-forward/dev-tools` appears as a Composer dependency, run `composer dev-tools tests --coverage=public/coverage --filter=TestClassName`. -- Otherwise prefer a project-defined composer script for tests. +- Prefer a project-defined Composer script for tests after inspecting its runner. +- For a project using DevTools 2.0 externally, run `cpx --skip-local fast-forward/dev-tools:2.0.0 tests --coverage=public/coverage --filter=TestClassName` from the consumer repository. - If there is no suitable script, run the repository's direct PHPUnit command with a class or file filter. - Always run the newly created or modified tests. - Re-run after fixes until the targeted tests pass. - If failures come from unrelated pre-existing breakage, call that out separately and do not silently claim success. -### Composer plugin validation flow +### Isolated CLI validation -Some repository test scenarios are only valid when DevTools is executed as a Composer plugin. -Create a temporary fixture under `backup/` before running plugin-based verification. +DevTools 2.0 runs outside the consumer Composer dependency graph. Keep application +dependencies installed in the consumer repository and execute CPX from that +working directory. Do not add DevTools to the consumer's `require-dev` solely to +run checks, and do not approve a DevTools Composer plugin. ```bash -PROJECT_ROOT="$(pwd)" -PLUGIN_FIXTURE="$PROJECT_ROOT/backup/composer-plugin-consumer" -mkdir -p "$PROJECT_ROOT/backup" -rm -rf "$PLUGIN_FIXTURE" -mkdir -p "$PLUGIN_FIXTURE" - -cat > "$PLUGIN_FIXTURE/composer.json" <<'JSON' -{ - "name": "fast-forward/dev-tools-composer-plugin-consumer-fixture", - "description": "Fixture project used to validate DevTools plugin behavior.", - "license": "MIT", - "type": "project", - "require": { - "fast-forward/dev-tools": "*@dev" - }, - "repositories": [ - { - "type": "path", - "url": "../..", - "options": { - "symlink": true - } - } - ], - "minimum-stability": "dev", - "prefer-stable": true, - "config": { - "allow-plugins": { - "ergebnis/composer-normalize": true, - "fast-forward/dev-tools": true, - "phpdocumentor/shim": true, - "phpro/grumphp-shim": true, - "pyrech/composer-changelogs": true - } - }, - "scripts": { - "dev-tools": "dev-tools", - "dev-tools:fix": "@dev-tools --fix" - } -} -JSON - -cd "$PLUGIN_FIXTURE" composer install -composer tests +cpx --skip-local fast-forward/dev-tools:2.0.0 tests --filter=TestClassName ``` +For DevTools source changes, use the checkout's own `php bin/dev-tools` so the +candidate is tested instead of the published package. Test consumer isolation in +a disposable directory; preserve the real repository and its dependency graph. + Read [references/generation-checklist.md](references/generation-checklist.md) before writing or repairing tests in an unfamiliar repository. diff --git a/resources/skills/fast-forward-phpunit-tests/agents/openai.yaml b/resources/skills/fast-forward-phpunit-tests/agents/openai.yaml new file mode 100644 index 0000000000..b58d5d3a73 --- /dev/null +++ b/resources/skills/fast-forward-phpunit-tests/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward PHPUnit Tests" + short_description: "Generate PHPUnit tests using local Prophecy patterns" + default_prompt: "Use $fast-forward-phpunit-tests to create or update PHPUnit tests for this PHP codebase using local conventions and Prophecy when applicable." diff --git a/.agents/skills/phpunit-tests/references/generation-checklist.md b/resources/skills/fast-forward-phpunit-tests/references/generation-checklist.md similarity index 100% rename from .agents/skills/phpunit-tests/references/generation-checklist.md rename to resources/skills/fast-forward-phpunit-tests/references/generation-checklist.md diff --git a/.agents/skills/phpunit-tests/references/project-discovery.md b/resources/skills/fast-forward-phpunit-tests/references/project-discovery.md similarity index 100% rename from .agents/skills/phpunit-tests/references/project-discovery.md rename to resources/skills/fast-forward-phpunit-tests/references/project-discovery.md diff --git a/resources/skills/fast-forward-pull-request-review/LICENSE b/resources/skills/fast-forward-pull-request-review/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-pull-request-review/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/pull-request-review/SKILL.md b/resources/skills/fast-forward-pull-request-review/SKILL.md similarity index 92% rename from .agents/skills/pull-request-review/SKILL.md rename to resources/skills/fast-forward-pull-request-review/SKILL.md index fcc816d6f0..f16d9e7bec 100644 --- a/.agents/skills/pull-request-review/SKILL.md +++ b/resources/skills/fast-forward-pull-request-review/SKILL.md @@ -1,6 +1,10 @@ --- -name: pull-request-review +name: fast-forward-pull-request-review description: Review Fast Forward pull requests with a findings-first contract that prioritizes bugs, regressions, missing tests, missing docs, generated-output drift, and workflow risk over general summaries. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward Pull Request Review @@ -37,7 +41,7 @@ possible. workflow or CI risks, generated-output drift, and sync side effects over stylistic nits. - Treat ``.github/workflows``, ``resources/github-actions``, ``.github/actions``, - ``.agents/skills``, ``.agents/agents``, ``README.md``, ``docs/``, + ``resources/skills``, ``.agents/agents``, ``README.md``, ``docs/``, ``CHANGELOG.md``, and ``.github/wiki`` as high-signal review surfaces. - When ``.github/wiki`` moves, verify whether the wiki preview or wiki maintenance workflow is expected to refresh the submodule pointer before diff --git a/.agents/skills/pull-request-review/agents/openai.yaml b/resources/skills/fast-forward-pull-request-review/agents/openai.yaml similarity index 51% rename from .agents/skills/pull-request-review/agents/openai.yaml rename to resources/skills/fast-forward-pull-request-review/agents/openai.yaml index e949079bc0..6ad3f19869 100644 --- a/.agents/skills/pull-request-review/agents/openai.yaml +++ b/resources/skills/fast-forward-pull-request-review/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Fast Forward Rigorous Review" short_description: "Review Fast Forward pull requests with a findings-first contract" - default_prompt: "Use $pull-request-review to perform a rigorous findings-first review of this Fast Forward pull request." + default_prompt: "Use $fast-forward-pull-request-review to perform a rigorous findings-first review of this Fast Forward pull request." diff --git a/.agents/skills/pull-request-review/references/review-contract.md b/resources/skills/fast-forward-pull-request-review/references/review-contract.md similarity index 100% rename from .agents/skills/pull-request-review/references/review-contract.md rename to resources/skills/fast-forward-pull-request-review/references/review-contract.md diff --git a/.agents/skills/pull-request-review/references/surface-priorities.md b/resources/skills/fast-forward-pull-request-review/references/surface-priorities.md similarity index 96% rename from .agents/skills/pull-request-review/references/surface-priorities.md rename to resources/skills/fast-forward-pull-request-review/references/surface-priorities.md index f950762ce1..77d55a8d48 100644 --- a/.agents/skills/pull-request-review/references/surface-priorities.md +++ b/resources/skills/fast-forward-pull-request-review/references/surface-priorities.md @@ -14,7 +14,7 @@ Highest priority release behavior, or CI guarantees may have changed. - ``resources/github-actions/`` when a repository-facing workflow wrapper can drift from the reusable workflow implementation. -- ``.agents/skills/`` and ``.agents/agents/`` when packaged prompts or +- ``resources/skills/`` and ``.agents/agents/`` when packaged prompts or consumer-synchronized capabilities changed. Additional Fast Forward review surfaces diff --git a/.agents/skills/pull-request-review/references/workflow-action-validation.md b/resources/skills/fast-forward-pull-request-review/references/workflow-action-validation.md similarity index 100% rename from .agents/skills/pull-request-review/references/workflow-action-validation.md rename to resources/skills/fast-forward-pull-request-review/references/workflow-action-validation.md diff --git a/resources/skills/fast-forward-sphinx-docs/LICENSE b/resources/skills/fast-forward-sphinx-docs/LICENSE new file mode 100644 index 0000000000..6374901e92 --- /dev/null +++ b/resources/skills/fast-forward-sphinx-docs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.agents/skills/sphinx-docs/SKILL.md b/resources/skills/fast-forward-sphinx-docs/SKILL.md similarity index 97% rename from .agents/skills/sphinx-docs/SKILL.md rename to resources/skills/fast-forward-sphinx-docs/SKILL.md index e6de21a56c..1897ee9187 100644 --- a/.agents/skills/sphinx-docs/SKILL.md +++ b/resources/skills/fast-forward-sphinx-docs/SKILL.md @@ -1,6 +1,10 @@ --- -name: sphinx-docs +name: fast-forward-sphinx-docs description: Generate or refresh Sphinx reStructuredText documentation for Fast Forward PHP packages. Use when an agent needs to create package docs in `docs/`, expand existing `.rst` sections, document APIs or workflows, or keep Fast Forward documentation structure consistent. +license: MIT +compatibility: DevTools commands support fast-forward/dev-tools 2.0; run the isolated CLI with CPX or a repository-defined script. Inspect the repository PHP and tool versions before use. +metadata: + api-baseline: "2.0.0" --- # Fast Forward Sphinx Docs diff --git a/resources/skills/fast-forward-sphinx-docs/agents/openai.yaml b/resources/skills/fast-forward-sphinx-docs/agents/openai.yaml new file mode 100644 index 0000000000..10c5f350e4 --- /dev/null +++ b/resources/skills/fast-forward-sphinx-docs/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fast Forward Sphinx Docs" + short_description: "Generate Sphinx docs for Fast Forward packages" + default_prompt: "Use $fast-forward-sphinx-docs to generate or revise Sphinx documentation for this Fast Forward PHP package." diff --git a/.agents/skills/sphinx-docs/references/rst-examples.md b/resources/skills/fast-forward-sphinx-docs/references/rst-examples.md similarity index 100% rename from .agents/skills/sphinx-docs/references/rst-examples.md rename to resources/skills/fast-forward-sphinx-docs/references/rst-examples.md diff --git a/.agents/skills/sphinx-docs/references/structure.md b/resources/skills/fast-forward-sphinx-docs/references/structure.md similarity index 100% rename from .agents/skills/sphinx-docs/references/structure.md rename to resources/skills/fast-forward-sphinx-docs/references/structure.md diff --git a/.agents/skills/sphinx-docs/references/update-triggers.md b/resources/skills/fast-forward-sphinx-docs/references/update-triggers.md similarity index 97% rename from .agents/skills/sphinx-docs/references/update-triggers.md rename to resources/skills/fast-forward-sphinx-docs/references/update-triggers.md index 01f110807b..046a1725fd 100644 --- a/.agents/skills/sphinx-docs/references/update-triggers.md +++ b/resources/skills/fast-forward-sphinx-docs/references/update-triggers.md @@ -35,7 +35,7 @@ changelog, without PR progress or execution receipts. ```bash # Build HTML docs locally -composer dev-tools docs +cpx --skip-local fast-forward/dev-tools:2.0.0 docs # View in browser (from public/) # Or serve locally diff --git a/skills.json b/skills.json new file mode 100644 index 0000000000..d32a2e4f21 --- /dev/null +++ b/skills.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/roxblnfk/skills/1.15.0/resources/skills.schema.json", + "target": ".agents/skills", + "aliases": [], + "discovery": false, + "auto-sync": true, + "vendor-sources": false, + "dependencies": { + "composer": { + "trusted": [ + "fast-forward/changelog" + ], + "trusted-replace": true + } + }, + "sources": [ + { + "from": "dir", + "path": "resources/skills", + "package": "fast-forward/dev-tools" + }, + { + "from": "github", + "package": "php-fast-forward/changelog", + "ref": "v0.3.0", + "skills": ["fast-forward-changelog"] + } + ] +} diff --git a/src/CodeOwners/CodeOwnersGenerator.php b/src/CodeOwners/CodeOwnersGenerator.php index 4d2c439ad0..9d2e2cdd5b 100644 --- a/src/CodeOwners/CodeOwnersGenerator.php +++ b/src/CodeOwners/CodeOwnersGenerator.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Composer\Json\Schema\AuthorInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use Symfony\Component\Config\FileLocatorInterface; + use function Safe\preg_match; use function Safe\parse_url; use function Safe\preg_replace; diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php deleted file mode 100644 index 105d49f508..0000000000 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Composer\Capability; - -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Composer\Command\ProxyCommand; -use FastForward\DevTools\Composer\DevToolsPluginInterface; -use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Container\ContainerFactory; -use Symfony\Component\Console\Command\Command; - -/** - * Provides a registry of custom dev-tools commands mapped for Composer integration. - * This capability struct MUST implement the defined `CommandProvider`. - */ -final readonly class DevToolsCommandProvider implements CommandProvider -{ - /** - * @var string the namespace prefix for dev-tools console commands to be registered as Composer commands - */ - private const string COMMAND_NAMESPACE = 'FastForward\DevTools\Console\Command'; - - private ?DevToolsPluginInterface $plugin; - - /** - * @param array $constructorArguments the Composer capability constructor arguments - */ - public function __construct(array $constructorArguments = []) - { - $plugin = $constructorArguments['plugin'] ?? null; - $this->plugin = $plugin instanceof DevToolsPluginInterface ? $plugin : null; - } - - /** - * {@inheritDoc} - */ - public function getCommands() - { - $commands = []; - - foreach (ContainerFactory::get(DevTools::class)->all() as $registeredName => $command) { - /** - * Composer plugin registrations must be canonicalized to one command per Symfony command. - * The application exposes alias keys in `all()`, but Composer interprets each entry as - * an independent command and emits override warnings. - */ - if ($registeredName !== $command->getName()) { - continue; - } - - if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) { - continue; - } - - if ($this->isRegisteredCommand($command->getName())) { - continue; - } - - $commands[] = new ProxyCommand($command, $this->getComposerAliases($command)); - } - - return $commands; - } - - /** - * Returns command aliases that may be safely exposed to Composer. - * - * @param Command $command the Symfony command being proxied - * - * @return list - */ - private function getComposerAliases(Command $command): array - { - return array_values(array_filter( - $command->getAliases(), - fn(string $alias): bool => ! $this->isRegisteredCommand($alias), - )); - } - - /** - * Detects names already owned by Composer's active command surface. - * - * @param string|null $name the command name or alias being evaluated - */ - private function isRegisteredCommand(?string $name): bool - { - return $this->plugin?->isRegisteredCommand($name) ?? false; - } -} diff --git a/src/Composer/Command/ProxyCommand.php b/src/Composer/Command/ProxyCommand.php deleted file mode 100644 index a83a46af3f..0000000000 --- a/src/Composer/Command/ProxyCommand.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Composer\Command; - -use Composer\Command\BaseCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Adapts migrated Symfony commands to Composer's BaseCommand contract. - */ -final class ProxyCommand extends BaseCommand -{ - /** - * @param Command $command the Symfony command adapted for Composer plugin execution - * @param list|null $aliases the optional alias list exposed to Composer - */ - public function __construct( - private readonly Command $command, - ?array $aliases = null, - ) { - parent::__construct($this->command->getName()); - - $this - ->setAliases($aliases ?? $this->command->getAliases()) - ->setDescription($this->command->getDescription()) - ->setHelp($this->command->getHelp()) - ->setDefinition(clone $this->command->getDefinition()) - ->setHidden($this->command->isHidden()); - } - - /** - * Executes the proxied Symfony command through Composer's command contract. - * - * @param InputInterface $input the Composer command input - * @param OutputInterface $output the Composer command output - * - * @return int the proxied command status code - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - return $this->command->run($input, $output); - } -} diff --git a/src/Composer/DevToolsPluginInterface.php b/src/Composer/DevToolsPluginInterface.php deleted file mode 100644 index d0f83ab931..0000000000 --- a/src/Composer/DevToolsPluginInterface.php +++ /dev/null @@ -1,93 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Composer; - -use Composer\Plugin\PluginInterface; - -/** - * Defines DevTools-specific Composer plugin conventions. - */ -interface DevToolsPluginInterface extends PluginInterface -{ - /** - * @var list composer command names and aliases that DevTools MUST NOT override - */ - public const array COMPOSER_COMMAND_NAMES = [ - '_complete', - 'about', - 'archive', - 'audit', - 'browse', - 'bump', - 'cc', - 'check-platform-reqs', - 'clear-cache', - 'clearcache', - 'completion', - 'config', - 'create-project', - 'depends', - 'diagnose', - 'dump-autoload', - 'dumpautoload', - 'exec', - 'fund', - 'global', - 'help', - 'home', - 'i', - 'info', - 'init', - 'install', - 'licenses', - 'list', - 'outdated', - 'prohibits', - 'r', - 'reinstall', - 'remove', - 'repo', - 'repository', - 'require', - 'rm', - 'run', - 'run-script', - 'search', - 'self-update', - 'selfupdate', - 'show', - 'status', - 'suggests', - 'u', - 'uninstall', - 'update', - 'upgrade', - 'validate', - 'why', - 'why-not', - ]; - - /** - * Detects whether a command name or alias is already registered in Composer's command surface. - * - * @param string|null $name the command name or alias being evaluated - */ - public function isRegisteredCommand(?string $name): bool; -} diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index 5367d31dea..45aaaea64b 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -29,6 +29,7 @@ use FastForward\DevTools\Composer\Json\Schema\SupportInterface; use FastForward\DevTools\Path\WorkingProjectPathResolver; use UnderflowException; + use function Safe\file_get_contents; use function Safe\json_decode; @@ -105,7 +106,7 @@ public function __construct(?string $path = null) */ public function getName(): string { - return $this->data['name']; + return $this->data['name'] ?? ''; } /** @@ -119,7 +120,7 @@ public function getName(): string */ public function getDescription(): string { - return $this->data['description']; + return $this->data['description'] ?? ''; } /** diff --git a/src/Composer/Plugin.php b/src/Composer/Plugin.php deleted file mode 100644 index e57b8e0d49..0000000000 --- a/src/Composer/Plugin.php +++ /dev/null @@ -1,179 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Composer; - -use Composer\Composer; -use Composer\EventDispatcher\EventSubscriberInterface; -use Composer\IO\IOInterface; -use Composer\Plugin\Capability\CommandProvider; -use Composer\Plugin\Capable; -use Composer\Script\Event; -use Composer\Script\ScriptEvents; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; - -/** - * Implements the lifecycle of the Composer dev-tools extension framework. - * This plugin class MUST initialize and coordinate custom script registrations securely. - */ -final class Plugin implements Capable, DevToolsPluginInterface, EventSubscriberInterface -{ - private ?Composer $composer = null; - - /** - * Resolves the implemented Composer capabilities structure. - * - * This method MUST map the primary capability handlers to custom implementations. - * It SHALL describe how tools seamlessly integrate into the execution layer. - * - * @return array the capability mapping configurations - */ - public function getCapabilities(): array - { - return [ - CommandProvider::class => DevToolsCommandProvider::class, - ]; - } - - /** - * Retrieves the comprehensive map of events this listener SHALL handle. - * - * This method MUST define the lifecycle triggers for script installation and - * synchronization during Composer package operations. - * - * @return array the event mapping registry - */ - public static function getSubscribedEvents(): array - { - return [ - ScriptEvents::POST_INSTALL_CMD => 'runSyncCommand', - ScriptEvents::POST_UPDATE_CMD => 'runSyncCommand', - ]; - } - - /** - * Handles the automated script installation. - * - * This method MUST execute the `dev-tools:sync` command after relevant Composer operations to ensure - * the development tools are correctly synchronized with the current project state. - * - * @param Event $event the Composer script event context - * - * @return void - */ - public function runSyncCommand(Event $event): void - { - $event->getComposer() - ->getLoop() - ->getProcessExecutor() - ->execute('vendor/bin/dev-tools dev-tools:sync'); - } - - /** - * Detects whether a command name or alias is already registered in Composer's command surface. - * - * @param string|null $name the command name or alias being evaluated - */ - public function isRegisteredCommand(?string $name): bool - { - return null !== $name && \in_array($name, $this->getReservedCommandNames(), true); - } - - /** - * Returns command names and aliases that DevTools plugin commands MUST NOT override. - * - * @return list - */ - private function getReservedCommandNames(): array - { - return array_values(array_unique([ - ...self::COMPOSER_COMMAND_NAMES, - ...$this->getRootScriptCommandNames(), - ])); - } - - /** - * Handles activation lifecycle events for the Composer session. - * - * This method MUST adhere to the standard Composer plugin activation protocol, even if no specific logic is required. - * - * @param Composer $composer the primary package configuration instance over Composer - * @param IOInterface $io interactive communication channels - * - * @return void - */ - public function activate(Composer $composer, IOInterface $io): void - { - $this->composer = $composer; - } - - /** - * Cleans up operations during Composer plugin deactivation events. - * - * This method MUST implement the standard Composer lifecycle correctly, even if vacant. - * - * @param Composer $composer the primary metadata controller object - * @param IOInterface $io defined interactions proxy - * - * @return void - */ - public function deactivate(Composer $composer, IOInterface $io): void - { - $this->composer = null; - } - - /** - * Handles final uninstallation processes logically. - * - * This method MUST manage cleanup duties per Composer constraints, even if empty. - * - * @param Composer $composer system package registry utility - * @param IOInterface $io execution runtime outputs and inputs proxy interface - * - * @return void - */ - public function uninstall(Composer $composer, IOInterface $io): void - { - $this->composer = null; - } - - /** - * Returns custom Composer script command names from the active root package. - * - * @return list - */ - private function getRootScriptCommandNames(): array - { - if (! $this->composer instanceof Composer) { - return []; - } - - $names = []; - - foreach (array_keys($this->composer->getPackage()->getScripts()) as $script) { - if (\defined(ScriptEvents::class . '::' . str_replace('-', '_', strtoupper($script)))) { - continue; - } - - $names[] = $script; - } - - return $names; - } -} diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index a3da7689cb..e2e2fb0b27 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -163,7 +163,6 @@ public static function applyPackagedRepositoryIgnores(Configuration $configurati self::DEFAULT_PACKAGED_PROD_ONLY_IN_DEV_DEPENDENCIES, [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV], ); - $configuration->ignoreErrorsOnPackage('composer/composer', [ErrorType::DEV_DEPENDENCY_IN_PROD]); return $configuration; } diff --git a/src/Config/ECSConfig.php b/src/Config/ECSConfig.php index bd2b3fd45f..8fc61fa43a 100644 --- a/src/Config/ECSConfig.php +++ b/src/Config/ECSConfig.php @@ -45,6 +45,7 @@ use Symplify\CodingStandard\Fixer\Strict\BlankLineAfterStrictTypesFixer; use Symplify\EasyCodingStandard\Config\ECSConfig as ECSConfigInterface; use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; + use function Safe\getcwd; /** diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index f8b8a0999a..a8a07879c0 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -30,6 +30,7 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveMixedDocblockOverruledByNativeTypeRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector; use Rector\Set\ValueObject\SetList; + use function Safe\getcwd; /** diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php deleted file mode 100644 index c4bdce0855..0000000000 --- a/src/Console/Command/AgentsCommand.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Console\Command; - -use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use FastForward\DevTools\Console\Input\HasJsonOption; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Synchronizes packaged Fast Forward project agents into the consumer repository. - */ -#[AsCommand( - name: 'agents:agents', - description: 'Synchronizes Fast Forward project agents into .agents/agents directory.', - aliases: ['agents'], -)] -final class AgentsCommand extends Command -{ - use HasJsonOption; - use LogsCommandResults; - - private const string AGENTS_DIRECTORY = '.agents/agents'; - - /** - * @param PackagedDirectorySynchronizer $synchronizer - * @param FilesystemInterface $filesystem - */ - public function __construct( - private readonly PackagedDirectorySynchronizer $synchronizer, - private readonly FilesystemInterface $filesystem, - ) { - parent::__construct(); - } - - /** - * Configures JSON output options for the synchronization command. - */ - protected function configure(): void - { - $this->setHelp( - 'This command ensures the consumer repository contains linked Fast Forward project agents by creating' - . ' symlinks to the packaged prompts and removing broken links.' - ); - $this->addJsonOption(); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $packageAgentsPath = DevToolsPathResolver::getPackagePath(self::AGENTS_DIRECTORY); - $agentsDir = $this->filesystem->getAbsolutePath(self::AGENTS_DIRECTORY); - $this->log('Starting agents synchronization...', $input); - - if (! $this->filesystem->exists($packageAgentsPath)) { - return $this->failure( - 'No packaged .agents/agents found at: {packaged_agents_path}', - $input, - [ - 'packaged_agents_path' => $packageAgentsPath, - 'agents_dir' => $agentsDir, - 'directory_created' => false, - ], - ); - } - - $directoryCreated = false; - - if (! $this->filesystem->exists($agentsDir)) { - $this->filesystem->mkdir($agentsDir); - $directoryCreated = true; - $this->log('Created .agents/agents directory.', $input); - } - - $result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY); - - if ($result->failed()) { - return $this->failure( - 'Agents synchronization failed.', - $input, - [ - 'packaged_agents_path' => $packageAgentsPath, - 'agents_dir' => $agentsDir, - 'directory_created' => $directoryCreated, - ], - ); - } - - return $this->success( - 'Agents synchronization completed successfully.', - $input, - [ - 'packaged_agents_path' => $packageAgentsPath, - 'agents_dir' => $agentsDir, - 'directory_created' => $directoryCreated, - ], - ); - } -} diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index f3953680ef..64379558cc 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Symfony\Component\Config\FileLocatorInterface; @@ -102,7 +103,7 @@ protected function configure(): void /** * Executes the code style checks and fixes block. * - * The method MUST execute `composer update --lock`, `composer normalize`, and ECS using secure processes. + * The method MUST execute the runtime Composer Normalize plugin and ECS without updating dependency locks. * It SHALL return `self::SUCCESS` if all commands succeed, or `self::FAILURE` otherwise. * * @param InputInterface $input the input interface to retrieve options @@ -120,15 +121,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('Running code style checks and fixes...', $input); - $composerUpdate = $this->processBuilder - ->withArgument('--lock') - ->withArgument('--quiet') - ->build('composer update'); - $composerNormalize = $this->processBuilder ->withArgument('--ansi') ->withArgument($fix ? '--quiet' : '--dry-run') - ->build('composer normalize'); + ->withArgument('--no-update-lock') + ->build([ + 'composer', + '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2), + 'normalize', + WorkingProjectPathResolver::getProjectPath('composer.json'), + ]); $processBuilder = $this->processBuilder ->withArgument('--ansi') @@ -148,7 +150,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $ecs = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('ecs')]); - $this->processQueue->add(process: $composerUpdate, label: 'Refreshing Composer Lock'); $this->processQueue->add( process: $composerNormalize, label: 'Normalizing composer.json with Composer Normalize' diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index 26b993691d..e5962a9dd2 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use InvalidArgumentException; @@ -34,6 +35,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function is_numeric; /** @@ -292,7 +294,13 @@ private function getComposerNormalizeCommand(): Process { return $this->processBuilder ->withArgument('--ansi') - ->build('composer normalize'); + ->withArgument('--no-update-lock') + ->build([ + 'composer', + '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2), + 'normalize', + WorkingProjectPathResolver::getProjectPath('composer.json'), + ]); } /** diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index 7f1cf34685..e718cbebff 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -38,6 +38,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; + use function Safe\getcwd; /** diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index 088e4560d0..dd67c980b6 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -25,6 +25,8 @@ use FastForward\DevTools\Funding\ComposerFundingCodec; use FastForward\DevTools\Funding\FundingProfileMerger; use FastForward\DevTools\Funding\FundingYamlCodec; +use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Resource\FileDiffer; @@ -488,20 +490,13 @@ private function normalizeComposerFile(string $composerFile, OutputInterface $ou ->withArgument('--ansi') ->withArgument('--no-update-lock'); - $workingDirectory = $this->filesystem->getDirectory($composerFile); - - if ('.' !== $workingDirectory) { - $processBuilder = $processBuilder->withArgument('--working-dir', $workingDirectory); - } - - $composerBasename = $this->filesystem->getBasename($composerFile); - - if ('composer.json' !== $composerBasename) { - $processBuilder = $processBuilder->withArgument('--file', $composerBasename); - } - $this->processQueue->add( - process: $processBuilder->build('composer normalize'), + process: $processBuilder->build([ + 'composer', + '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2), + 'normalize', + WorkingProjectPathResolver::getProjectPath($composerFile), + ]), label: 'Normalizing composer.json with Composer Normalize', ); diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index 0bc3c9db23..ebd61497fb 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -38,6 +38,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; + use function Safe\getcwd; /** diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index d83004e5eb..2f1752ff4a 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -128,7 +128,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $sourcePath = $this->fileLocator->locate((string) $input->getOption('source')); - $projectPath = (string) $this->filesystem->getAbsolutePath('.'); + $this->filesystem->getAbsolutePath('.'); $targetPath = (string) $this->filesystem->getAbsolutePath((string) $input->getOption('target')); $overwrite = ! $input->getOption('no-overwrite'); $dryRun = (bool) $input->getOption('dry-run'); @@ -146,7 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($files as $file) { $sourcePath = $file->getRealPath(); $sourceContents = $this->filesystem->readFile($sourcePath); - $renderedSourceContents = $this->hookContentRenderer->render($sourceContents, $projectPath); + $renderedSourceContents = $this->hookContentRenderer->render($sourceContents); $hookPath = Path::join($targetPath, $file->getRelativePathname()); if (($dryRun || $check) && ! $this->filesystem->exists($hookPath)) { diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 3bfd6c824e..97e101996e 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -31,6 +31,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; + use function rtrim; #[AsCommand( diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 00eecfbdde..0bf05cf95b 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -32,6 +32,7 @@ use Psr\Log\LogLevel; use Twig\Environment; use Throwable; +use UnderflowException; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -271,12 +272,18 @@ private function ensureDocHeaderExists(InputInterface $input): void 'rfc2119' => 'https://datatracker.ietf.org/doc/html/rfc2119', ])); + try { + $copyrightHolder = (string) $this->composer->getAuthors(true); + } catch (UnderflowException) { + $copyrightHolder = ''; + } + $docHeader = $this->renderer->render('docblock/.docheader', [ 'package' => $this->composer->getName(), 'description' => rtrim($this->composer->getDescription(), '.'), 'year' => $this->clock->now() ->format('Y'), - 'copyright_holder' => (string) $this->composer->getAuthors(true), + 'copyright_holder' => $copyrightHolder, 'license' => $this->composer->getLicense(), 'links' => $links, ]); diff --git a/src/Console/Command/RuntimeCommand.php b/src/Console/Command/RuntimeCommand.php new file mode 100644 index 0000000000..94923fd624 --- /dev/null +++ b/src/Console/Command/RuntimeCommand.php @@ -0,0 +1,78 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console\Command; + +use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +use function Safe\json_encode; + +/** + * Exposes runtime paths without relying on an executor's private cache layout. + * + * The command MUST emit one JSON object without changing the working project. + */ +#[AsCommand(name: 'runtime', description: 'Prints the active DevTools runtime as JSON.', aliases: [])] +final class RuntimeCommand extends Command +{ + use HasJsonOption; + + /** + * @param VersionCheckerInterface $versionChecker resolves the active package version + */ + public function __construct( + private readonly VersionCheckerInterface $versionChecker + ) { + parent::__construct(); + } + + /** + * @return void + */ + protected function configure(): void + { + $this->addJsonOption(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln(json_encode([ + 'binary' => DevToolsPathResolver::getBinaryPath(), + 'autoload' => DevToolsPathResolver::getRuntimeAutoloadPath(), + 'package_root' => DevToolsPathResolver::getPackagePath(), + 'project_root' => WorkingProjectPathResolver::getProjectPath(), + 'version' => $this->versionChecker->getCurrentVersion(), + ], \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR)); + + return self::SUCCESS; + } +} diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php deleted file mode 100644 index 38f3058e28..0000000000 --- a/src/Console/Command/SkillsCommand.php +++ /dev/null @@ -1,158 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Console\Command; - -use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use FastForward\DevTools\Console\Input\HasJsonOption; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Synchronizes packaged Fast Forward skills into the consumer repository. - * - * This command SHALL ensure that the consumer repository contains the expected - * `.agents/skills` directory structure backed by the packaged skill set. The - * command MUST verify that the packaged skills directory exists before any - * synchronization is attempted. If the target skills directory does not exist, - * it SHALL be created before the synchronization process begins. - * - * The synchronization workflow is delegated to {@see PackagedDirectorySynchronizer}. This - * command MUST act as an orchestration layer only: it prepares the source and - * target paths, triggers synchronization, and translates the resulting status - * into Symfony Console output and process exit codes. - */ -#[AsCommand( - name: 'agents:skills', - description: 'Synchronizes Fast Forward skills into .agents/skills directory.', - aliases: ['skills'] -)] -final class SkillsCommand extends Command -{ - use HasJsonOption; - use LogsCommandResults; - - private const string SKILLS_DIRECTORY = '.agents/skills'; - - /** - * Initializes the command with an optional skills synchronizer instance. - * - * @param PackagedDirectorySynchronizer $synchronizer the synchronizer responsible - * for applying the skills - * synchronization process - * @param FilesystemInterface $filesystem filesystem used to resolve - * and manage the skills - * directory structure - */ - public function __construct( - private readonly PackagedDirectorySynchronizer $synchronizer, - private readonly FilesystemInterface $filesystem, - ) { - parent::__construct(); - } - - /** - * Configures the supported JSON output option. - */ - protected function configure(): void - { - $this->setHelp( - 'This command ensures the consumer repository contains linked Fast Forward skills by creating' - . ' symlinks to the packaged skills and removing broken links.' - ); - $this->addJsonOption(); - } - - /** - * Executes the skills synchronization workflow. - * - * This method SHALL: - * - announce the start of synchronization; - * - resolve the packaged skills path and consumer target directory; - * - fail when the packaged skills directory does not exist; - * - create the target directory when it is missing; - * - delegate synchronization to {@see PackagedDirectorySynchronizer}; - * - return a success or failure exit code based on the synchronization result. - * - * The command MUST return {@see self::FAILURE} when packaged skills are not - * available or when the synchronizer reports a failure. It MUST return - * {@see self::SUCCESS} only when synchronization completes successfully. - * - * @param InputInterface $input the console input instance provided by Symfony - * @param OutputInterface $output the console output instance used to report progress - * - * @return int The process exit status. This MUST be {@see self::SUCCESS} on - * success and {@see self::FAILURE} on failure. - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $packageSkillsPath = DevToolsPathResolver::getPackagePath(self::SKILLS_DIRECTORY); - $skillsDir = $this->filesystem->getAbsolutePath(self::SKILLS_DIRECTORY); - $this->log('Starting skills synchronization...', $input); - - if (! $this->filesystem->exists($packageSkillsPath)) { - return $this->failure( - 'No packaged skills found at: {packaged_skills_path}', - $input, - [ - 'packaged_skills_path' => $packageSkillsPath, - 'skills_dir' => $skillsDir, - 'directory_created' => false, - ], - ); - } - - $directoryCreated = false; - - if (! $this->filesystem->exists($skillsDir)) { - $this->filesystem->mkdir($skillsDir); - $directoryCreated = true; - $this->log('Created .agents/skills directory.', $input); - } - - $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY); - - if ($result->failed()) { - return $this->failure( - 'Skills synchronization failed.', - $input, - [ - 'packaged_skills_path' => $packageSkillsPath, - 'skills_dir' => $skillsDir, - 'directory_created' => $directoryCreated, - ], - ); - } - - return $this->success( - 'Skills synchronization completed successfully.', - $input, - [ - 'packaged_skills_path' => $packageSkillsPath, - 'skills_dir' => $skillsDir, - 'directory_created' => $directoryCreated, - ], - ); - } -} diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index ce4bb19205..f7e79dd900 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -63,7 +63,7 @@ protected function configure(): void { $this->setHelp( 'This command runs the dedicated synchronization commands for composer.json, resources, CODEOWNERS,' - . ' funding metadata, wiki, git metadata, packaged skills, packaged agents, license, and Git hooks.' + . ' funding metadata, wiki, git metadata, license, and Git hooks.' ); $this->addJsonOption() @@ -114,6 +114,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->queueDevToolsCommand(['update-composer-json', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); $this->queueDevToolsCommand(['funding', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); + $this->queueDevToolsCommand( + [ + 'copy-resource', + '--source=grumphp.yml', + '--target=grumphp.yml', + $overwrite ? '--overwrite' : null, + ...$modeArguments, + ], + $allowDetached, + $jsonOutput, + $prettyJsonOutput, + ); $this->queueDevToolsCommand( [ 'copy-resource', @@ -159,7 +171,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($dryRun || $check || $interactive) { $this->log( - 'Skipping wiki, skills, and agents during preview/check modes because they do not yet expose non-destructive verification.', + 'Skipping wiki during preview/check modes because it does not yet expose non-destructive verification.', $input, logLevel: LogLevel::WARNING, ); @@ -170,8 +182,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $jsonOutput, $prettyJsonOutput ); - $this->queueDevToolsCommand(['skills'], true, $jsonOutput, $prettyJsonOutput); - $this->queueDevToolsCommand(['agents'], true, $jsonOutput, $prettyJsonOutput); } $this->queueDevToolsCommand(['gitignore', ...$modeArguments], $allowDetached, $jsonOutput, $prettyJsonOutput); diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index f07cf79eae..0b092f611e 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -45,6 +45,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function is_numeric; use function Safe\json_decode; use function Safe\preg_match; diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index 06d79b520c..65ea93ff19 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -26,6 +26,7 @@ use FastForward\DevTools\GrumPhp\ManagedConfigPathSynchronizer; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -35,8 +36,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; + use function Safe\json_decode; use function Safe\json_encode; +use function Safe\preg_match; /** * Updates composer.json with the Fast Forward dev-tools integration metadata. @@ -60,6 +63,7 @@ final class UpdateComposerJsonCommand extends Command * @param ManagedConfigPathSynchronizer $managedConfigPathSynchronizer synchronizes managed GrumPHP metadata * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param SymfonyStyle $io the input/output service used to interact with the user + * @param VersionCheckerInterface $versionChecker */ public function __construct( private readonly ComposerJsonInterface $composer, @@ -68,6 +72,7 @@ public function __construct( private readonly ManagedConfigPathSynchronizer $managedConfigPathSynchronizer, private readonly FileDiffer $fileDiffer, private readonly SymfonyStyle $io, + private readonly VersionCheckerInterface $versionChecker, ) { parent::__construct(); } @@ -220,7 +225,7 @@ private function updatedComposerJsonContents(string $currentContents, string $fi $scripts = []; } - foreach ($this->getScripts() as $name => $command) { + foreach ($this->getScripts($composerJsonData['name'] ?? '') as $name => $command) { $scripts[$name] = $command; } @@ -253,12 +258,22 @@ private function updatedComposerJsonContents(string $currentContents, string $fi /** * Returns the Composer scripts managed by this command. * + * @param string $packageName + * * @return array the script name to command map */ - private function getScripts(): array + private function getScripts(string $packageName): array { + $version = $this->versionChecker->getCurrentVersion(); + + if (1 !== preg_match('/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/', $version)) { + $version = '2.0.0'; + } + return [ - 'dev-tools' => 'dev-tools', + 'dev-tools' => 'fast-forward/dev-tools' === $packageName + ? '@php bin/dev-tools' + : 'cpx --skip-local fast-forward/dev-tools:' . ltrim($version, 'v'), 'dev-tools:fix' => '@dev-tools --fix', ]; } diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 52dc558b69..67a3f049f0 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -38,6 +38,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Path; + use function Safe\getcwd; /** diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index ed5d3a1168..ecf0c41713 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -223,6 +223,10 @@ private function runAutoUpdateWhenRequested(InputInterface $input, OutputInterfa */ private function shouldRenderLogo(InputInterface $input): bool { + if ('runtime' === $input->getFirstArgument()) { + return false; + } + if ($this->runtimeEnvironment->isAgentPresent()) { return false; } diff --git a/src/Console/Logger/OutputFormatLogger.php b/src/Console/Logger/OutputFormatLogger.php index 454e6e1187..de824a0b6d 100644 --- a/src/Console/Logger/OutputFormatLogger.php +++ b/src/Console/Logger/OutputFormatLogger.php @@ -30,6 +30,7 @@ use Psr\Log\LogLevel; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutputInterface; + use function Safe\json_encode; /** diff --git a/src/Console/Logger/Processor/CommandOutputProcessor.php b/src/Console/Logger/Processor/CommandOutputProcessor.php index 808df0a4f7..4e8bd87bd0 100644 --- a/src/Console/Logger/Processor/CommandOutputProcessor.php +++ b/src/Console/Logger/Processor/CommandOutputProcessor.php @@ -23,6 +23,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; + use function Safe\json_decode; /** diff --git a/src/Console/Output/OutputCapabilityDetector.php b/src/Console/Output/OutputCapabilityDetector.php index 7c9e1a70c2..0622edd3e8 100644 --- a/src/Console/Output/OutputCapabilityDetector.php +++ b/src/Console/Output/OutputCapabilityDetector.php @@ -22,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Throwable; + use function Safe\stream_isatty; /** diff --git a/src/Container/ServiceProvider/DevToolsServiceProvider.php b/src/Container/ServiceProvider/DevToolsServiceProvider.php index 2a6191be81..64ca570baa 100644 --- a/src/Container/ServiceProvider/DevToolsServiceProvider.php +++ b/src/Container/ServiceProvider/DevToolsServiceProvider.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Container\ServiceProvider; -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; @@ -174,7 +172,6 @@ public function getFactories(): array InputInterface::class => get(ArgvInput::class), OutputInterface::class => get(ConsoleOutputInterface::class), CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - CommandProvider::class => get(DevToolsCommandProvider::class), ConsoleOutputInterface::class => create(ConsoleOutput::class) ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) ->method('setFormatter', get(LogLevelOutputFormatter::class)), diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index 83b3756c4e..8f22a2bb93 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -21,6 +21,7 @@ use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; use Symfony\Component\Filesystem\Path; + use function Safe\getcwd; /** diff --git a/src/Funding/FundingYamlCodec.php b/src/Funding/FundingYamlCodec.php index f7d5318fb3..02f6a1acf8 100644 --- a/src/Funding/FundingYamlCodec.php +++ b/src/Funding/FundingYamlCodec.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools\Funding; use Symfony\Component\Yaml\Yaml; + use function array_filter; use function array_values; use function trim; diff --git a/src/Git/GitClient.php b/src/Git/GitClient.php index c58b496d22..4cb498410f 100644 --- a/src/Git/GitClient.php +++ b/src/Git/GitClient.php @@ -24,6 +24,7 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use RuntimeException; use Symfony\Component\Filesystem\Path; + use function rtrim; use function str_starts_with; use function trim; diff --git a/src/GitAttributes/Writer.php b/src/GitAttributes/Writer.php index c1ddd00dda..bebc41d3a1 100644 --- a/src/GitAttributes/Writer.php +++ b/src/GitAttributes/Writer.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools\GitAttributes; use FastForward\DevTools\Filesystem\FilesystemInterface; + use function Safe\preg_split; /** diff --git a/src/GitHooks/HookContentRenderer.php b/src/GitHooks/HookContentRenderer.php index 3757b16e3e..2094f1ce87 100644 --- a/src/GitHooks/HookContentRenderer.php +++ b/src/GitHooks/HookContentRenderer.php @@ -19,15 +19,13 @@ namespace FastForward\DevTools\GitHooks; -use FastForward\DevTools\Path\DevToolsPathResolver; - /** - * Renders packaged Git hooks with runtime-specific DevTools hook configuration paths. + * Renders packaged Git hooks with project-owned configuration paths. */ final class HookContentRenderer { /** - * Placeholder replaced with the packaged GrumPHP config path rendered relative to the project when possible. + * Placeholder replaced with the durable project-local GrumPHP config path. */ public const string MANAGED_GRUMPHP_CONFIG_PLACEHOLDER = '__DEV_TOOLS_GRUMPHP_CONFIG__'; @@ -35,16 +33,11 @@ final class HookContentRenderer * Renders the hook contents for the active DevTools runtime. * * @param string $contents the packaged hook contents - * @param string $projectPath the consumer project root that will own the synchronized hook * * @return string the rendered hook contents */ - public function render(string $contents, string $projectPath = ''): string + public function render(string $contents): string { - return str_replace( - self::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, - escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', $projectPath)), - $contents, - ); + return str_replace(self::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, escapeshellarg('./grumphp.yml'), $contents); } } diff --git a/src/Path/WorkingProjectPathResolver.php b/src/Path/WorkingProjectPathResolver.php index fd42649cb4..d8eb220f5e 100644 --- a/src/Path/WorkingProjectPathResolver.php +++ b/src/Path/WorkingProjectPathResolver.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Environment\EnvironmentInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Path; + use function Safe\getcwd; /** diff --git a/src/PhpUnit/Coverage/CoverageSummaryLoader.php b/src/PhpUnit/Coverage/CoverageSummaryLoader.php index 8dfde907ab..0bb6bc44f7 100644 --- a/src/PhpUnit/Coverage/CoverageSummaryLoader.php +++ b/src/PhpUnit/Coverage/CoverageSummaryLoader.php @@ -21,6 +21,7 @@ use RuntimeException; use SebastianBergmann\CodeCoverage\CodeCoverage; + use function is_file; /** diff --git a/src/Process/XdebugDisablingProcessEnvironmentConfigurator.php b/src/Process/XdebugDisablingProcessEnvironmentConfigurator.php index a31fcc210b..f3a09ffa72 100644 --- a/src/Process/XdebugDisablingProcessEnvironmentConfigurator.php +++ b/src/Process/XdebugDisablingProcessEnvironmentConfigurator.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Php\ExtensionInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function Safe\preg_match; /** diff --git a/src/Project/ProjectCapabilitiesResolver.php b/src/Project/ProjectCapabilitiesResolver.php index 793c318a07..e06839c254 100644 --- a/src/Project/ProjectCapabilitiesResolver.php +++ b/src/Project/ProjectCapabilitiesResolver.php @@ -24,6 +24,7 @@ use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; + use function array_key_first; use function array_values; use function is_dir; diff --git a/src/Rector/RemoveEmptyDocBlockRector.php b/src/Rector/RemoveEmptyDocBlockRector.php index a124be3b42..0cf7dadc32 100644 --- a/src/Rector/RemoveEmptyDocBlockRector.php +++ b/src/Rector/RemoveEmptyDocBlockRector.php @@ -26,6 +26,7 @@ use PhpParser\Node\Stmt\Class_; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + use function Safe\preg_split; use function Safe\preg_replace; diff --git a/src/Resource/FileDiffer.php b/src/Resource/FileDiffer.php index 7cd29e9c2c..b5c5ecf14b 100644 --- a/src/Resource/FileDiffer.php +++ b/src/Resource/FileDiffer.php @@ -21,6 +21,7 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use Throwable; + use function explode; use function implode; use function str_contains; diff --git a/src/Resource/UnifiedDiffer.php b/src/Resource/UnifiedDiffer.php index f0a187c30f..0818566cd6 100644 --- a/src/Resource/UnifiedDiffer.php +++ b/src/Resource/UnifiedDiffer.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools\Resource; use SebastianBergmann\Diff\Differ; + use function trim; /** diff --git a/src/SelfUpdate/ComposerSelfUpdateRunner.php b/src/SelfUpdate/ComposerSelfUpdateRunner.php index cd1425be34..492ac00249 100644 --- a/src/SelfUpdate/ComposerSelfUpdateRunner.php +++ b/src/SelfUpdate/ComposerSelfUpdateRunner.php @@ -24,7 +24,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Updates DevTools through the active Composer executable. + * Updates DevTools through the package manager that owns its installation. */ final readonly class ComposerSelfUpdateRunner implements SelfUpdateRunnerInterface { @@ -49,8 +49,8 @@ public function __construct( */ public function update(bool $global, OutputInterface $output): int { - $command = $global ? 'composer global update' : 'composer update'; - $label = $global ? 'Updating global DevTools installation' : 'Updating project DevTools installation'; + $command = $global ? 'composer global update' : 'cpx update'; + $label = $global ? 'Updating global DevTools installation' : 'Updating CPX DevTools cache'; $this->processQueue->add( process: $this->processBuilder diff --git a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php index 52ad109f69..978659fb12 100644 --- a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php +++ b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use Symfony\Component\Filesystem\Path; use Throwable; + use function Safe\realpath; /** diff --git a/src/SelfUpdate/ComposerVersionChecker.php b/src/SelfUpdate/ComposerVersionChecker.php index 10ce6fc02b..e2178de8a5 100644 --- a/src/SelfUpdate/ComposerVersionChecker.php +++ b/src/SelfUpdate/ComposerVersionChecker.php @@ -24,6 +24,7 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use JsonException; use Throwable; + use function Safe\preg_match; use function Safe\json_decode; diff --git a/src/SelfUpdate/WorkingDirectorySwitcher.php b/src/SelfUpdate/WorkingDirectorySwitcher.php index c48fba7970..afd32b2942 100644 --- a/src/SelfUpdate/WorkingDirectorySwitcher.php +++ b/src/SelfUpdate/WorkingDirectorySwitcher.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools\SelfUpdate; use InvalidArgumentException; + use function Safe\chdir; use function Safe\realpath; diff --git a/src/Sync/PackagedDirectorySynchronizer.php b/src/Sync/PackagedDirectorySynchronizer.php deleted file mode 100644 index 733af80fc2..0000000000 --- a/src/Sync/PackagedDirectorySynchronizer.php +++ /dev/null @@ -1,240 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Sync; - -use FastForward\DevTools\Filesystem\FinderFactoryInterface; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerInterface; -use Symfony\Component\Filesystem\Path; - -/** - * Synchronizes one packaged directory of symlinked entries into a consumer repository. - */ -final readonly class PackagedDirectorySynchronizer -{ - /** - * Initializes the synchronizer with a filesystem and finder factory. - * - * @param FilesystemInterface $filesystem Filesystem instance for file operations - * @param FinderFactoryInterface $finderFactory Factory for locating packaged directories - * @param LoggerInterface $logger Logger for recording synchronization actions and decisions - */ - public function __construct( - private FilesystemInterface $filesystem, - private FinderFactoryInterface $finderFactory, - private LoggerInterface $logger, - ) {} - - /** - * Synchronizes packaged directory entries into the consumer repository. - * - * @param string $targetDir Absolute path to the consumer directory to populate - * @param string $packagePath Absolute path to the packaged directory to mirror - * @param string $directoryLabel Human-readable directory label used in log messages - * - * @return SynchronizeResult Result containing counts of created, preserved, and removed links - */ - public function synchronize(string $targetDir, string $packagePath, string $directoryLabel): SynchronizeResult - { - $result = new SynchronizeResult(); - - if (! $this->filesystem->exists($packagePath)) { - $this->logger->error('No packaged ' . $directoryLabel . ' found at: ' . $packagePath); - $result->markFailed(); - - return $result; - } - - if (! $this->filesystem->exists($targetDir)) { - $this->filesystem->mkdir($targetDir); - $this->logger->info('Created ' . $directoryLabel . ' directory.'); - } - - $finder = $this->finderFactory - ->create() - ->in($packagePath) - ->depth('== 0'); - - foreach ($finder as $packagedEntry) { - $entryName = $packagedEntry->getFilename(); - $targetLink = Path::makeAbsolute($entryName, $targetDir); - $sourcePath = $packagedEntry->getRealPath(); - $isDirectory = $packagedEntry->isDir(); - - $this->processLink($entryName, $targetLink, $sourcePath, $isDirectory, $result); - } - - return $result; - } - - /** - * Routes an entry link to the appropriate handling method based on target state. - * - * @param string $entryName Name of the packaged entry being processed - * @param string $targetLink Absolute path where the symlink should exist - * @param string $sourcePath Absolute path to the packaged source directory - * @param SynchronizeResult $result Result tracker for reporting outcomes - * @param bool $isDirectory - */ - private function processLink( - string $entryName, - string $targetLink, - string $sourcePath, - bool $isDirectory, - SynchronizeResult $result, - ): void { - if (! $this->filesystem->exists($targetLink)) { - $this->createNewLink($entryName, $targetLink, $sourcePath, $isDirectory, $result); - - return; - } - - if (! $this->isSymlink($targetLink)) { - $this->preserveExistingNonSymlink($entryName, $result); - - return; - } - - $this->processExistingSymlink($entryName, $targetLink, $sourcePath, $isDirectory, $result); - } - - /** - * Creates a new symlink pointing to the packaged entry. - * - * @param string $entryName Name identifying the entry - * @param string $targetLink Absolute path where the symlink will be created - * @param string $sourcePath Absolute path to the packaged directory - * @param SynchronizeResult $result Result object for tracking creation - * @param bool $isDirectory - */ - private function createNewLink( - string $entryName, - string $targetLink, - string $sourcePath, - bool $isDirectory, - SynchronizeResult $result, - ): void { - $relativeSourcePath = $this->normalizeRelativeSourcePath( - $this->filesystem->makePathRelative($sourcePath, $this->filesystem->getDirectory($targetLink)), - $isDirectory, - ); - - $this->filesystem->symlink($relativeSourcePath, $targetLink); - $this->logger->info('Created link: ' . $entryName . ' -> ' . $relativeSourcePath); - $result->addCreatedLink($entryName); - } - - /** - * Handles an existing non-symlink item at the target path. - * - * @param string $entryName Name of the entry with the conflicting item - * @param SynchronizeResult $result Result tracker for preserved items - */ - private function preserveExistingNonSymlink(string $entryName, SynchronizeResult $result): void - { - $this->logger->notice( - 'Existing non-symlink found: ' . $entryName . ' (keeping as is, skipping link creation)' - ); - $result->addPreservedLink($entryName); - } - - /** - * Evaluates an existing symlink and determines whether to preserve or repair it. - * - * @param string $entryName Name of the entry with the existing symlink - * @param string $targetLink Absolute path to the existing symlink - * @param string $sourcePath Absolute path to the expected source directory - * @param SynchronizeResult $result Result tracker for preserved or removed links - * @param bool $isDirectory - */ - private function processExistingSymlink( - string $entryName, - string $targetLink, - string $sourcePath, - bool $isDirectory, - SynchronizeResult $result, - ): void { - $linkPath = $this->filesystem->readlink($targetLink, true); - - if (! $linkPath || ! $this->filesystem->exists($linkPath)) { - $this->repairBrokenLink($entryName, $targetLink, $sourcePath, $isDirectory, $result); - - return; - } - - $this->logger->notice('Preserved existing link: ' . $entryName); - $result->addPreservedLink($entryName); - } - - /** - * Removes a broken symlink and creates a fresh one pointing to the current source. - * - * @param string $entryName Name of the entry with the broken symlink - * @param string $targetLink Absolute path to the broken symlink - * @param string $sourcePath Absolute path to the current packaged directory - * @param SynchronizeResult $result Result tracker for removed and created items - * @param bool $isDirectory - */ - private function repairBrokenLink( - string $entryName, - string $targetLink, - string $sourcePath, - bool $isDirectory, - SynchronizeResult $result, - ): void { - $this->filesystem->remove($targetLink); - $this->logger->notice('Existing link is broken: ' . $entryName . ' (removing and recreating)'); - $result->addRemovedBrokenLink($entryName); - - $this->createNewLink($entryName, $targetLink, $sourcePath, $isDirectory, $result); - } - - /** - * Normalizes a relative symlink target emitted by Symfony path helpers. - * - * Files MUST NOT keep the trailing slash that directory-oriented path helpers - * may append, otherwise link creation treats them as non-existent directories. - * - * @param string $relativeSourcePath Relative path from the consumer target directory to the packaged source - * @param bool $isDirectory Whether the packaged source is a directory - * - * @return string Normalized relative symlink target - */ - private function normalizeRelativeSourcePath(string $relativeSourcePath, bool $isDirectory): string - { - if ($isDirectory) { - return $relativeSourcePath; - } - - return rtrim($relativeSourcePath, '/'); - } - - /** - * Checks if a path is a symbolic link. - * - * @param string $path the target path - * - * @return bool whether the path is a symbolic link - */ - private function isSymlink(string $path): bool - { - return null !== $this->filesystem->readlink($path); - } -} diff --git a/src/Sync/SynchronizeResult.php b/src/Sync/SynchronizeResult.php deleted file mode 100644 index bfbd3a6ed9..0000000000 --- a/src/Sync/SynchronizeResult.php +++ /dev/null @@ -1,147 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Sync; - -/** - * Result object for packaged directory synchronization operations. - * - * This value object tracks the outcome of a synchronization run by recording - * which links were created, which existing links or directories were - * preserved, and which broken links were removed before recreation. It also - * carries a failure flag so callers can distinguish successful runs from - * aborted or invalid synchronization attempts. - */ -final class SynchronizeResult -{ - /** - * Stores the entry names for symlinks created during synchronization. - * - * @var list - */ - private array $createdLinks = []; - - /** - * Stores the entry names that were already valid and therefore preserved. - * - * @var list - */ - private array $preservedLinks = []; - - /** - * Stores the entry names for broken links removed during repair. - * - * @var list - */ - private array $removedBrokenLinks = []; - - /** - * Indicates whether the synchronization process encountered a fatal failure. - */ - private bool $failed = false; - - /** - * Records the name of a link that was newly created. - * - * @param string $link Entry name identifying the created link - * - * @return void - */ - public function addCreatedLink(string $link): void - { - $this->createdLinks[] = $link; - } - - /** - * Records the name of an entry that was preserved as-is. - * - * @param string $link Entry name identifying the preserved link or directory - * - * @return void - */ - public function addPreservedLink(string $link): void - { - $this->preservedLinks[] = $link; - } - - /** - * Records the name of a broken link that was removed before recreation. - * - * @param string $link Entry name identifying the repaired broken link - * - * @return void - */ - public function addRemovedBrokenLink(string $link): void - { - $this->removedBrokenLinks[] = $link; - } - - /** - * Marks the synchronization as failed. - * - * Callers SHOULD use this when a precondition or runtime error prevents - * the synchronization result from being considered successful. - * - * @return void - */ - public function markFailed(): void - { - $this->failed = true; - } - - /** - * Returns the names of links created during synchronization. - * - * @return list - */ - public function getCreatedLinks(): array - { - return $this->createdLinks; - } - - /** - * Returns the names of links or directories preserved during synchronization. - * - * @return list - */ - public function getPreservedLinks(): array - { - return $this->preservedLinks; - } - - /** - * Returns the names of broken links removed during synchronization repair. - * - * @return list - */ - public function getRemovedBrokenLinks(): array - { - return $this->removedBrokenLinks; - } - - /** - * Indicates whether the synchronization result represents a failed run. - * - * @return bool - */ - public function failed(): bool - { - return $this->failed; - } -} diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php deleted file mode 100644 index 781484acb5..0000000000 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ /dev/null @@ -1,240 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Composer\Capability; - -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\Composer\Command\ProxyCommand; -use FastForward\DevTools\Composer\DevToolsPluginInterface; -use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; -use FastForward\DevTools\Console\Output\GithubActionOutput; -use FastForward\DevTools\Container\ContainerFactory; -use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; -use FastForward\DevTools\Path\DevToolsPathResolver; -use Symfony\Component\Console\Application; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -#[UsesClass(GithubActionOutput::class)] -#[CoversClass(DevToolsCommandProvider::class)] -#[UsesClass(ContainerFactory::class)] -#[UsesClass(DevToolsPathResolver::class)] -#[UsesClass(DevToolsServiceProvider::class)] -#[UsesClass(ProxyCommand::class)] -final class DevToolsCommandProviderTest extends TestCase -{ - use ProphecyTrait; - - private ObjectProphecy $plugin; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $devTools; - - /** - * @var array - */ - private array $applicationCommands = []; - - private DevToolsCommandProvider $commandProvider; - - /** - * @return void - */ - protected function setUp(): void - { - ContainerFactory::reset(); - $this->plugin = $this->prophesize(DevToolsPluginInterface::class); - $this->devTools = $this->prophesize(Application::class); - - $this->plugin->isRegisteredCommand(null) - ->willReturn(false); - $this->plugin->isRegisteredCommand('agents') - ->willReturn(false); - $this->plugin->isRegisteredCommand('reports:tests') - ->willReturn(false); - $this->plugin->isRegisteredCommand('tests') - ->willReturn(false); - $this->plugin->isRegisteredCommand('phpunit') - ->willReturn(false); - $this->plugin->isRegisteredCommand('dev-tools:standards') - ->willReturn(false); - $this->plugin->isRegisteredCommand('standards') - ->willReturn(false); - $this->plugin->isRegisteredCommand('dev-tools:self-update') - ->willReturn(false); - $this->plugin->isRegisteredCommand('self-update') - ->willReturn(true); - $this->plugin->isRegisteredCommand('install') - ->willReturn(true); - - $this->commandProvider = new DevToolsCommandProvider([ - 'plugin' => $this->plugin->reveal(), - ]); - - $testCase = $this; - $this->devTools->all() - ->will(static fn(): array => $testCase->applicationCommands); - ContainerFactory::set(DevTools::class, $this->devTools->reveal()); - } - - /** - * @return void - */ - protected function tearDown(): void - { - ContainerFactory::reset(); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): void - { - $commands = $this->commandProvider->getCommands(); - - self::assertIsArray($commands); - self::assertEmpty($commands); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCommands(): void - { - $symfonyCommand = new FixtureWithoutAsCommand('agents'); - $symfonyCommand->setAliases([]); - $symfonyCommand->setDescription('Synchronize agents.'); - $symfonyCommand->setHelp(''); - $symfonyCommand->setHidden(false); - - $this->applicationCommands = [ - 'agents' => $symfonyCommand, - ]; - - $commands = array_values($this->commandProvider->getCommands()); - $command = $commands[0]; - - self::assertIsArray($commands); - self::assertCount(1, $commands); - self::assertInstanceOf(ProxyCommand::class, $command); - self::assertSame('agents', $command->getName()); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): void - { - $symfonyCommand = new FixtureWithoutAsCommand('reports:tests'); - $symfonyCommand->setAliases(['tests', 'phpunit']); - $symfonyCommand->setDescription('Runs PHPUnit tests.'); - $symfonyCommand->setHelp(''); - $symfonyCommand->setHidden(false); - - $this->applicationCommands = [ - 'reports:tests' => $symfonyCommand, - 'tests' => $symfonyCommand, - ]; - - $commands = array_values($this->commandProvider->getCommands()); - $proxyCommand = $commands[0]; - - self::assertCount(1, $commands); - self::assertInstanceOf(ProxyCommand::class, $proxyCommand); - self::assertSame('reports:tests', $proxyCommand->getName()); - self::assertSame(['tests', 'phpunit'], $proxyCommand->getAliases()); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillPreserveSafeAliasesThroughComposerPlugin(): void - { - $symfonyCommand = new FixtureWithoutAsCommand('dev-tools:standards'); - $symfonyCommand->setAliases(['standards']); - $symfonyCommand->setDescription('Runs standards checks.'); - $symfonyCommand->setHelp(''); - $symfonyCommand->setHidden(false); - - $this->applicationCommands = [ - 'dev-tools:standards' => $symfonyCommand, - 'standards' => $symfonyCommand, - ]; - - $proxyCommand = array_values($this->commandProvider->getCommands())[0]; - - self::assertInstanceOf(ProxyCommand::class, $proxyCommand); - self::assertSame('dev-tools:standards', $proxyCommand->getName()); - self::assertSame(['standards'], $proxyCommand->getAliases()); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillNotExposeSelfUpdateAliasToComposer(): void - { - $symfonyCommand = new FixtureWithoutAsCommand('dev-tools:self-update'); - $symfonyCommand->setAliases(['self-update']); - $symfonyCommand->setDescription('Updates DevTools.'); - $symfonyCommand->setHelp(''); - $symfonyCommand->setHidden(false); - - $this->applicationCommands = [ - 'dev-tools:self-update' => $symfonyCommand, - 'self-update' => $symfonyCommand, - ]; - - $proxyCommand = array_values($this->commandProvider->getCommands())[0]; - - self::assertInstanceOf(ProxyCommand::class, $proxyCommand); - self::assertSame('dev-tools:self-update', $proxyCommand->getName()); - self::assertSame([], $proxyCommand->getAliases()); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillNotExposeCommandsOwnedByComposer(): void - { - $symfonyCommand = new FixtureWithoutAsCommand('install'); - $symfonyCommand->setAliases([]); - $symfonyCommand->setDescription('Conflicting command.'); - $symfonyCommand->setHelp(''); - $symfonyCommand->setHidden(false); - - $this->applicationCommands = [ - 'install' => $symfonyCommand, - ]; - - self::assertSame([], $this->commandProvider->getCommands()); - } -} diff --git a/tests/Composer/Json/ComposerJsonTest.php b/tests/Composer/Json/ComposerJsonTest.php index b8c4c6ad58..af93bea62c 100644 --- a/tests/Composer/Json/ComposerJsonTest.php +++ b/tests/Composer/Json/ComposerJsonTest.php @@ -32,6 +32,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use UnderflowException; + use function Safe\file_put_contents; use function Safe\json_encode; use function Safe\tempnam; @@ -80,6 +81,22 @@ public function identificationAccessorsWillReturnConfiguredData(): void self::assertSame('composer-plugin', $composerJson->getType()); } + /** + * @return void + */ + #[Test] + public function identificationAccessorsWillDefaultToEmptyStringsForMinimalRootProjects(): void + { + $composerJson = $this->createComposerJson([ + 'require' => [ + 'php' => '^8.3', + ], + ]); + + self::assertSame('', $composerJson->getName()); + self::assertSame('', $composerJson->getDescription()); + } + /** * @return void */ diff --git a/tests/Composer/PluginTest.php b/tests/Composer/PluginTest.php deleted file mode 100644 index 29a075cefc..0000000000 --- a/tests/Composer/PluginTest.php +++ /dev/null @@ -1,183 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Composer; - -use Composer\Composer; -use Composer\IO\IOInterface; -use Composer\Package\RootPackageInterface; -use Composer\Plugin\Capability\CommandProvider; -use Composer\Script\Event as ScriptEvent; -use Composer\Util\Loop; -use Composer\Util\ProcessExecutor; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\Composer\Plugin; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -#[CoversClass(Plugin::class)] -final class PluginTest extends TestCase -{ - use ProphecyTrait; - - private Plugin $plugin; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $composer; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $io; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $rootPackage; - - /** - * @return void - */ - protected function setUp(): void - { - $this->plugin = new Plugin(); - $this->composer = $this->prophesize(Composer::class); - $this->io = $this->prophesize(IOInterface::class); - $this->rootPackage = $this->prophesize(RootPackageInterface::class); - $this->composer->getPackage() - ->willReturn($this->rootPackage->reveal()); - $this->rootPackage->getScripts() - ->willReturn([]); - } - - /** - * @return void - */ - #[Test] - public function getCapabilitiesWillReturnDevToolsCommandProviderMapping(): void - { - self::assertSame( - [ - CommandProvider::class => DevToolsCommandProvider::class, - ], - $this->plugin->getCapabilities(), - ); - } - - /** - * @return void - */ - #[Test] - public function getSubscribedEventsWillReturnExpectedEventMapping(): void - { - self::assertSame( - [ - 'post-install-cmd' => 'runSyncCommand', - 'post-update-cmd' => 'runSyncCommand', - ], - Plugin::getSubscribedEvents(), - ); - } - - /** - * @return void - */ - #[Test] - public function activateWillDoNothing(): void - { - self::assertNull($this->plugin->activate($this->composer->reveal(), $this->io->reveal())); - } - - /** - * @return void - */ - #[Test] - public function isRegisteredCommandWillDetectReservedCommandNames(): void - { - $this->rootPackage->getScripts() - ->willReturn([ - 'custom-script' => [], - 'post-install-cmd' => [], - ]); - $this->plugin->activate($this->composer->reveal(), $this->io->reveal()); - - self::assertTrue($this->plugin->isRegisteredCommand('install')); - self::assertTrue($this->plugin->isRegisteredCommand('i')); - self::assertTrue($this->plugin->isRegisteredCommand('self-update')); - self::assertTrue($this->plugin->isRegisteredCommand('selfupdate')); - self::assertTrue($this->plugin->isRegisteredCommand('custom-script')); - self::assertFalse($this->plugin->isRegisteredCommand('post-install-cmd')); - self::assertFalse($this->plugin->isRegisteredCommand('code-style')); - self::assertFalse($this->plugin->isRegisteredCommand(null)); - } - - /** - * @return void - */ - #[Test] - public function runSyncCommandWillExecuteDevToolsSync(): void - { - $event = $this->prophesize(ScriptEvent::class); - - // Mock ProcessExecutor - $processExecutor = $this->prophesize(ProcessExecutor::class); - $processExecutor->execute('vendor/bin/dev-tools dev-tools:sync') - ->shouldBeCalled(); - - // Mock Loop - $loop = $this->prophesize(Loop::class); - $loop->getProcessExecutor() - ->willReturn($processExecutor->reveal()); - - // Mock Composer - $composer = $this->prophesize(Composer::class); - $composer->getLoop() - ->willReturn($loop->reveal()); - - $event->getComposer() - ->willReturn($composer->reveal()); - - $this->plugin->runSyncCommand($event->reveal()); - - self::assertTrue(true); // Evita teste risky - } - - /** - * @return void - */ - #[Test] - public function deactivateWillDoNothing(): void - { - self::assertNull($this->plugin->deactivate($this->composer->reveal(), $this->io->reveal())); - } - - /** - * @return void - */ - #[Test] - public function uninstallWillDoNothing(): void - { - self::assertNull($this->plugin->uninstall($this->composer->reveal(), $this->io->reveal())); - } -} diff --git a/tests/Config/ECSConfigTest.php b/tests/Config/ECSConfigTest.php index c81b244f56..7cf40d9ecc 100644 --- a/tests/Config/ECSConfigTest.php +++ b/tests/Config/ECSConfigTest.php @@ -41,6 +41,7 @@ use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; use Symplify\EasyCodingStandard\Config\ECSConfig as ECSConfigInterface; use Symplify\CodingStandard\Fixer\ArrayNotation\ArrayListItemNewlineFixer; + use function Safe\getcwd; #[CoversClass(ECSConfig::class)] diff --git a/tests/Config/RectorConfigTest.php b/tests/Config/RectorConfigTest.php index 3b900f7b1c..529d5167ee 100644 --- a/tests/Config/RectorConfigTest.php +++ b/tests/Config/RectorConfigTest.php @@ -39,6 +39,7 @@ use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Config\RectorConfig as RectorConfigInterface; use Rector\Php54\Rector\Array_\LongArrayToShortArrayRector; + use function Safe\getcwd; #[CoversClass(RectorConfig::class)] diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php deleted file mode 100644 index d276f158e4..0000000000 --- a/tests/Console/Command/AgentsCommandTest.php +++ /dev/null @@ -1,205 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Console\Command; - -use FastForward\DevTools\Console\Command\AgentsCommand; -use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use FastForward\DevTools\Sync\SynchronizeResult; -use FastForward\DevTools\Container\ContainerFactory; -use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; -use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; -use FastForward\DevTools\Environment\RuntimeEnvironment; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\Attributes\UsesTrait; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Log\LoggerInterface; -use FastForward\DevTools\Tests\Container\UsesContainerFactory; -use ReflectionMethod; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use function Safe\getcwd; - -#[UsesClass(ContainerFactory::class)] -#[UsesClass(DevToolsServiceProvider::class)] -#[UsesClass(DevToolsEnvironment::class)] -#[UsesClass(RuntimeEnvironment::class)] -#[CoversClass(AgentsCommand::class)] -#[UsesClass(DevToolsPathResolver::class)] -#[UsesClass(PackagedDirectorySynchronizer::class)] -#[UsesClass(SynchronizeResult::class)] -#[UsesTrait(LogsCommandResults::class)] -final class AgentsCommandTest extends TestCase -{ - use ProphecyTrait; - use UsesContainerFactory; - - private ObjectProphecy $synchronizer; - - private ObjectProphecy $filesystem; - - private ObjectProphecy $logger; - - private ObjectProphecy $input; - - private ObjectProphecy $output; - - private AgentsCommand $command; - - /** - * @return void - */ - protected function setUp(): void - { - $this->synchronizer = $this->prophesize(PackagedDirectorySynchronizer::class); - $this->filesystem = $this->prophesize(FilesystemInterface::class); - $this->logger = $this->prophesize(LoggerInterface::class); - $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); - $this->input = $this->prophesize(InputInterface::class); - - $this->input->getOption('json') - ->willReturn(false); - $this->input->getOption('pretty-json') - ->willReturn(false); - $this->output = $this->prophesize(OutputInterface::class); - $this->filesystem->getAbsolutePath('.agents/agents') - ->willReturn(getcwd() . '/.agents/agents'); - - $this->command = new AgentsCommand($this->synchronizer->reveal(), $this->filesystem->reveal()); - } - - /** - * @return void - */ - #[Test] - public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void - { - $agentsPath = getcwd() . '/.agents/agents'; - - $this->filesystem->exists($agentsPath) - ->willReturn(false); - $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->log('info', 'Starting agents synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->error( - 'No packaged .agents/agents found at: {packaged_agents_path}', - [ - 'input' => $this->input->reveal(), - 'file' => null, - 'line' => null, - 'packaged_agents_path' => $agentsPath, - 'agents_dir' => $agentsPath, - 'directory_created' => false, - ], - )->shouldBeCalledOnce(); - - self::assertSame(AgentsCommand::FAILURE, $this->invokeExecute()); - } - - /** - * @return void - */ - #[Test] - public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void - { - $agentsPath = getcwd() . '/.agents/agents'; - $result = new SynchronizeResult(); - - $this->filesystem->exists($agentsPath) - ->willReturn(true, false); - $this->filesystem->mkdir($agentsPath) - ->shouldBeCalledOnce(); - $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') - ->willReturn($result) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Starting agents synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Created .agents/agents directory.', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->log( - 'info', - 'Agents synchronization completed successfully.', - [ - 'input' => $this->input->reveal(), - 'packaged_agents_path' => $agentsPath, - 'agents_dir' => $agentsPath, - 'directory_created' => true, - ], - )->shouldBeCalledOnce(); - - self::assertSame(AgentsCommand::SUCCESS, $this->invokeExecute()); - } - - /** - * @return void - */ - #[Test] - public function executeWillReturnFailureWhenSynchronizerFails(): void - { - $agentsPath = getcwd() . '/.agents/agents'; - $result = new SynchronizeResult(); - $result->markFailed(); - - $this->filesystem->exists($agentsPath) - ->willReturn(true, true); - $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') - ->willReturn($result) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Starting agents synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->error( - 'Agents synchronization failed.', - [ - 'input' => $this->input->reveal(), - 'file' => null, - 'line' => null, - 'packaged_agents_path' => $agentsPath, - 'agents_dir' => $agentsPath, - 'directory_created' => false, - ], - )->shouldBeCalledOnce(); - - self::assertSame(AgentsCommand::FAILURE, $this->invokeExecute()); - } - - /** - * @return int - */ - private function invokeExecute(): int - { - return (new ReflectionMethod($this->command, 'execute')) - ->invoke($this->command, $this->input->reveal(), $this->output->reveal()); - } -} diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 5be14906c3..fe88a12dc7 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -111,7 +111,7 @@ protected function setUp(): void ); $this->processBuilder->build(Argument::any())->willReturn($this->process->reveal()); $this->processQueue->add($this->process->reveal(), Argument::cetera()) - ->shouldBeCalledTimes(3); + ->shouldBeCalledTimes(2); $this->command = new CodeStyleCommand( $this->fileLocator->reveal(), diff --git a/tests/Console/Command/CommandAttributeCompatibilityTest.php b/tests/Console/Command/CommandAttributeCompatibilityTest.php index 7572f3395e..f9c71e7409 100644 --- a/tests/Console/Command/CommandAttributeCompatibilityTest.php +++ b/tests/Console/Command/CommandAttributeCompatibilityTest.php @@ -22,6 +22,7 @@ use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; + use function Safe\glob; use function Safe\preg_match_all; use function Safe\file_get_contents; diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index e90272e6d1..70bd948451 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -46,6 +46,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; + use function Safe\mkdir; use function Safe\file_put_contents; use function Safe\unlink; diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index 56fc1f569e..0a54c17ab6 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -38,6 +38,7 @@ use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; use FastForward\DevTools\Environment\RuntimeEnvironment; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -53,10 +54,12 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; use Symfony\Component\Yaml\Yaml; + use function Safe\json_decode; #[UsesClass(ContainerFactory::class)] #[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(WorkingProjectPathResolver::class)] #[UsesClass(DevToolsServiceProvider::class)] #[UsesClass(DevToolsEnvironment::class)] #[UsesClass(RuntimeEnvironment::class)] @@ -141,7 +144,9 @@ protected function setUp(): void $this->processBuilder->withArgument(Argument::any(), Argument::any())->willReturn( $this->processBuilder->reveal() ); - $this->processBuilder->build('composer normalize') + $this->processBuilder->build(Argument::that(static fn(array $command): bool => 'composer' === $command[0] + && '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2) === $command[1] + && 'normalize' === $command[2])) ->willReturn($this->normalizeProcess->reveal()); $this->command = new FundingCommand( @@ -596,11 +601,12 @@ public function executeWillPassWorkingDirectoryAndAlternateManifestToComposerNor ->willReturn('build/custom'); $this->filesystem->getBasename($composerFile) ->willReturn('composer.alt.json'); - $this->processBuilder->withArgument('--working-dir', 'build/custom') - ->willReturn($this->processBuilder->reveal()) - ->shouldBeCalledOnce(); - $this->processBuilder->withArgument('--file', 'composer.alt.json') - ->willReturn($this->processBuilder->reveal()) + $this->processBuilder->build([ + 'composer', + '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2), + 'normalize', + WorkingProjectPathResolver::getProjectPath($composerFile), + ])->willReturn($this->normalizeProcess->reveal()) ->shouldBeCalledOnce(); $this->fileDiffer->diffContents( 'generated funding metadata synchronization', @@ -700,8 +706,12 @@ public function privateHelpersWillPromptAndNormalizeComposerFileArguments(): voi ->willReturn('.'); $this->filesystem->getBasename('composer.alt.json') ->willReturn('composer.alt.json'); - $this->processBuilder->withArgument('--file', 'composer.alt.json') - ->willReturn($this->processBuilder->reveal()) + $this->processBuilder->build([ + 'composer', + '--working-dir=' . \dirname(DevToolsPathResolver::getRuntimeAutoloadPath(), 2), + 'normalize', + WorkingProjectPathResolver::getProjectPath('composer.alt.json'), + ])->willReturn($this->normalizeProcess->reveal()) ->shouldBeCalledOnce(); $this->processQueue->add($this->normalizeProcess->reveal(), Argument::cetera()) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index bb0b4f5ab4..94230998a9 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -51,6 +51,7 @@ use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + use function Safe\getcwd; #[UsesClass(ContainerFactory::class)] diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 93d69b5be5..2c58f55907 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -51,6 +51,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Finder\Finder; + use function is_file; use function Safe\glob; use function Safe\mkdir; @@ -247,10 +248,7 @@ public function executeWillRenderManagedGrumPhpConfigIntoPlaceholderHooks(): voi $this->filesystem->dumpFile( '/app/.githooks/pre-commit', Argument::that( - static fn(string $contents): bool => str_contains( - $contents, - escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/app')) - ) + static fn(string $contents): bool => str_contains($contents, escapeshellarg('./grumphp.yml')) ), )->shouldBeCalledOnce(); $this->filesystem->chmod(Argument::type('string'), 0o755) diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index e7f2d4d71d..e2ec9d20c3 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -48,6 +48,7 @@ use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + use function Safe\getcwd; #[UsesClass(ContainerFactory::class)] diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index 3d935ae016..d6952a3bb9 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -46,6 +46,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function Safe\putenv; #[UsesClass(ContainerFactory::class)] diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index 92c7be9b6a..518b0b1d08 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -57,6 +57,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use UnderflowException; #[UsesClass(ContainerFactory::class)] #[UsesClass(DevToolsServiceProvider::class)] @@ -229,6 +231,32 @@ public function executeWillCreateDocHeaderAndRunPhpDocProcesses(): void self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWithoutAuthorsWillOmitAuthorHeaderAndRunPhpDocProcesses(): void + { + $this->composer->getAuthors(true) + ->willThrow(new UnderflowException('No author was defined.')); + $renderer = new Environment(new FilesystemLoader(DevToolsPathResolver::getResourcesPath())); + $this->renderer->render('docblock/.docheader', Argument::that( + static fn(array $context): bool => '' === $context['copyright_holder'], + ))->will(static fn(array $arguments): string => $renderer->render(...$arguments)) + ->shouldBeCalledOnce(); + $this->filesystem->dumpFile(PhpDocCommand::FILENAME, Argument::that( + static fn(string $header): bool => ! str_contains($header, '@author') + && str_contains($header, 'fast-forward/dev-tools') + && str_contains($header, '@license'), + ))->shouldBeCalledOnce(); + $this->processQueue->run($this->output->reveal()) + ->willReturn(PhpDocCommand::SUCCESS) + ->shouldBeCalledOnce(); + $this->logger->log(Argument::cetera())->shouldBeCalled(); + + self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute()); + } + /** * @return void */ diff --git a/tests/Console/Command/RuntimeCommandTest.php b/tests/Console/Command/RuntimeCommandTest.php new file mode 100644 index 0000000000..efe4d6c415 --- /dev/null +++ b/tests/Console/Command/RuntimeCommandTest.php @@ -0,0 +1,128 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Console\Command; + +use FastForward\DevTools\Console\Command\RuntimeCommand; +use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\Attributes\UsesTrait; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +use function Safe\getcwd; +use function Safe\json_decode; +use function Safe\mkdir; +use function Safe\realpath; + +#[CoversClass(RuntimeCommand::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(WorkingProjectPathResolver::class)] +#[UsesTrait(HasJsonOption::class)] +final class RuntimeCommandTest extends TestCase +{ + use ProphecyTrait; + + /** + * @return void + */ + #[Test] + public function runtimeWillEmitFlatJsonWithPackagePathsAndVersion(): void + { + $versionChecker = $this->prophesize(VersionCheckerInterface::class); + $versionChecker->getCurrentVersion() + ->willReturn('2.0.0') + ->shouldBeCalledOnce(); + $tester = new CommandTester(new RuntimeCommand($versionChecker->reveal())); + $workingDirectory = getcwd(); + + self::assertSame(RuntimeCommand::SUCCESS, $tester->execute([ + '--json' => true, + ])); + $metadata = json_decode($tester->getDisplay(), true); + $packageRoot = \dirname(__DIR__, 3); + + self::assertSame([ + 'binary' => $packageRoot . '/bin/dev-tools', + 'autoload' => $packageRoot . '/vendor/autoload.php', + 'package_root' => $packageRoot, + 'project_root' => $workingDirectory, + 'version' => '2.0.0', + ], $metadata); + self::assertSame($workingDirectory, getcwd()); + } + + /** + * @return void + */ + #[Test] + public function runtimeWillEmitJsonByDefault(): void + { + $versionChecker = $this->prophesize(VersionCheckerInterface::class); + $versionChecker->getCurrentVersion() + ->willReturn('dev-main'); + $tester = new CommandTester(new RuntimeCommand($versionChecker->reveal())); + + self::assertSame(RuntimeCommand::SUCCESS, $tester->execute([])); + self::assertSame('dev-main', json_decode($tester->getDisplay(), true)['version']); + } + + /** + * @return void + */ + #[Test] + public function globalWorkingDirectoryWillSelectConsumerWithoutChangingRuntimePaths(): void + { + $directory = sys_get_temp_dir() . '/runtime-consumer-' . bin2hex(random_bytes(4)); + mkdir($directory); + $directory = realpath($directory); + + try { + $packageRoot = \dirname(__DIR__, 3); + $process = new Process([ + \PHP_BINARY, + $packageRoot . '/bin/dev-tools', + '--working-dir=' . $directory, + 'runtime', + '--json', + '--no-interaction', + '--no-ansi', + ], $packageRoot); + $process->mustRun(); + $metadata = json_decode($process->getOutput(), true); + + self::assertSame($directory, $metadata['project_root']); + self::assertSame($packageRoot, $metadata['package_root']); + self::assertSame($packageRoot . '/bin/dev-tools', $metadata['binary']); + self::assertSame($packageRoot . '/vendor/autoload.php', $metadata['autoload']); + self::assertIsString($metadata['version']); + } finally { + new Filesystem() + ->remove($directory); + } + } +} diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php deleted file mode 100644 index d5ee3d0465..0000000000 --- a/tests/Console/Command/SkillsCommandTest.php +++ /dev/null @@ -1,205 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Console\Command; - -use FastForward\DevTools\Console\Command\SkillsCommand; -use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use FastForward\DevTools\Sync\SynchronizeResult; -use FastForward\DevTools\Container\ContainerFactory; -use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; -use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; -use FastForward\DevTools\Environment\RuntimeEnvironment; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\Attributes\UsesTrait; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Log\LoggerInterface; -use FastForward\DevTools\Tests\Container\UsesContainerFactory; -use ReflectionMethod; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use function Safe\getcwd; - -#[UsesClass(ContainerFactory::class)] -#[UsesClass(DevToolsServiceProvider::class)] -#[UsesClass(DevToolsEnvironment::class)] -#[UsesClass(RuntimeEnvironment::class)] -#[CoversClass(SkillsCommand::class)] -#[UsesClass(DevToolsPathResolver::class)] -#[UsesClass(PackagedDirectorySynchronizer::class)] -#[UsesClass(SynchronizeResult::class)] -#[UsesTrait(LogsCommandResults::class)] -final class SkillsCommandTest extends TestCase -{ - use ProphecyTrait; - use UsesContainerFactory; - - private ObjectProphecy $synchronizer; - - private ObjectProphecy $filesystem; - - private ObjectProphecy $logger; - - private ObjectProphecy $input; - - private ObjectProphecy $output; - - private SkillsCommand $command; - - /** - * @return void - */ - protected function setUp(): void - { - $this->synchronizer = $this->prophesize(PackagedDirectorySynchronizer::class); - $this->filesystem = $this->prophesize(FilesystemInterface::class); - $this->logger = $this->prophesize(LoggerInterface::class); - $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); - $this->input = $this->prophesize(InputInterface::class); - - $this->input->getOption('json') - ->willReturn(false); - $this->input->getOption('pretty-json') - ->willReturn(false); - $this->output = $this->prophesize(OutputInterface::class); - $this->filesystem->getAbsolutePath('.agents/skills') - ->willReturn(getcwd() . '/.agents/skills'); - - $this->command = new SkillsCommand($this->synchronizer->reveal(), $this->filesystem->reveal()); - } - - /** - * @return void - */ - #[Test] - public function executeWillFailWhenPackagedSkillsDirectoryDoesNotExist(): void - { - $skillsPath = getcwd() . '/.agents/skills'; - - $this->filesystem->exists($skillsPath) - ->willReturn(false); - $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->log('info', 'Starting skills synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->error( - 'No packaged skills found at: {packaged_skills_path}', - [ - 'input' => $this->input->reveal(), - 'file' => null, - 'line' => null, - 'packaged_skills_path' => $skillsPath, - 'skills_dir' => $skillsPath, - 'directory_created' => false, - ], - )->shouldBeCalledOnce(); - - self::assertSame(SkillsCommand::FAILURE, $this->invokeExecute()); - } - - /** - * @return void - */ - #[Test] - public function executeWillCreateSkillsDirectoryWhenItDoesNotExist(): void - { - $skillsPath = getcwd() . '/.agents/skills'; - $result = new SynchronizeResult(); - - $this->filesystem->exists($skillsPath) - ->willReturn(true, false); - $this->filesystem->mkdir($skillsPath) - ->shouldBeCalledOnce(); - $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') - ->willReturn($result) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Starting skills synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Created .agents/skills directory.', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->log( - 'info', - 'Skills synchronization completed successfully.', - [ - 'input' => $this->input->reveal(), - 'packaged_skills_path' => $skillsPath, - 'skills_dir' => $skillsPath, - 'directory_created' => true, - ], - )->shouldBeCalledOnce(); - - self::assertSame(SkillsCommand::SUCCESS, $this->invokeExecute()); - } - - /** - * @return void - */ - #[Test] - public function executeWillReturnFailureWhenSynchronizerFails(): void - { - $skillsPath = getcwd() . '/.agents/skills'; - $result = new SynchronizeResult(); - $result->markFailed(); - - $this->filesystem->exists($skillsPath) - ->willReturn(true, true); - $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') - ->willReturn($result) - ->shouldBeCalledOnce(); - $this->logger->log('info', 'Starting skills synchronization...', [ - 'input' => $this->input->reveal(), - ]) - ->shouldBeCalledOnce(); - $this->logger->error( - 'Skills synchronization failed.', - [ - 'input' => $this->input->reveal(), - 'file' => null, - 'line' => null, - 'packaged_skills_path' => $skillsPath, - 'skills_dir' => $skillsPath, - 'directory_created' => false, - ], - )->shouldBeCalledOnce(); - - self::assertSame(SkillsCommand::FAILURE, $this->invokeExecute()); - } - - /** - * @return int - */ - private function invokeExecute(): int - { - return (new ReflectionMethod($this->command, 'execute')) - ->invoke($this->command, $this->input->reveal(), $this->output->reveal()); - } -} diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index 46d69f9e51..f57934aeea 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -101,7 +101,7 @@ public function executeWillQueueDedicatedSynchronizationCommands(): void ->will(static function (array $arguments) use (&$detachedCommandLines): void { $detachedCommandLines[] = $arguments[0]->getCommandLine(); }) - ->shouldBeCalledTimes(11); + ->shouldBeCalledTimes(10); $this->processQueue->run($this->output->reveal()) ->willReturn(SyncCommand::SUCCESS) ->shouldBeCalledOnce(); @@ -136,7 +136,7 @@ public function executeWillDisableDetachedModeWhenCheckingDrift(): void $this->input->getOption('check') ->willReturn(true); $this->processQueue->add(Argument::type(Process::class), false, false, Argument::cetera())->shouldBeCalledTimes( - 10 + 11 ); $this->processQueue->add(Argument::type(Process::class), false, true, Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run($this->output->reveal()) @@ -148,7 +148,7 @@ public function executeWillDisableDetachedModeWhenCheckingDrift(): void ->shouldBeCalled(); $this->logger->log( 'warning', - 'Skipping wiki, skills, and agents during preview/check modes because they do not yet expose non-destructive verification.', + 'Skipping wiki during preview/check modes because it does not yet expose non-destructive verification.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface), )->shouldBeCalled(); $this->logger->error( @@ -179,7 +179,7 @@ public function executeWillPropagateJsonOutputFormatToSubCommands(): void false, true, Argument::cetera() - )->shouldBeCalledTimes(11); + )->shouldBeCalledTimes(10); $this->processQueue->run(Argument::type('object')) ->willReturn(SyncCommand::SUCCESS) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index e397be7c9a..87949c42f2 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -55,6 +55,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function Safe\file_put_contents; use function Safe\unlink; use function Safe\mkdir; diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index be2d39a854..8cd708a81c 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -21,6 +21,7 @@ use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\UpdateComposerJsonCommand; +use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\GrumPhp\ManagedConfigPathSynchronizer; use FastForward\DevTools\Path\DevToolsPathResolver; @@ -45,6 +46,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; + use function Safe\json_decode; #[UsesClass(ContainerFactory::class)] @@ -78,6 +80,8 @@ final class UpdateComposerJsonCommandTest extends TestCase private UpdateComposerJsonCommand $command; + private ObjectProphecy $versionChecker; + /** * @return void */ @@ -116,6 +120,10 @@ protected function setUp(): void $this->fileLocator->locate('grumphp.yml', Argument::type('string')) ->willReturn('/app/vendor/fast-forward/dev-tools/grumphp.yml'); + $this->versionChecker = $this->prophesize(VersionCheckerInterface::class); + $this->versionChecker->getCurrentVersion() + ->willReturn('2.0.0'); + $this->command = new UpdateComposerJsonCommand( $this->composer->reveal(), $this->filesystem->reveal(), @@ -123,6 +131,35 @@ protected function setUp(): void new ManagedConfigPathSynchronizer(), $this->fileDiffer->reveal(), $this->io->reveal(), + $this->versionChecker->reveal(), + ); + } + + /** + * @return void + */ + #[Test] + public function managedScriptsWillFollowTheReleasedRuntimeWithoutChangingSelfExecution(): void + { + $scripts = new ReflectionMethod($this->command, 'getScripts'); + $this->versionChecker->getCurrentVersion() + ->willReturn('v2.4.1'); + + self::assertSame( + 'cpx --skip-local fast-forward/dev-tools:2.4.1', + $scripts->invoke($this->command, 'example/package')['dev-tools'], + ); + self::assertSame( + '@php bin/dev-tools', + $scripts->invoke($this->command, 'fast-forward/dev-tools')['dev-tools'], + ); + + $this->versionChecker->getCurrentVersion() + ->willReturn('dev-main'); + + self::assertSame( + 'cpx --skip-local fast-forward/dev-tools:2.0.0', + $scripts->invoke($this->command, 'example/package')['dev-tools'], ); } @@ -175,7 +212,7 @@ public function executeWillUpdateComposerJsonScriptsAndManagedMetadata(): void Argument::that(static function (string $contents): bool { $composerJson = json_decode($contents, true); - return 'dev-tools' === $composerJson['scripts']['dev-tools'] + return 'cpx --skip-local fast-forward/dev-tools:2.0.0' === $composerJson['scripts']['dev-tools'] && '@dev-tools --fix' === $composerJson['scripts']['dev-tools:fix'] && ! isset($composerJson['extra']['grumphp']['config-default-path']); }), diff --git a/tests/Console/Command/WikiCommandTest.php b/tests/Console/Command/WikiCommandTest.php index 200079c779..8e7d354f24 100644 --- a/tests/Console/Command/WikiCommandTest.php +++ b/tests/Console/Command/WikiCommandTest.php @@ -50,6 +50,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function Safe\getcwd; #[UsesClass(ContainerFactory::class)] diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index ce936fe05b..f375c78c8c 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -20,7 +20,7 @@ namespace FastForward\DevTools\Tests\Console\CommandLoader; use ArrayIterator; -use FastForward\DevTools\Console\Command\AgentsCommand; +use FastForward\DevTools\Console\Command\RuntimeCommand; use FastForward\DevTools\Console\Command\SyncCommand; use FastForward\DevTools\Console\Command\TestsCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; @@ -41,7 +41,7 @@ #[CoversClass(DevToolsCommandLoader::class)] #[UsesClass(ClassReflection::class)] -#[UsesClass(AgentsCommand::class)] +#[UsesClass(RuntimeCommand::class)] #[UsesClass(SyncCommand::class)] final class DevToolsCommandLoaderTest extends TestCase { @@ -96,16 +96,16 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void ->shouldBeCalled(); $this->finder->getIterator() ->willReturn(new ArrayIterator([ - new SplFileInfo($commandDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), + new SplFileInfo($commandDirectory . '/RuntimeCommand.php', '', 'RuntimeCommand.php'), ]))->shouldBeCalled(); - $this->container->has(AgentsCommand::class)->willReturn(true)->shouldBeCalled(); - $this->container->get(AgentsCommand::class)->willReturn($command->reveal())->shouldBeCalled(); + $this->container->has(RuntimeCommand::class)->willReturn(true)->shouldBeCalled(); + $this->container->get(RuntimeCommand::class)->willReturn($command->reveal())->shouldBeCalled(); $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); - self::assertTrue($loader->has('agents')); - self::assertSame($command->reveal(), $loader->get('agents')); + self::assertTrue($loader->has('runtime')); + self::assertSame($command->reveal(), $loader->get('runtime')); } /** @@ -204,12 +204,12 @@ public function constructorWillFailWhenCommandNameConflicts(): void '', 'FixtureDuplicateCommandName.php' ), - new SplFileInfo($srcDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), + new SplFileInfo($srcDirectory . '/RuntimeCommand.php', '', 'RuntimeCommand.php'), ]))->shouldBeCalled(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage( - 'Command agents is already registered and cannot be assigned to FastForward\\DevTools\\Console\\Command\\AgentsCommand.' + 'Command runtime is already registered and cannot be assigned to FastForward\\DevTools\\Console\\Command\\RuntimeCommand.' ); new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); diff --git a/tests/Console/Logger/OutputFormatLoggerTest.php b/tests/Console/Logger/OutputFormatLoggerTest.php index fb0c8374f0..8e49a7ae48 100644 --- a/tests/Console/Logger/OutputFormatLoggerTest.php +++ b/tests/Console/Logger/OutputFormatLoggerTest.php @@ -40,6 +40,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; + use function Safe\json_decode; #[CoversClass(OutputFormatLogger::class)] diff --git a/tests/Console/Output/OutputCapabilityDetectorTest.php b/tests/Console/Output/OutputCapabilityDetectorTest.php index 725b7dc2b8..51260d6c95 100644 --- a/tests/Console/Output/OutputCapabilityDetectorTest.php +++ b/tests/Console/Output/OutputCapabilityDetectorTest.php @@ -26,6 +26,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; + use function Safe\fopen; use function Safe\fclose; diff --git a/tests/Distribution/ExternalExecutionTest.php b/tests/Distribution/ExternalExecutionTest.php new file mode 100644 index 0000000000..40c461caa8 --- /dev/null +++ b/tests/Distribution/ExternalExecutionTest.php @@ -0,0 +1,193 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Distribution; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +use function Safe\realpath; +use function Safe\file_get_contents; +use function Safe\json_decode; +use function Safe\json_encode; + +/** + * An external runtime MUST execute consumer tests without entering its dependency graph. + * + * @copyright 2026 Felipe SayĆ£o Lobato Abreu + * @license https://opensource.org/licenses/MIT MIT License + */ +#[CoversNothing] +final class ExternalExecutionTest extends TestCase +{ + private Filesystem $filesystem; + + private string $consumer; + + /** + * @return void + */ + protected function setUp(): void + { + $this->filesystem = new Filesystem(); + $this->consumer = sys_get_temp_dir() . '/dev-tools-external-' . bin2hex(random_bytes(8)); + $this->filesystem->dumpFile($this->consumer . '/composer.json', json_encode([ + 'name' => 'fixture/consumer', + 'require' => [ + 'php' => '^8.3', + ], + 'autoload' => [ + 'psr-4' => [ + 'Consumer\\' => 'src/', + ], + ], + ], \JSON_THROW_ON_ERROR)); + $this->filesystem->dumpFile($this->consumer . '/composer.lock', '{"packages":[],"packages-dev":[]}'); + $this->filesystem->dumpFile($this->consumer . '/src/Example.php', <<<'PHP' + filesystem->dumpFile($this->consumer . '/vendor/autoload.php', <<<'PHP' + filesystem->remove($this->consumer); + } + + /** + * @return void + */ + #[Test] + public function runtimeDiscoveryWillSeparatePackageAndConsumerPathsWithoutSync(): void + { + $process = $this->runCli(['runtime', '--json']); + + self::assertSame(0, $process->getExitCode(), $process->getOutput() . $process->getErrorOutput()); + $runtime = json_decode($process->getOutput(), true, flags: \JSON_THROW_ON_ERROR); + self::assertSame(realpath(\dirname(__DIR__, 2)), realpath($runtime['package_root'])); + self::assertSame(realpath($this->consumer), realpath($runtime['project_root'])); + self::assertFileExists($runtime['autoload']); + self::assertDirectoryDoesNotExist($this->consumer . '/.agents'); + self::assertDirectoryDoesNotExist($this->consumer . '/.github'); + self::assertDirectoryDoesNotExist($this->consumer . '/vendor/fast-forward'); + } + + /** + * @param bool $fails + * + * @return void + */ + #[Test] + #[DataProvider('consumerResults')] + public function testsWillUseConsumerAutoloadAndPropagateFailuresWithoutInstallingDevTools(bool $fails): void + { + $expected = $fails ? 'incorrect value' : 'consumer source'; + $this->filesystem->dumpFile($this->consumer . '/tests/ConsumerTest.php', str_replace( + '__EXPECTED__', + $expected, + <<<'PHP_WRAP' + consumer . '/composer.json'); + $lockBefore = file_get_contents($this->consumer . '/composer.lock'); + + $process = $this->runCli(['tests', '--json']); + + self::assertSame($fails ? 1 : 0, $process->getExitCode(), $process->getOutput() . $process->getErrorOutput()); + $output = $process->getOutput() . $process->getErrorOutput(); + $result = json_decode($output, true, flags: \JSON_THROW_ON_ERROR)['context']['output']; + self::assertSame($fails ? 'failure' : 'success', $result['result']); + self::assertSame(1, $result['summary']['tests']); + self::assertSame(1, $result['summary']['assertions']); + self::assertSame($fails ? 1 : 0, $result['summary']['failures']); + + if ($fails) { + self::assertStringContainsString('incorrect value', $output); + } + + self::assertFileExists($this->consumer . '/.dev-tools/cache/phpunit/bootstrap.php'); + self::assertSame($composerBefore, file_get_contents($this->consumer . '/composer.json')); + self::assertSame($lockBefore, file_get_contents($this->consumer . '/composer.lock')); + self::assertDirectoryDoesNotExist($this->consumer . '/vendor/fast-forward'); + } + + /** + * @return iterable + */ + public static function consumerResults(): iterable + { + yield 'passing consumer' => [false]; + yield 'failing consumer' => [true]; + } + + /** + * @param list $arguments + */ + private function runCli(array $arguments): Process + { + $process = new Process([ + \PHP_BINARY, + \dirname(__DIR__, 2) . '/bin/dev-tools', + ...$arguments, + '--no-ansi', + '--no-interaction', + ], $this->consumer, [ + 'CI' => 'true', + 'FAST_FORWARD_MIN_COVERAGE' => false, + ]); + $process->setTimeout(60); + $process->run(); + + return $process; + } +} diff --git a/tests/Distribution/SkillsDistributionTest.php b/tests/Distribution/SkillsDistributionTest.php new file mode 100644 index 0000000000..52bd0c10f5 --- /dev/null +++ b/tests/Distribution/SkillsDistributionTest.php @@ -0,0 +1,90 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Distribution; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Yaml; + +use function Safe\file_get_contents; +use function Safe\glob; +use function Safe\json_decode; + +/** + * The Composer distribution MUST expose portable skills independently of the CLI. + * + * @copyright 2026 Felipe SayĆ£o Lobato Abreu + * @license https://opensource.org/licenses/MIT MIT License + */ +#[CoversNothing] +final class SkillsDistributionTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function distributionWillPublishNamespacedSkillsWithoutComposerPluginSetup(): void + { + $root = \dirname(__DIR__, 2); + $composer = json_decode(file_get_contents($root . '/composer.json'), true, flags: \JSON_THROW_ON_ERROR); + + self::assertSame('resources/skills', $composer['extra']['skills']['source']); + self::assertArrayHasKey('llm/skills', $composer['suggest']); + self::assertSame('library', $composer['type']); + self::assertArrayNotHasKey('class', $composer['extra']); + + $entrypoints = glob($root . '/resources/skills/*/SKILL.md'); + self::assertCount(8, $entrypoints); + + foreach ($entrypoints as $entrypoint) { + $contents = file_get_contents($entrypoint); + $parts = explode('---', $contents, 3); + $metadata = Yaml::parse($parts[1]); + $directory = \dirname($entrypoint); + + self::assertSame(basename($directory), $metadata['name']); + self::assertStringStartsWith('fast-forward-', $metadata['name']); + self::assertFileExists($directory . '/LICENSE'); + self::assertFalse(is_link($directory)); + self::assertStringNotContainsString('composer require --dev fast-forward/dev-tools', $contents); + self::assertStringNotContainsString('DevTools\\Composer\\Plugin', $contents); + } + } + + /** + * @return void + */ + #[Test] + public function selfConsumptionWillUseCanonicalDirectoryWithoutCacheAliases(): void + { + $root = \dirname(__DIR__, 2); + $config = json_decode(file_get_contents($root . '/skills.json'), true, flags: \JSON_THROW_ON_ERROR); + + self::assertSame('.agents/skills', $config['target']); + self::assertSame([], $config['aliases']); + self::assertFalse($config['vendor-sources']); + self::assertContains([ + 'from' => 'dir', + 'path' => 'resources/skills', + 'package' => 'fast-forward/dev-tools', + ], $config['sources']); + } +} diff --git a/tests/Environment/EnvironmentTest.php b/tests/Environment/EnvironmentTest.php index f52ab48bf9..51179c1f21 100644 --- a/tests/Environment/EnvironmentTest.php +++ b/tests/Environment/EnvironmentTest.php @@ -23,6 +23,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; + use function Safe\putenv; #[CoversClass(Environment::class)] diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index 85c4a4c57f..70bebd4090 100644 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -24,6 +24,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Path; + use function Safe\chdir; use function Safe\fileperms; use function Safe\file_put_contents; diff --git a/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php index ae023fd3f5..8a41895805 100644 --- a/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php +++ b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php @@ -22,5 +22,5 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -#[AsCommand(name: 'agents')] +#[AsCommand(name: 'runtime')] final class FixtureDuplicateCommandName extends Command {} diff --git a/tests/Funding/ComposerFundingCodecTest.php b/tests/Funding/ComposerFundingCodecTest.php index 4462b911f1..e025d75a57 100644 --- a/tests/Funding/ComposerFundingCodecTest.php +++ b/tests/Funding/ComposerFundingCodecTest.php @@ -25,6 +25,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; + use function Safe\json_decode; #[CoversClass(ComposerFundingCodec::class)] diff --git a/tests/GitAttributes/ReaderTest.php b/tests/GitAttributes/ReaderTest.php index 3fdd9dee89..60954bd42b 100644 --- a/tests/GitAttributes/ReaderTest.php +++ b/tests/GitAttributes/ReaderTest.php @@ -23,6 +23,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; + use function Safe\file_put_contents; use function Safe\unlink; diff --git a/tests/GitHooks/HookContentRendererTest.php b/tests/GitHooks/HookContentRendererTest.php index 3155b8cc1d..c04396f28e 100644 --- a/tests/GitHooks/HookContentRendererTest.php +++ b/tests/GitHooks/HookContentRendererTest.php @@ -41,13 +41,8 @@ public function renderWillReplaceTheManagedGrumPhpConfigPlaceholder(): void $renderer = new HookContentRenderer(); self::assertSame( - 'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg( - DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/workspaces/project') - ), - $renderer->render( - 'DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, - '/workspaces/project' - ), + 'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg('./grumphp.yml'), + $renderer->render('DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER), ); } } diff --git a/tests/GitHooks/PackagedHooksTest.php b/tests/GitHooks/PackagedHooksTest.php index 06245e7c34..e35d0ea18b 100644 --- a/tests/GitHooks/PackagedHooksTest.php +++ b/tests/GitHooks/PackagedHooksTest.php @@ -21,48 +21,270 @@ use FastForward\DevTools\GitHooks\HookContentRenderer; use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +use function Safe\chmod; use function Safe\file_get_contents; +use function Safe\file_put_contents; +use function Safe\realpath; +/** + * Packaged hooks MUST execute independently of the DevTools installation path. + * + * @copyright 2026 Felipe SayĆ£o Lobato Abreu + * @license https://opensource.org/licenses/MIT MIT License + */ #[CoversNothing] final class PackagedHooksTest extends TestCase { + private string $project; + + private Filesystem $filesystem; + + /** + * @return void + */ + protected function setUp(): void + { + $this->filesystem = new Filesystem(); + $this->project = sys_get_temp_dir() . '/dev-tools-hook-' . bin2hex(random_bytes(8)); + $this->filesystem->mkdir($this->project . '/bin'); + $this->project = realpath($this->project); + $this->git(['init', '--quiet']); + $this->git(['config', 'user.name', 'Hook Test User']); + $this->git(['config', 'user.email', 'hook@example.invalid']); + file_put_contents($this->project . '/staged file.txt', "Staged content\n"); + $this->git(['add', 'staged file.txt']); + file_put_contents($this->project . '/commit message.txt', "Test commit message\n"); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $this->filesystem->remove($this->project); + } + /** + * @return iterable + */ + public static function hookExecutions(): iterable + { + foreach (['pre-commit', 'commit-msg'] as $hook) { + foreach ([true, false] as $config) { + foreach ([0, 23] as $status) { + yield $hook . ($config ? ' with config ' : ' without config ') . $status => [ + $hook, + $config, + $status, + ]; + } + } + } + } + + /** + * @param string $hook + * @param bool $config + * @param int $status + * * @return void */ #[Test] - public function packagedPreCommitHookWillPreferProjectConfigAndFallbackToTheManagedPlaceholder(): void + #[DataProvider('hookExecutions')] + public function packagedHookWillInvokeCpxOnceWithArgumentsDiffAndOriginalExitStatus( + string $hook, + bool $config, + int $status + ): void { + $this->writeRunner('bin/cpx'); + if ($config) { + file_put_contents($this->project . '/grumphp.yml', "grumphp: {}\n"); + } + + $process = $this->executeHook($hook, $status); + + self::assertSame($status, $process->getExitCode(), $process->getErrorOutput()); + self::assertSame("called\n", file_get_contents($this->project . '/calls')); + self::assertSame( + $this->expectedArguments($hook, $config, true), + file_get_contents($this->project . '/arguments') + ); + self::assertSame($this->gitDiff(), file_get_contents($this->project . '/stdin')); + self::assertSame($this->project, file_get_contents($this->project . '/working-directory')); + } + + /** + * @return iterable + */ + public static function localRunners(): iterable { - $contents = file_get_contents(\dirname(__DIR__, 2) . '/resources/git-hooks/pre-commit'); + foreach (['pre-commit', 'commit-msg'] as $hook) { + foreach (['grumphp', 'grumphp.phar'] as $runner) { + yield $hook . ' ' . $runner => [$hook, $runner]; + } + } + } - self::assertStringContainsString("./grumphp.yml' ]; then", $contents); - self::assertStringContainsString( - 'DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, - $contents, + /** + * @param string $hook + * @param string $runner + * + * @return void + */ + #[Test] + #[DataProvider('localRunners')] + public function packagedHookWillPreferExecutableLocalRunnerToCpx(string $hook, string $runner): void + { + $this->writeRunner('vendor/bin/' . $runner); + file_put_contents($this->project . '/bin/cpx', "#!/bin/sh\nexit 99\n"); + chmod($this->project . '/bin/cpx', 0o755); + file_put_contents($this->project . '/grumphp.yml', "grumphp: {}\n"); + + $process = $this->executeHook($hook, 17); + + self::assertSame(17, $process->getExitCode(), $process->getErrorOutput()); + self::assertSame("called\n", file_get_contents($this->project . '/calls')); + self::assertSame( + $this->expectedArguments($hook, true, false), + file_get_contents($this->project . '/arguments') ); - self::assertStringContainsString('if [ -x "vendor/bin/grumphp" ]; then', $contents); - self::assertStringContainsString("GRUMPHP_COMMAND='vendor/bin/grumphp'", $contents); - self::assertStringContainsString('printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}"', $contents); - self::assertStringContainsString("'--config' \"\${GRUMPHP_CONFIG_FILE}\" 'git:pre-commit'", $contents); + self::assertSame($this->gitDiff(), file_get_contents($this->project . '/stdin')); } /** * @return void */ #[Test] - public function packagedCommitMsgHookWillPreferProjectConfigAndFallbackToTheManagedPlaceholder(): void + public function packagedHookWillFailClearlyWhenNoRunnerIsAvailable(): void { - $contents = file_get_contents(\dirname(__DIR__, 2) . '/resources/git-hooks/commit-msg'); + $process = $this->executeHook('pre-commit', 0); - self::assertStringContainsString("./grumphp.yml' ]; then", $contents); + self::assertSame(1, $process->getExitCode()); self::assertStringContainsString( - 'DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, + 'DevTools hooks require CPX or a project-local GrumPHP binary.', + $process->getErrorOutput() + ); + self::assertFileDoesNotExist($this->project . '/calls'); + } + + /** + * @param string $hook + * @param int $status + * + * @return Process + */ + private function executeHook(string $hook, int $status): Process + { + $contents = file_get_contents(\dirname(__DIR__, 2) . '/resources/git-hooks/' . $hook); + file_put_contents($this->project . '/hook', str_replace( + HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER, + "'./grumphp.yml'", $contents, + )); + $process = new Process( + ['/bin/sh', $this->project . '/hook', $this->project . '/commit message.txt'], + $this->project, + [ + 'PATH' => $this->project . '/bin:/usr/bin:/bin', + 'HOOK_CAPTURE_DIR' => $this->project, + 'HOOK_EXIT_STATUS' => (string) $status, + 'GIT_CONFIG_GLOBAL' => '/dev/null', + 'GIT_CONFIG_NOSYSTEM' => '1', + ], + ); + $process->run(); + + return $process; + } + + /** + * @param string $path + * + * @return void + */ + private function writeRunner(string $path): void + { + $this->filesystem->mkdir(\dirname($this->project . '/' . $path)); + file_put_contents($this->project . '/' . $path, <<<'SH' + #!/bin/sh + printf 'called\n' >> "$HOOK_CAPTURE_DIR/calls" + printf '%s\n' "$@" > "$HOOK_CAPTURE_DIR/arguments" + printf '%s' "$GRUMPHP_GIT_WORKING_DIR" > "$HOOK_CAPTURE_DIR/working-directory" + cat > "$HOOK_CAPTURE_DIR/stdin" + exit "$HOOK_EXIT_STATUS" + SH); + chmod($this->project . '/' . $path, 0o755); + } + + /** + * @param string $hook + * @param bool $config + * @param bool $cpx + * + * @return string + */ + private function expectedArguments(string $hook, bool $config, bool $cpx): string + { + $arguments = $cpx ? ['--skip-local', 'phpro/grumphp-shim:2.26.0'] : []; + if ($config) { + $arguments[] = '--config'; + $arguments[] = './grumphp.yml'; + } + + $arguments[] = 'git:' . $hook; + if ('pre-commit' === $hook) { + $arguments[] = '--skip-success-output'; + } else { + $arguments[] = '--git-user=Hook Test User'; + $arguments[] = '--git-email=hook@example.invalid'; + $arguments[] = $this->project . '/commit message.txt'; + } + + return implode("\n", $arguments) . "\n"; + } + + /** + * @return string + */ + private function gitDiff(): string + { + return $this->git( + [ + '-c', + 'diff.mnemonicprefix=false', + '-c', + 'diff.noprefix=false', + '--no-pager', + 'diff', + '--raw', + '-r', + '-p', + '-m', + '-M', + '--full-index', + '--no-color', + '--staged', + ] ); - self::assertStringContainsString('if [ -x "vendor/bin/grumphp" ]; then', $contents); - self::assertStringContainsString("GRUMPHP_COMMAND='vendor/bin/grumphp'", $contents); - self::assertStringContainsString('printf "%s\n" "${DIFF}" | exec "${GRUMPHP_COMMAND}"', $contents); - self::assertStringContainsString("'--config' \"\${GRUMPHP_CONFIG_FILE}\" 'git:commit-msg'", $contents); + } + + /** + * @param list $arguments + */ + private function git(array $arguments): string + { + $process = new Process(['git', ...$arguments], $this->project, [ + 'GIT_CONFIG_GLOBAL' => '/dev/null', + 'GIT_CONFIG_NOSYSTEM' => '1', + ]); + $process->mustRun(); + + return $process->getOutput(); } } diff --git a/tests/GitHubActions/ChangelogWorkflowTest.php b/tests/GitHubActions/ChangelogWorkflowTest.php index 6b1b5b965a..e6745defc5 100644 --- a/tests/GitHubActions/ChangelogWorkflowTest.php +++ b/tests/GitHubActions/ChangelogWorkflowTest.php @@ -71,7 +71,9 @@ public function workflowWillUseTheOfficialSingleJobCallerWithAutomaticPublishing ], $workflow['jobs']['changelog']); } - /** Confirms identical lifecycle settings with an independent consuming repository identity in the title. */ + /** + * Confirms identical lifecycle settings with an independent consuming repository identity in the title. + */ #[Test] public function consumerTemplateWillMatchTheLifecycleWithItsOwnRepositoryName(): void { diff --git a/tests/GitHubActions/RefreshPreviewPointerActionTest.php b/tests/GitHubActions/RefreshPreviewPointerActionTest.php index 9297fefa4f..5346e9b642 100644 --- a/tests/GitHubActions/RefreshPreviewPointerActionTest.php +++ b/tests/GitHubActions/RefreshPreviewPointerActionTest.php @@ -27,6 +27,7 @@ use RecursiveIteratorIterator; use SplFileInfo; use Symfony\Component\Process\Process; + use function Safe\chmod; use function Safe\file_get_contents; use function Safe\file_put_contents; diff --git a/tests/GitHubActions/ReportTemplateRuntimeTest.php b/tests/GitHubActions/ReportTemplateRuntimeTest.php new file mode 100644 index 0000000000..bd826b91bc --- /dev/null +++ b/tests/GitHubActions/ReportTemplateRuntimeTest.php @@ -0,0 +1,160 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\GitHubActions; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +use function Safe\file_get_contents; +use function Safe\file_put_contents; +use function Safe\json_decode; +use function Safe\mkdir; +use function Safe\realpath; + +#[CoversNothing] +final class ReportTemplateRuntimeTest extends TestCase +{ + private const string PROJECT_ROOT = __DIR__ . '/../..'; + + private string $workspace; + + /** + * @return void + */ + protected function setUp(): void + { + $this->workspace = sys_get_temp_dir() . '/report-template-' . bin2hex(random_bytes(4)); + mkdir($this->workspace); + $this->workspace = realpath($this->workspace); + } + + /** + * @return void + */ + protected function tearDown(): void + { + new Filesystem() + ->remove($this->workspace); + } + + /** + * @return void + */ + #[Test] + public function reportTemplateWillFallBackToTheIsolatedRuntimeDependency(): void + { + $process = $this->resolveTemplate(); + $process->mustRun(); + + $metadata = json_decode($process->getOutput(), true); + + self::assertSame( + realpath(self::PROJECT_ROOT . '/vendor/fast-forward/phpdoc-bootstrap-template'), + $metadata['DEV_TOOLS_REPORTS_TEMPLATE'], + ); + self::assertStringContainsString( + 'DEV_TOOLS_REPORTS_TEMPLATE=' . $metadata['DEV_TOOLS_REPORTS_TEMPLATE'], + file_get_contents($this->workspace . '/env'), + ); + } + + /** + * @return void + */ + #[Test] + public function reportTemplateWillUseConsumerPathAndMatchingConsumerVersionTogether(): void + { + $template = $this->createConsumerTemplate('v2.4.0'); + $process = $this->resolveTemplate(); + $process->mustRun(); + + $metadata = json_decode($process->getOutput(), true); + + self::assertSame($template, $metadata['DEV_TOOLS_REPORTS_TEMPLATE']); + self::assertSame('v2.4.0', $metadata['DEV_TOOLS_REPORTS_TEMPLATE_VERSION']); + self::assertSame('consumer-reference', $metadata['DEV_TOOLS_REPORTS_TEMPLATE_REFERENCE']); + } + + /** + * @return void + */ + #[Test] + public function reportTemplateWillRejectAnOldSelectedConsumerTemplate(): void + { + $this->createConsumerTemplate('v2.0.0'); + $process = $this->resolveTemplate(); + $process->run(); + + self::assertFalse($process->isSuccessful()); + self::assertStringContainsString( + 'selected documentation template must be installed at version 2.1.0', + $process->getErrorOutput() + ); + self::assertFileDoesNotExist($this->workspace . '/env'); + } + + /** + * @param string $version + * + * @return string + */ + private function createConsumerTemplate(string $version): string + { + $template = $this->workspace . '/vendor/fast-forward/phpdoc-bootstrap-template'; + mkdir($template, 0o777, true); + mkdir($this->workspace . '/vendor/composer'); + file_put_contents( + $this->workspace . '/vendor/autoload.php', + 'register(); return $loader;', + ); + file_put_contents( + $this->workspace . '/vendor/composer/installed.php', + ' [], + 'versions' => [ + 'fast-forward/phpdoc-bootstrap-template' => [ + 'pretty_version' => $version, + 'reference' => 'consumer-reference', + 'install_path' => $template, + ], + ], + ], true) . ';', + ); + + return $template; + } + + /** + * @return Process + */ + private function resolveTemplate(): Process + { + return new Process([ + \PHP_BINARY, + self::PROJECT_ROOT . '/.github/actions/php/setup-composer/resolve-report-template.php', + ], $this->workspace, [ + 'DEV_TOOLS_AUTOLOAD' => realpath(self::PROJECT_ROOT . '/vendor/autoload.php'), + 'GITHUB_ENV' => $this->workspace . '/env', + ]); + } +} diff --git a/tests/GitHubActions/ReportsWorkflowTest.php b/tests/GitHubActions/ReportsWorkflowTest.php index 3fbb9d2774..4c600bc966 100644 --- a/tests/GitHubActions/ReportsWorkflowTest.php +++ b/tests/GitHubActions/ReportsWorkflowTest.php @@ -27,6 +27,50 @@ #[CoversNothing] final class ReportsWorkflowTest extends TestCase { + /** + * @return void + */ + #[Test] + public function nativeReportsWillResolveOneSelectedTemplateForVersionAndAssetVerification(): void + { + $workflow = Yaml::parseFile(__DIR__ . '/../../.github/workflows/reports-native.yml'); + $steps = $workflow['jobs']['reports']['steps']; + + self::assertStringContainsString( + 'resolve-report-template.php', + $this->findStep($steps, 'Verify selected template version')['run'], + ); + self::assertStringContainsString( + "getenv('DEV_TOOLS_REPORTS_TEMPLATE')", + $this->findStep($steps, 'Verify generated identity assets')['run'], + ); + self::assertSame( + './.dev-tools-actions/.github/actions/php/setup-phpdoc', + $this->findStep($steps, 'Install signed documentation generator in isolation')['uses'], + ); + } + + /** + * @return void + */ + #[Test] + public function consumerWrappersWillPinWorkflowAndRuntimeToTheSameRelease(): void + { + foreach (['tests', 'reports', 'wiki', 'auto-assign', 'label-sync', 'review'] as $name) { + $workflow = Yaml::parseFile(__DIR__ . '/../../resources/github-actions/' . $name . '.yml'); + foreach ($workflow['jobs'] as $job) { + if (! isset($job['uses']) || ! str_starts_with($job['uses'], 'php-fast-forward/dev-tools/')) { + continue; + } + + self::assertStringEndsWith('@v2.0.0', $job['uses']); + if (\in_array($name, ['tests', 'reports', 'wiki'], true)) { + self::assertSame('2.0.0', $job['with']['dev-tools-version']); + } + } + } + } + /** * @return void */ diff --git a/tests/GitHubActions/RetryTransientFailuresActionTest.php b/tests/GitHubActions/RetryTransientFailuresActionTest.php index e72a8e7be8..c06ba6d868 100644 --- a/tests/GitHubActions/RetryTransientFailuresActionTest.php +++ b/tests/GitHubActions/RetryTransientFailuresActionTest.php @@ -27,6 +27,7 @@ use RecursiveIteratorIterator; use SplFileInfo; use Symfony\Component\Process\Process; + use function Safe\chmod; use function Safe\file_get_contents; use function Safe\file_put_contents; diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php index 02bd73698b..4ae3cb3684 100644 --- a/tests/GitHubActions/SetupComposerActionTest.php +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -19,21 +19,18 @@ namespace FastForward\DevTools\Tests\GitHubActions; -use FilesystemIterator; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use SplFileInfo; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; + use function Safe\chmod; use function Safe\file_get_contents; use function Safe\file_put_contents; +use function Safe\json_encode; use function Safe\mkdir; use function Safe\realpath; -use function Safe\rmdir; -use function Safe\unlink; #[CoversNothing] final class SetupComposerActionTest extends TestCase @@ -49,6 +46,7 @@ protected function setUp(): void { $this->workspace = sys_get_temp_dir() . '/setup-composer-action-test-' . bin2hex(random_bytes(4)); mkdir($this->workspace, 0o777, true); + $this->workspace = realpath($this->workspace); } /** @@ -56,297 +54,201 @@ protected function setUp(): void */ protected function tearDown(): void { - if (! is_dir($this->workspace)) { - return; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($this->workspace, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST, - ); - - /** @var SplFileInfo $item */ - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir($item->getPathname()); - - continue; - } - - unlink($item->getPathname()); - } - - rmdir($this->workspace); + new Filesystem() + ->remove($this->workspace); } /** * @return void */ #[Test] - public function detectRuntimeWillPreferTheConsumerLocalInstallation(): void + public function detectRuntimeWillUseTheWorkspaceRootForDevToolsSelfTests(): void { - $this->createInstalledRuntimeFiles($this->workspace); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - - $this->runActionScript('detect-dev-tools-runtime.sh', [ - 'GITHUB_OUTPUT' => $files['output'], - ]); - - $outputs = $this->parseKeyValueFile($files['output']); - + $this->createRuntime($this->workspace); + $process = $this->runScript('detect-dev-tools-runtime.sh'); + self::assertTrue($process->isSuccessful(), $process->getErrorOutput()); + $outputs = $this->readValues('output'); self::assertSame('local', $outputs['source']); - self::assertSame('false', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/vendor/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/vendor/autoload.php', $outputs['autoload']); + self::assertSame($this->workspace . '/bin/dev-tools', $outputs['binary']); + self::assertSame($this->workspace . '/vendor/autoload.php', $outputs['autoload']); } /** * @return void */ #[Test] - public function fallbackInstallShouldNotRequireComposerLockFile(): void + public function detectRuntimeWillIgnoreConsumerInstalledDevToolsAndWorkflowSource(): void { - $actionFile = self::ACTION_PATH . '/action.yml'; - $actionContents = file_get_contents($actionFile); - - self::assertStringContainsString("require-lock-file: 'false'", $actionContents); - self::assertStringNotContainsString("require-lock-file: 'true'", $actionContents); + $this->createRuntime($this->workspace, 'example/consumer'); + $this->createRuntime($this->workspace . '/vendor/fast-forward/dev-tools'); + $this->createRuntime($this->workspace . '/.dev-tools-actions'); + $process = $this->runScript('detect-dev-tools-runtime.sh'); + self::assertTrue($process->isSuccessful(), $process->getErrorOutput()); + self::assertSame('cpx', $this->readValues('output')['source']); } /** * @return void */ #[Test] - public function detectRuntimeWillFallbackToTheWorkflowSourceWhenTheConsumerDoesNotInstallDevTools(): void + public function consumerRuntimeWillUsePinnedCpxMetadataAndPreserveArguments(): void { - $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - - $this->runActionScript('detect-dev-tools-runtime.sh', [ - 'GITHUB_OUTPUT' => $files['output'], + $runtime = $this->workspace . '/isolated runtime'; + $this->createRuntime($runtime); + $this->createCpx(json_encode([ + 'binary' => $runtime . '/bin/dev-tools', + 'autoload' => $runtime . '/vendor/autoload.php', + ])); + + $process = $this->runScript('expose-dev-tools-runtime.sh', [ + 'INPUT_DEV_TOOLS_VERSION' => '2.0.1', ]); + self::assertTrue($process->isSuccessful(), $process->getErrorOutput()); + $outputs = $this->readValues('output'); + $environment = $this->readValues('env'); - $outputs = $this->parseKeyValueFile($files['output']); + self::assertSame('cpx', $outputs['source']); + self::assertSame($runtime . '/bin/dev-tools', $outputs['binary']); + self::assertSame($runtime . '/vendor/autoload.php', $outputs['autoload']); + self::assertSame($outputs['autoload'], $environment['DEV_TOOLS_AUTOLOAD']); + self::assertSame($outputs['autoload'], $environment['DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD']); + self::assertSame($outputs['binary'], $environment['DEV_TOOLS_BINARY']); + self::assertSame('cpx', $environment['DEV_TOOLS_RUNTIME_SOURCE']); + self::assertStringContainsString( + "--skip-local\nfast-forward/dev-tools:2.0.1\nruntime\n--json", + file_get_contents($this->workspace . '/cpx-arguments') + ); + self::assertStringContainsString( + $this->workspace . '/dev-tools-runtime/bin', + file_get_contents($this->workspace . '/path') + ); - self::assertSame('workflow', $outputs['source']); - self::assertSame('true', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); + $command = new Process([$outputs['command'], 'wiki', '--target=directory with spaces'], $this->workspace); + $command->mustRun(); + self::assertSame("wiki\n--target=directory with spaces\n", $command->getOutput()); } /** * @return void */ #[Test] - public function detectRuntimeWillPreferTheWorkspaceRootRepositoryCheckout(): void + public function consumerRuntimeWillRejectRelativeMetadataPaths(): void { - $this->createRepositoryRuntimeFiles($this->workspace); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - - $this->runActionScript('detect-dev-tools-runtime.sh', [ - 'GITHUB_OUTPUT' => $files['output'], - ]); - - $outputs = $this->parseKeyValueFile($files['output']); - - self::assertSame('local', $outputs['source']); - self::assertSame('false', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/vendor/autoload.php', $outputs['autoload']); + $this->createCpx('{"binary":"vendor/bin/dev-tools","autoload":"vendor/autoload.php"}'); + $process = $this->runScript('expose-dev-tools-runtime.sh'); + self::assertFalse($process->isSuccessful()); + self::assertStringContainsString('Invalid DevTools runtime path', $process->getErrorOutput()); + self::assertFileDoesNotExist($this->workspace . '/env'); } /** * @return void */ #[Test] - public function detectRuntimeWillIgnoreAnUnrelatedWorkspaceRepositoryBinary(): void + public function consumerRuntimeWillRejectMissingAutoload(): void { - $this->createRepositoryRuntimeFiles($this->workspace, 'example/consumer'); - $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - - $this->runActionScript('detect-dev-tools-runtime.sh', [ - 'GITHUB_OUTPUT' => $files['output'], - ]); - - $outputs = $this->parseKeyValueFile($files['output']); - - self::assertSame('workflow', $outputs['source']); - self::assertSame('true', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); + $this->createRuntime($this->workspace . '/isolated'); + $this->createCpx(json_encode([ + 'binary' => $this->workspace . '/isolated/bin/dev-tools', + 'autoload' => $this->workspace . '/missing.php', + ])); + $process = $this->runScript('expose-dev-tools-runtime.sh'); + self::assertFalse($process->isSuccessful()); + self::assertStringContainsString('autoload file was not found', $process->getErrorOutput()); } /** * @return void */ #[Test] - public function detectRuntimeWillIgnoreAnUnrelatedInstalledBinary(): void + public function consumerRuntimeWillRejectFloatingVersionsBeforeExecutingCpx(): void { - $this->createInstalledRuntimeFiles($this->workspace, 'example/consumer'); - $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - - $this->runActionScript('detect-dev-tools-runtime.sh', [ - 'GITHUB_OUTPUT' => $files['output'], + $this->createCpx('{}'); + $process = $this->runScript('expose-dev-tools-runtime.sh', [ + 'INPUT_DEV_TOOLS_VERSION' => 'dev-main', ]); - - $outputs = $this->parseKeyValueFile($files['output']); - - self::assertSame('workflow', $outputs['source']); - self::assertSame('true', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); + self::assertFalse($process->isSuccessful()); + self::assertStringContainsString('must be an exact published version', $process->getErrorOutput()); + self::assertFileDoesNotExist($this->workspace . '/cpx-arguments'); } /** * @return void */ #[Test] - public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWorkflowFallback(): void + public function actionWillInstallCpxWithoutInstallingWorkflowSourceDependencies(): void { - $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); - $resolvedWorkspace = realpath($this->workspace); - - $files = $this->createGitHubActionFiles(); - $runnerTemp = $this->workspace . '/runner-temp'; - mkdir($runnerTemp, 0o777, true); - - $this->runActionScript('expose-dev-tools-runtime.sh', [ - 'GITHUB_ENV' => $files['env'], - 'GITHUB_OUTPUT' => $files['output'], - 'GITHUB_PATH' => $files['path'], - 'RUNNER_TEMP' => $runnerTemp, - ]); - - $outputs = $this->parseKeyValueFile($files['output']); - $environment = $this->parseKeyValueFile($files['env']); - $pathEntries = array_filter(explode("\n", trim(file_get_contents($files['path'])))); - - self::assertSame('workflow', $outputs['source']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/bin/dev-tools', $outputs['binary']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); - self::assertSame($outputs['binary'], $environment['DEV_TOOLS_BINARY']); - self::assertSame($outputs['autoload'], $environment['DEV_TOOLS_AUTOLOAD']); - self::assertSame($outputs['autoload'], $environment['DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD']); - self::assertSame('workflow', $environment['DEV_TOOLS_RUNTIME_SOURCE']); - self::assertContains($runnerTemp . '/dev-tools-runtime/bin', $pathEntries); - - $wrapper = $outputs['command']; - $process = new Process([$wrapper, 'wiki', '--target=.github/wiki'], $this->workspace); - $process->mustRun(); - - self::assertSame("dev-tools:wiki --target=.github/wiki\n", $process->getOutput()); + $contents = file_get_contents(self::ACTION_PATH . '/action.yml'); + self::assertStringContainsString('tools: cpx', $contents); + self::assertStringContainsString("default: '2.0.0'", $contents); + self::assertStringNotContainsString('Install fallback DevTools runtime', $contents); } /** - * @param string $runtimeRoot - * @param string $packageName + * @param string $root + * @param string $name * * @return void */ - private function createInstalledRuntimeFiles( - string $runtimeRoot, - string $packageName = 'fast-forward/dev-tools' - ): void { - mkdir($runtimeRoot . '/vendor/bin', 0o777, true); - mkdir($runtimeRoot . '/vendor/' . $packageName, 0o777, true); - file_put_contents( - $runtimeRoot . '/vendor/bin/dev-tools', - "#!/usr/bin/env bash\nprintf 'dev-tools:%s\\n' \"\$*\"\n", - ); - chmod($runtimeRoot . '/vendor/bin/dev-tools', 0o755); - file_put_contents( - $runtimeRoot . '/vendor/' . $packageName . '/composer.json', - \sprintf("{\n \"name\": \"%s\"\n}\n", $packageName) - ); - file_put_contents($runtimeRoot . '/vendor/autoload.php', " $name, + ])); + file_put_contents($root . '/vendor/autoload.php', 'workspace . '/mock-bin', 0o777, true); + file_put_contents($this->workspace . '/metadata.json', $metadata); file_put_contents( - $runtimeRoot . '/bin/dev-tools', - "#!/usr/bin/env bash\nprintf 'dev-tools:%s\\n' \"\$*\"\n", + $this->workspace . '/mock-bin/cpx', + "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > cpx-arguments\ncat metadata.json\n" ); - chmod($runtimeRoot . '/bin/dev-tools', 0o755); - file_put_contents($runtimeRoot . '/composer.json', \sprintf("{\n \"name\": \"%s\"\n}\n", $packageName)); - file_put_contents($runtimeRoot . '/vendor/autoload.php', "workspace . '/mock-bin/cpx', 0o755); } /** - * @return array{env: string, output: string, path: string} - */ - private function createGitHubActionFiles(): array - { - $directory = $this->workspace . '/.github-action-files'; - - mkdir($directory, 0o777, true); - - return [ - 'env' => $directory . '/github-env', - 'output' => $directory . '/github-output', - 'path' => $directory . '/github-path', - ]; - } - - /** - * @param string $script * @param array $environment - * - * @return void + * @param string $script */ - private function runActionScript(string $script, array $environment = []): void + private function runScript(string $script, array $environment = []): Process { - $process = new Process( - ['bash', self::ACTION_PATH . '/' . $script], - $this->workspace, - $environment + [ - 'INPUT_DEV_TOOLS_SOURCE_DIRECTORY' => '.dev-tools-actions', - ], - ); + $process = new Process(['bash', self::ACTION_PATH . '/' . $script], $this->workspace, $environment + [ + 'GITHUB_OUTPUT' => $this->workspace . '/output', + 'GITHUB_ENV' => $this->workspace . '/env', + 'GITHUB_PATH' => $this->workspace . '/path', + 'RUNNER_TEMP' => $this->workspace, + 'PATH' => $this->workspace . '/mock-bin:' . getenv('PATH'), + ]); + $process->run(); - $process->mustRun(); + return $process; } /** - * @param string $path + * @param string $file * * @return array */ - private function parseKeyValueFile(string $path): array + private function readValues(string $file): array { - if (! is_file($path)) { - return []; - } - - $entries = []; - - foreach (array_filter(explode("\n", trim(file_get_contents($path)))) as $line) { + $values = []; + foreach (array_filter(explode("\n", trim(file_get_contents($this->workspace . '/' . $file)))) as $line) { [$key, $value] = explode('=', $line, 2); - $entries[$key] = $value; + $values[$key] = $value; } - return $entries; + return $values; } } diff --git a/tests/GitHubActions/WikiWorkflowsTest.php b/tests/GitHubActions/WikiWorkflowsTest.php index 777624875d..ef693d9d38 100644 --- a/tests/GitHubActions/WikiWorkflowsTest.php +++ b/tests/GitHubActions/WikiWorkflowsTest.php @@ -27,6 +27,29 @@ #[CoversNothing] final class WikiWorkflowsTest extends TestCase { + /** + * @return void + */ + #[Test] + public function directWikiGenerationWillUseTheExposedRuntimeWithoutInstallingWorkflowSource(): void + { + $workflow = Yaml::parseFile(__DIR__ . '/../../.github/workflows/wiki-direct.yml'); + $steps = $workflow['jobs']['generate']['steps']; + $setup = $this->findStep($steps, 'Setup PHP and dependencies'); + $generation = $this->findStep($steps, 'Generate API pages')['run']; + + self::assertSame('${{ inputs.dev-tools-version }}', $setup['with']['dev-tools-version']); + self::assertStringContainsString('require getenv("DEV_TOOLS_AUTOLOAD")', $generation); + self::assertStringContainsString('dev-tools wiki --target=', $generation); + self::assertStringNotContainsString('.dev-tools-actions/vendor', $generation); + self::assertStringNotContainsString('cp ', $generation); + self::assertNotContains('Install the pinned Markdown renderer', array_column($steps, 'name')); + self::assertSame( + './.dev-tools-actions/.github/actions/php/setup-phpdoc', + $this->findStep($steps, 'Install signed documentation generator in isolation')['uses'], + ); + } + /** * @return void */ diff --git a/tests/GitIgnore/GitIgnoreTest.php b/tests/GitIgnore/GitIgnoreTest.php index 6a695e9448..b27ef53db8 100644 --- a/tests/GitIgnore/GitIgnoreTest.php +++ b/tests/GitIgnore/GitIgnoreTest.php @@ -24,6 +24,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; + use function Safe\file_put_contents; use function Safe\unlink; diff --git a/tests/GitIgnore/ReaderTest.php b/tests/GitIgnore/ReaderTest.php index 5476d30934..3f94715902 100644 --- a/tests/GitIgnore/ReaderTest.php +++ b/tests/GitIgnore/ReaderTest.php @@ -26,6 +26,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; + use function Safe\file_put_contents; use function Safe\unlink; diff --git a/tests/Path/DevToolsPathResolverTest.php b/tests/Path/DevToolsPathResolverTest.php index fc2d62fd5c..e75fd0a828 100644 --- a/tests/Path/DevToolsPathResolverTest.php +++ b/tests/Path/DevToolsPathResolverTest.php @@ -27,6 +27,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; + use function Safe\file_put_contents; use function Safe\mkdir; use function Safe\rmdir; diff --git a/tests/Path/WorkingProjectPathResolverTest.php b/tests/Path/WorkingProjectPathResolverTest.php index 1dd4a44082..6879c15f63 100644 --- a/tests/Path/WorkingProjectPathResolverTest.php +++ b/tests/Path/WorkingProjectPathResolverTest.php @@ -28,6 +28,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; + use function Safe\scandir; use function Safe\rmdir; use function Safe\unlink; diff --git a/tests/PhpUnit/Coverage/CoverageSummaryLoaderTest.php b/tests/PhpUnit/Coverage/CoverageSummaryLoaderTest.php index 99a2847a47..fe5abf0c13 100644 --- a/tests/PhpUnit/Coverage/CoverageSummaryLoaderTest.php +++ b/tests/PhpUnit/Coverage/CoverageSummaryLoaderTest.php @@ -31,6 +31,7 @@ use SebastianBergmann\CodeCoverage\Driver\Driver; use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\Report\PHP as PhpReport; + use function Safe\file_put_contents; use function Safe\tempnam; use function Safe\unlink; diff --git a/tests/Process/ProcessQueueTest.php b/tests/Process/ProcessQueueTest.php index 62e27f553e..545b27f4f7 100644 --- a/tests/Process/ProcessQueueTest.php +++ b/tests/Process/ProcessQueueTest.php @@ -38,6 +38,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Process; + use function Safe\preg_replace; #[CoversClass(ProcessQueue::class)] diff --git a/tests/Project/ProjectCapabilitiesResolverTest.php b/tests/Project/ProjectCapabilitiesResolverTest.php index 68e271dc1b..1133b0fcf9 100644 --- a/tests/Project/ProjectCapabilitiesResolverTest.php +++ b/tests/Project/ProjectCapabilitiesResolverTest.php @@ -31,6 +31,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; + use function Safe\chdir; use function Safe\file_put_contents; use function Safe\getcwd; diff --git a/tests/SelfUpdate/ComposerSelfUpdateRunnerTest.php b/tests/SelfUpdate/ComposerSelfUpdateRunnerTest.php index 6520479645..e50f7196ef 100644 --- a/tests/SelfUpdate/ComposerSelfUpdateRunnerTest.php +++ b/tests/SelfUpdate/ComposerSelfUpdateRunnerTest.php @@ -78,15 +78,15 @@ protected function setUp(): void * @return void */ #[Test] - public function updateWillRunProjectComposerUpdate(): void + public function updateWillRunCpxUpdate(): void { - $this->processBuilder->build('composer update') + $this->processBuilder->build('cpx update') ->willReturn($this->process->reveal()); $this->processQueue->add( $this->process->reveal(), false, false, - 'Updating project DevTools installation', + 'Updating CPX DevTools cache', )->shouldBeCalledOnce(); $this->processQueue->run($this->output->reveal()) ->willReturn(ProcessQueueInterface::SUCCESS); diff --git a/tests/ServiceProvider/DevToolsServiceProviderTest.php b/tests/ServiceProvider/DevToolsServiceProviderTest.php index efeac2342f..392fe573b4 100644 --- a/tests/ServiceProvider/DevToolsServiceProviderTest.php +++ b/tests/ServiceProvider/DevToolsServiceProviderTest.php @@ -29,6 +29,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocatorInterface; + use function Safe\chdir; use function Safe\file_put_contents; use function Safe\getcwd; diff --git a/tests/Sync/PackagedDirectorySynchronizerTest.php b/tests/Sync/PackagedDirectorySynchronizerTest.php deleted file mode 100644 index bd6c11ceee..0000000000 --- a/tests/Sync/PackagedDirectorySynchronizerTest.php +++ /dev/null @@ -1,334 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Sync; - -use ArrayIterator; -use FastForward\DevTools\Console\Output\GithubActionOutput; -use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use FastForward\DevTools\Sync\SynchronizeResult; -use FastForward\DevTools\Filesystem\FinderFactoryInterface; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Log\LoggerInterface; -use Symfony\Component\Finder\Finder; -use Symfony\Component\Finder\SplFileInfo; - -#[CoversClass(PackagedDirectorySynchronizer::class)] -#[UsesClass(GithubActionOutput::class)] -#[UsesClass(SynchronizeResult::class)] -final class PackagedDirectorySynchronizerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $filesystem; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $finderFactory; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $finder; - - /** - * @var ObjectProphecy - */ - private ObjectProphecy $logger; - - /** - * @return void - */ - protected function setUp(): void - { - $this->filesystem = $this->prophesize(FilesystemInterface::class); - $this->finderFactory = $this->prophesize(FinderFactoryInterface::class); - $this->finder = $this->prophesize(Finder::class); - $this->logger = $this->prophesize(LoggerInterface::class); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWithMissingPackagePathWillReturnFailedResult(): void - { - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(false); - $this->logger->error('No packaged .agents/agents found at: /package/.agents/agents') - ->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertTrue($result->failed()); - self::assertSame([], $result->getCreatedLinks()); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWithMissingTargetDirWillCreateItAndCreateLinks(): void - { - $entryPath = '/package/.agents/agents/issue-editor'; - $relativeEntryPath = '../../../package/.agents/agents/issue-editor'; - - $this->mockFinder($this->createEntry('issue-editor', $entryPath)); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(true); - $this->filesystem->exists('/consumer/.agents/agents') - ->willReturn(false); - $this->filesystem->mkdir('/consumer/.agents/agents') - ->shouldBeCalledOnce(); - $this->logger->info('Created .agents/agents directory.') - ->shouldBeCalledOnce(); - $this->filesystem->exists('/consumer/.agents/agents/issue-editor') - ->willReturn(false); - $this->filesystem->getDirectory('/consumer/.agents/agents/issue-editor') - ->willReturn('/consumer/.agents/agents') - ->shouldBeCalledOnce(); - $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') - ->willReturn($relativeEntryPath) - ->shouldBeCalledOnce(); - $this->filesystem->symlink($relativeEntryPath, '/consumer/.agents/agents/issue-editor') - ->shouldBeCalledOnce(); - $this->logger->info('Created link: issue-editor -> ' . $relativeEntryPath)->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertFalse($result->failed()); - self::assertSame(['issue-editor'], $result->getCreatedLinks()); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWillPreserveExistingValidSymlink(): void - { - $entryPath = '/package/.agents/agents/issue-editor'; - $targetLink = '/consumer/.agents/agents/issue-editor'; - - $this->mockFinder($this->createEntry('issue-editor', $entryPath)); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(true); - $this->filesystem->exists('/consumer/.agents/agents') - ->willReturn(true); - $this->filesystem->exists($targetLink) - ->willReturn(true); - $this->filesystem->readlink($targetLink) - ->willReturn($entryPath); - $this->filesystem->readlink($targetLink, true) - ->willReturn($entryPath); - $this->filesystem->exists($entryPath) - ->willReturn(true); - $this->logger->notice('Preserved existing link: issue-editor') - ->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertFalse($result->failed()); - self::assertSame([], $result->getCreatedLinks()); - self::assertSame(['issue-editor'], $result->getPreservedLinks()); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWillRepairBrokenSymlink(): void - { - $entryPath = '/package/.agents/agents/issue-editor'; - $targetLink = '/consumer/.agents/agents/issue-editor'; - $brokenPath = '/obsolete/.agents/agents/issue-editor'; - $relativeEntryPath = '../../../package/.agents/agents/issue-editor'; - - $this->mockFinder($this->createEntry('issue-editor', $entryPath)); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(true); - $this->filesystem->exists('/consumer/.agents/agents') - ->willReturn(true); - $this->filesystem->exists($targetLink) - ->willReturn(true); - $this->filesystem->readlink($targetLink) - ->willReturn($brokenPath); - $this->filesystem->readlink($targetLink, true) - ->willReturn($brokenPath); - $this->filesystem->exists($brokenPath) - ->willReturn(false); - $this->filesystem->remove($targetLink) - ->shouldBeCalledOnce(); - $this->filesystem->getDirectory($targetLink) - ->willReturn('/consumer/.agents/agents') - ->shouldBeCalledOnce(); - $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') - ->willReturn($relativeEntryPath) - ->shouldBeCalledOnce(); - $this->filesystem->symlink($relativeEntryPath, $targetLink) - ->shouldBeCalledOnce(); - $this->logger->notice('Existing link is broken: issue-editor (removing and recreating)') - ->shouldBeCalledOnce(); - $this->logger->info('Created link: issue-editor -> ' . $relativeEntryPath) - ->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertFalse($result->failed()); - self::assertSame(['issue-editor'], $result->getCreatedLinks()); - self::assertSame(['issue-editor'], $result->getRemovedBrokenLinks()); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWillPreserveExistingNonSymlinkDirectory(): void - { - $entryPath = '/package/.agents/agents/issue-editor'; - $targetLink = '/consumer/.agents/agents/issue-editor'; - - $this->mockFinder($this->createEntry('issue-editor', $entryPath)); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(true); - $this->filesystem->exists('/consumer/.agents/agents') - ->willReturn(true); - $this->filesystem->exists($targetLink) - ->willReturn(true); - $this->filesystem->readlink($targetLink) - ->willReturn(null); - $this->logger->notice( - 'Existing non-symlink found: issue-editor (keeping as is, skipping link creation)' - )->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertFalse($result->failed()); - self::assertSame([], $result->getCreatedLinks()); - self::assertSame(['issue-editor'], $result->getPreservedLinks()); - } - - /** - * @return void - */ - #[Test] - public function synchronizeWillCreateLinksForTopLevelFiles(): void - { - $entryPath = '/package/.agents/agents/issue-editor.md'; - $targetLink = '/consumer/.agents/agents/issue-editor.md'; - $relativeEntryPath = '../../../package/.agents/agents/issue-editor.md'; - - $this->mockFinder($this->createEntry('issue-editor.md', $entryPath, false)); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(true); - $this->filesystem->exists('/consumer/.agents/agents') - ->willReturn(true); - $this->filesystem->exists($targetLink) - ->willReturn(false); - $this->filesystem->getDirectory($targetLink) - ->willReturn('/consumer/.agents/agents') - ->shouldBeCalledOnce(); - $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') - ->willReturn($relativeEntryPath . '/') - ->shouldBeCalledOnce(); - $this->filesystem->symlink($relativeEntryPath, $targetLink) - ->shouldBeCalledOnce(); - $this->logger->info('Created link: issue-editor.md -> ' . $relativeEntryPath) - ->shouldBeCalledOnce(); - - $result = $this->createSynchronizer() - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertFalse($result->failed()); - self::assertSame(['issue-editor.md'], $result->getCreatedLinks()); - } - - /** - * @param SplFileInfo $entries - * - * @return void - */ - private function mockFinder(SplFileInfo ...$entries): void - { - $finder = $this->finder->reveal(); - - $this->finderFactory->create() - ->willReturn($finder) - ->shouldBeCalledOnce(); - $this->finder->in('/package/.agents/agents') - ->willReturn($finder) - ->shouldBeCalledOnce(); - $this->finder->depth('== 0') - ->willReturn($finder) - ->shouldBeCalledOnce(); - $this->finder->getIterator() - ->willReturn(new ArrayIterator($entries)); - } - - /** - * @param string $entryName - * @param string $sourcePath - * @param bool $isDirectory - * - * @return SplFileInfo - */ - private function createEntry(string $entryName, string $sourcePath, bool $isDirectory = true): SplFileInfo - { - $entry = $this->prophesize(SplFileInfo::class); - $entry->getFilename() - ->willReturn($entryName); - $entry->getRealPath() - ->willReturn($sourcePath); - $entry->isDir() - ->willReturn($isDirectory); - - return $entry->reveal(); - } - - /** - * @return PackagedDirectorySynchronizer - */ - private function createSynchronizer(): PackagedDirectorySynchronizer - { - return new PackagedDirectorySynchronizer( - $this->filesystem->reveal(), - $this->finderFactory->reveal(), - $this->logger->reveal(), - ); - } -} diff --git a/tests/Sync/SynchronizeResultTest.php b/tests/Sync/SynchronizeResultTest.php deleted file mode 100644 index 5fb89fe8de..0000000000 --- a/tests/Sync/SynchronizeResultTest.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Sync; - -use FastForward\DevTools\Sync\SynchronizeResult; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; - -#[CoversClass(SynchronizeResult::class)] -final class SynchronizeResultTest extends TestCase -{ - /** - * @return void - */ - #[Test] - public function addCreatedLinkWillRecordTheLinkName(): void - { - $result = new SynchronizeResult(); - - $result->addCreatedLink('issue-editor'); - - self::assertSame(['issue-editor'], $result->getCreatedLinks()); - } - - /** - * @return void - */ - #[Test] - public function addPreservedLinkWillRecordTheLinkName(): void - { - $result = new SynchronizeResult(); - - $result->addPreservedLink('issue-editor'); - - self::assertSame(['issue-editor'], $result->getPreservedLinks()); - } - - /** - * @return void - */ - #[Test] - public function addRemovedBrokenLinkWillRecordTheLinkName(): void - { - $result = new SynchronizeResult(); - - $result->addRemovedBrokenLink('issue-editor'); - - self::assertSame(['issue-editor'], $result->getRemovedBrokenLinks()); - } - - /** - * @return void - */ - #[Test] - public function markFailedWillSetTheFailedFlag(): void - { - $result = new SynchronizeResult(); - - self::assertFalse($result->failed()); - - $result->markFailed(); - - self::assertTrue($result->failed()); - } -} From 69b4811b54601fc45efb4f99e867d0d49a6210b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 23 Oct 2026 18:23:44 -0300 Subject: [PATCH 2/5] fix: restore PHP 8.3 and CI fixture compatibility --- src/Config/ComposerDependencyAnalyserConfig.php | 1 - tests/Config/ComposerDependencyAnalyserConfigTest.php | 4 ++++ tests/Console/Command/RuntimeCommandTest.php | 4 ++-- tests/Distribution/ExternalExecutionTest.php | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index e2e2fb0b27..1a3a5d93af 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -62,7 +62,6 @@ final class ComposerDependencyAnalyserConfig 'php-parallel-lint/php-parallel-lint', 'phpdocumentor/shim', 'phpmetrics/phpmetrics', - 'phpro/grumphp-shim', 'pyrech/composer-changelogs', 'rector/swiss-knife', 'saggre/phpdocumentor-markdown', diff --git a/tests/Config/ComposerDependencyAnalyserConfigTest.php b/tests/Config/ComposerDependencyAnalyserConfigTest.php index 2fdbc40f96..b8c46bb13d 100644 --- a/tests/Config/ComposerDependencyAnalyserConfigTest.php +++ b/tests/Config/ComposerDependencyAnalyserConfigTest.php @@ -105,6 +105,10 @@ static function (Configuration $configuration) use (&$customizeWasCalled): void $configuration->getIgnoreList() ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'ergebnis/phpunit-agent-reporter') ); + self::assertFalse( + $configuration->getIgnoreList() + ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'phpro/grumphp-shim') + ); self::assertTrue( $configuration->getIgnoreList() ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'vendor/custom-package') diff --git a/tests/Console/Command/RuntimeCommandTest.php b/tests/Console/Command/RuntimeCommandTest.php index efe4d6c415..056bf28b0d 100644 --- a/tests/Console/Command/RuntimeCommandTest.php +++ b/tests/Console/Command/RuntimeCommandTest.php @@ -121,8 +121,8 @@ public function globalWorkingDirectoryWillSelectConsumerWithoutChangingRuntimePa self::assertSame($packageRoot . '/vendor/autoload.php', $metadata['autoload']); self::assertIsString($metadata['version']); } finally { - new Filesystem() - ->remove($directory); + $filesystem = new Filesystem(); + $filesystem->remove($directory); } } } diff --git a/tests/Distribution/ExternalExecutionTest.php b/tests/Distribution/ExternalExecutionTest.php index 40c461caa8..1158cf909f 100644 --- a/tests/Distribution/ExternalExecutionTest.php +++ b/tests/Distribution/ExternalExecutionTest.php @@ -183,6 +183,7 @@ private function runCli(array $arguments): Process '--no-interaction', ], $this->consumer, [ 'CI' => 'true', + 'COMPOSER_TESTS_ARE_RUNNING' => 'true', 'FAST_FORWARD_MIN_COVERAGE' => false, ]); $process->setTimeout(60); From e174f9ae1cc4779979840a88a679a77b6d832b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 23 Oct 2026 18:34:19 -0300 Subject: [PATCH 3/5] fix: keep workflow fixture cleanup compatible with PHP 8.3 --- tests/GitHubActions/ReportTemplateRuntimeTest.php | 4 ++-- tests/GitHubActions/SetupComposerActionTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/GitHubActions/ReportTemplateRuntimeTest.php b/tests/GitHubActions/ReportTemplateRuntimeTest.php index bd826b91bc..f7c8fd6180 100644 --- a/tests/GitHubActions/ReportTemplateRuntimeTest.php +++ b/tests/GitHubActions/ReportTemplateRuntimeTest.php @@ -53,8 +53,8 @@ protected function setUp(): void */ protected function tearDown(): void { - new Filesystem() - ->remove($this->workspace); + $filesystem = new Filesystem(); + $filesystem->remove($this->workspace); } /** diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php index 4ae3cb3684..8cf160c2a2 100644 --- a/tests/GitHubActions/SetupComposerActionTest.php +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -54,8 +54,8 @@ protected function setUp(): void */ protected function tearDown(): void { - new Filesystem() - ->remove($this->workspace); + $filesystem = new Filesystem(); + $filesystem->remove($this->workspace); } /** From 55341a86a10564c4c9ed32ac3de92542b8a63f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 24 Oct 2026 08:04:57 -0300 Subject: [PATCH 4/5] fix: run isolated GrumPHP hooks and consumer PHPDoc bootstrap --- .php-cs-fixer.dist.php | 4 + docs/commands/git-hooks.rst | 7 +- grumphp.yml | 2 +- rector.php | 10 +- resources/git-hooks/commit-msg | 2 +- resources/git-hooks/pre-commit | 2 +- src/Console/Command/PhpDocCommand.php | 3 +- src/GitIgnore/Merger.php | 4 +- .../Command/CopyResourceCommandTest.php | 69 +++++++--- tests/Console/Command/GitHooksCommandTest.php | 44 +++++-- tests/Console/Command/PhpDocCommandTest.php | 39 ++++-- tests/Console/Command/RefactorCommandTest.php | 37 ++++-- tests/Console/Command/ReportsCommandTest.php | 32 +++-- .../Console/Command/StandardsCommandTest.php | 18 ++- tests/Console/Command/SyncCommandTest.php | 31 ++++- tests/Console/Command/TestsCommandTest.php | 19 ++- tests/Distribution/PhpCsFixerConfigTest.php | 108 ++++++++++++++++ tests/GitAttributes/MergerTest.php | 8 +- tests/GitHooks/PackagedHooksTest.php | 59 ++++++++- .../PagesDeploymentActionTest.php | 121 +++++++++++++----- .../PendingTestStatusWorkflowTest.php | 71 +++++++--- tests/GitIgnore/GitIgnoreTest.php | 2 +- tests/GitIgnore/MergerTest.php | 4 +- .../ProjectCapabilitiesResolverTest.php | 15 ++- 24 files changed, 572 insertions(+), 139 deletions(-) create mode 100644 tests/Distribution/PhpCsFixerConfigTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index dc1783f75b..be475d7793 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -4,6 +4,10 @@ use FastForward\DevTools\Path\WorkingProjectPathResolver; +if (! class_exists(WorkingProjectPathResolver::class)) { + require_once __DIR__ . '/src/Path/WorkingProjectPathResolver.php'; +} + $rules = [ 'phpdoc_indent' => true, 'phpdoc_order' => [ diff --git a/docs/commands/git-hooks.rst b/docs/commands/git-hooks.rst index fac809479a..2e683e74da 100644 --- a/docs/commands/git-hooks.rst +++ b/docs/commands/git-hooks.rst @@ -100,8 +100,13 @@ Runtime and durable configuration --------------------------------- Hooks prefer ``vendor/bin/grumphp`` or ``vendor/bin/grumphp.phar``; otherwise -they run ``cpx --skip-local phpro/grumphp-shim:2.26.0``. CPX must be available +they run ``cpx --skip-local phpro/grumphp-shim:2.26.0 grumphp``. The explicit +binary name avoids an interactive choice between ``grumphp`` and ``grumphp.phar``. +CPX must be available when no local executable exists. GrumPHP is not a DevTools runtime dependency. Explicit ``dev-tools:sync`` copies a missing ``grumphp.yml`` into the consumer root. Local overrides remain authoritative; hooks do not persist temporary runtime/cache paths. + +The packaged GrumPHP configuration allows 600 seconds per process so its +``composer dev-tools`` task can complete the full coverage and reports pipeline. diff --git a/grumphp.yml b/grumphp.yml index bb90b63330..74026cccb8 100644 --- a/grumphp.yml +++ b/grumphp.yml @@ -1,7 +1,7 @@ grumphp: stop_on_failure: true ignore_unstaged_changes: false - process_timeout: 120 + process_timeout: 600 environment: variables: diff --git a/rector.php b/rector.php index 89836ba077..ad7141e5e0 100644 --- a/rector.php +++ b/rector.php @@ -17,6 +17,14 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ +use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; +use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector; use FastForward\DevTools\Config\RectorConfig; -return RectorConfig::configure(); +return RectorConfig::configure(static function (Rector\Config\RectorConfig $rectorConfig): void { + // Keep the legacy classifier constructor contract retained by the Git rule precedence fix. + $rectorConfig->skip([ + RemoveUnusedPromotedPropertyRector::class => [__DIR__ . '/src/GitIgnore/Merger.php'], + RemoveEmptyClassMethodRector::class => [__DIR__ . '/src/GitIgnore/Merger.php'], + ]); +}); diff --git a/resources/git-hooks/commit-msg b/resources/git-hooks/commit-msg index 99afc2b70b..dea2e36c3f 100755 --- a/resources/git-hooks/commit-msg +++ b/resources/git-hooks/commit-msg @@ -34,7 +34,7 @@ run_grumphp() { elif [ -x "vendor/bin/grumphp.phar" ]; then vendor/bin/grumphp.phar "$@" else - cpx --skip-local phpro/grumphp-shim:2.26.0 "$@" + cpx --skip-local phpro/grumphp-shim:2.26.0 grumphp "$@" fi } diff --git a/resources/git-hooks/pre-commit b/resources/git-hooks/pre-commit index 38209c12d5..b8a02433b7 100755 --- a/resources/git-hooks/pre-commit +++ b/resources/git-hooks/pre-commit @@ -30,7 +30,7 @@ run_grumphp() { elif [ -x "vendor/bin/grumphp.phar" ]; then vendor/bin/grumphp.phar "$@" else - cpx --skip-local phpro/grumphp-shim:2.26.0 "$@" + cpx --skip-local phpro/grumphp-shim:2.26.0 grumphp "$@" fi } diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 0bf05cf95b..d80658e319 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -196,7 +196,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ([] !== $paths) { - $processBuilder = $processBuilder->withArgument('--path-mode=intersection')->withArgument('--'); + $processBuilder = $processBuilder->withArgument('--path-mode=intersection') + ->withArgument('--'); foreach ($paths as $path) { $processBuilder = $processBuilder->withArgument($path); diff --git a/src/GitIgnore/Merger.php b/src/GitIgnore/Merger.php index 62ebd7e4e4..1f526bf27b 100644 --- a/src/GitIgnore/Merger.php +++ b/src/GitIgnore/Merger.php @@ -28,6 +28,8 @@ { /** * Retains the classifier constructor parameter for compatibility with consumers. + * + * @param ClassifierInterface $classifier */ public function __construct( private ClassifierInterface $classifier = new Classifier() @@ -44,7 +46,7 @@ public function merge(GitIgnoreInterface $canonical, GitIgnoreInterface $project $canonicalEntries = $this->patterns($canonical->entries()); $projectEntries = $this->patterns($project->entries()); - if (array_slice($projectEntries, 0, count($canonicalEntries)) === $canonicalEntries) { + if (\array_slice($projectEntries, 0, \count($canonicalEntries)) === $canonicalEntries) { return new GitIgnore($project->path(), $projectEntries); } diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index 70bd948451..ad1bceaf3b 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -48,6 +48,7 @@ use Symfony\Component\Finder\Finder; use function Safe\mkdir; +use function Safe\realpath; use function Safe\file_put_contents; use function Safe\unlink; use function Safe\rmdir; @@ -91,6 +92,7 @@ protected function setUp(): void { $this->sourceDirectory = sys_get_temp_dir() . '/copy-resource-command-test-' . bin2hex(random_bytes(4)); mkdir($this->sourceDirectory . '/nested', 0o777, true); + $this->sourceDirectory = realpath($this->sourceDirectory); file_put_contents($this->sourceDirectory . '/nested/example.yml', 'name: example'); $this->filesystem = $this->prophesize(FilesystemInterface::class); @@ -470,6 +472,13 @@ public function executeWillSkipReplacingDriftedFileWhenInteractiveConfirmationIs self::assertSame(CopyResourceCommand::SUCCESS, $this->executeCommand()); } + /** + * @param bool $directory + * @param bool $dryRun + * @param bool $check + * + * @return void + */ #[Test] #[DataProvider('missingTargetPreviewModes')] public function executeWillNotCreateMissingTargetsInPreviewModes(bool $directory, bool $dryRun, bool $check): void @@ -477,16 +486,26 @@ public function executeWillNotCreateMissingTargetsInPreviewModes(bool $directory $source = $directory ? $this->sourceDirectory : $this->sourceDirectory . '/nested/example.yml'; $target = $this->sourceDirectory . '/missing/output'; $destination = $directory ? $target . '/nested/example.yml' : $target; - $this->input->getOption('source')->willReturn($source); - $this->input->getOption('target')->willReturn($target); - $this->input->getOption('overwrite')->willReturn(true); - $this->input->getOption('interactive')->willReturn(true); - $this->input->getOption('dry-run')->willReturn($dryRun); - $this->input->getOption('check')->willReturn($check); - $this->fileLocator->locate($source)->willReturn($source); - $this->filesystem->getAbsolutePath($target)->willReturn($target); - $this->filesystem->exists($destination)->willReturn(false); - $this->finderFactory->create()->willReturn(new Finder()); + $this->input->getOption('source') + ->willReturn($source); + $this->input->getOption('target') + ->willReturn($target); + $this->input->getOption('overwrite') + ->willReturn(true); + $this->input->getOption('interactive') + ->willReturn(true); + $this->input->getOption('dry-run') + ->willReturn($dryRun); + $this->input->getOption('check') + ->willReturn($check); + $this->fileLocator->locate($source) + ->willReturn($source); + $this->filesystem->getAbsolutePath($target) + ->willReturn($target); + $this->filesystem->exists($destination) + ->willReturn(false); + $this->finderFactory->create() + ->willReturn(new Finder()); $this->filesystem->copy(Argument::cetera())->shouldNotBeCalled(); $this->filesystem->mkdir(Argument::cetera())->shouldNotBeCalled(); $this->filesystem->dumpFile(Argument::cetera())->shouldNotBeCalled(); @@ -498,6 +517,9 @@ public function executeWillNotCreateMissingTargetsInPreviewModes(bool $directory self::assertDirectoryDoesNotExist($this->sourceDirectory . '/missing'); } + /** + * @return void + */ #[Test] public function executeWillCheckAllFilesWithoutWritingWhenDirectoryContainsExistingAndMissingTargets(): void { @@ -505,15 +527,22 @@ public function executeWillCheckAllFilesWithoutWritingWhenDirectoryContainsExist file_put_contents($extraSource, 'second'); try { $target = $this->sourceDirectory . '/missing'; - $this->input->getOption('source')->willReturn($this->sourceDirectory); - $this->input->getOption('target')->willReturn($target); - $this->input->getOption('overwrite')->willReturn(false); - $this->input->getOption('check')->willReturn(true); - $this->fileLocator->locate($this->sourceDirectory)->willReturn($this->sourceDirectory); - $this->filesystem->getAbsolutePath($target)->willReturn($target); + $this->input->getOption('source') + ->willReturn($this->sourceDirectory); + $this->input->getOption('target') + ->willReturn($target); + $this->input->getOption('overwrite') + ->willReturn(false); + $this->input->getOption('check') + ->willReturn(true); + $this->fileLocator->locate($this->sourceDirectory) + ->willReturn($this->sourceDirectory); + $this->filesystem->getAbsolutePath($target) + ->willReturn($target); $this->filesystem->exists($target . '/nested/example.yml')->willReturn(true); $this->filesystem->exists($target . '/nested/second.yml')->willReturn(false); - $this->finderFactory->create()->willReturn(new Finder()); + $this->finderFactory->create() + ->willReturn(new Finder()); $this->fileDiffer->diff($this->sourceDirectory . '/nested/example.yml', $target . '/nested/example.yml') ->willReturn(new FileDiff(FileDiff::STATUS_UNCHANGED, 'Unchanged'))->shouldBeCalledOnce(); $this->filesystem->copy(Argument::cetera())->shouldNotBeCalled(); @@ -526,6 +555,9 @@ public function executeWillCheckAllFilesWithoutWritingWhenDirectoryContainsExist } } + /** + * @return iterable + */ public static function missingTargetPreviewModes(): iterable { foreach ([false, true] as $directory) { @@ -535,6 +567,9 @@ public static function missingTargetPreviewModes(): iterable } } + /** + * @return int + */ private function executeCommand(): int { $reflectionMethod = new ReflectionMethod($this->command, 'execute'); diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 2c58f55907..38fea2e7c5 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -509,23 +509,41 @@ public function executeWillReportInstallFailureWhenReplacementStillCannotBeWritt self::assertSame(GitHooksCommand::FAILURE, $this->executeCommand()); } + /** + * @param bool $rendered + * @param bool $dryRun + * @param bool $check + * + * @return void + */ #[Test] #[DataProvider('missingHookPreviewModes')] public function executeWillNotInstallMissingHooksInPreviewModes(bool $rendered, bool $dryRun, bool $check): void { $target = $this->sourceDirectory . '/missing/hooks'; - $this->input->getOption('source')->willReturn($this->sourceDirectory); - $this->input->getOption('target')->willReturn($target); - $this->input->getOption('no-overwrite')->willReturn(false); - $this->input->getOption('interactive')->willReturn(true); - $this->input->getOption('dry-run')->willReturn($dryRun); - $this->input->getOption('check')->willReturn($check); - $this->fileLocator->locate($this->sourceDirectory)->willReturn($this->sourceDirectory); - $this->filesystem->getAbsolutePath($target)->willReturn($target); + $this->input->getOption('source') + ->willReturn($this->sourceDirectory); + $this->input->getOption('target') + ->willReturn($target); + $this->input->getOption('no-overwrite') + ->willReturn(false); + $this->input->getOption('interactive') + ->willReturn(true); + $this->input->getOption('dry-run') + ->willReturn($dryRun); + $this->input->getOption('check') + ->willReturn($check); + $this->fileLocator->locate($this->sourceDirectory) + ->willReturn($this->sourceDirectory); + $this->filesystem->getAbsolutePath($target) + ->willReturn($target); $this->filesystem->exists($target . '/post-merge')->willReturn(false); $this->filesystem->readFile($this->sourceDirectory . '/post-merge') - ->willReturn($rendered ? '#!/bin/sh\n' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER : '#!/bin/sh'); - $this->finderFactory->create()->willReturn(new Finder()); + ->willReturn( + $rendered ? '#!/bin/sh\n' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER : '#!/bin/sh' + ); + $this->finderFactory->create() + ->willReturn(new Finder()); $this->filesystem->copy(Argument::cetera())->shouldNotBeCalled(); $this->filesystem->dumpFile(Argument::cetera())->shouldNotBeCalled(); $this->filesystem->chmod(Argument::cetera())->shouldNotBeCalled(); @@ -539,6 +557,9 @@ public function executeWillNotInstallMissingHooksInPreviewModes(bool $rendered, self::assertDirectoryDoesNotExist($this->sourceDirectory . '/missing'); } + /** + * @return iterable + */ public static function missingHookPreviewModes(): iterable { foreach ([false, true] as $rendered) { @@ -548,6 +569,9 @@ public static function missingHookPreviewModes(): iterable } } + /** + * @return int + */ private function executeCommand(): int { $reflectionMethod = new ReflectionMethod($this->command, 'execute'); diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index 518b0b1d08..e9b2d49245 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -123,7 +123,8 @@ protected function setUp(): void $this->process->setEnv(Argument::type('array')) ->willReturn($this->process->reveal()); - $this->input->getArgument('path')->willReturn([]); + $this->input->getArgument('path') + ->willReturn([]); $this->input->getOption('fix') ->willReturn(false); $this->input->getOption('cache') @@ -383,16 +384,23 @@ public function executeWillForwardSelectedPathsToBothTools(array $paths): void $this->renderer->reveal(), $this->clock->reveal(), ); - $this->input->getArgument('path')->willReturn($paths); - $this->input->getOption('fix')->willReturn(true); + $this->input->getArgument('path') + ->willReturn($paths); + $this->input->getOption('fix') + ->willReturn(true); foreach ($paths as $path) { - $this->filesystem->exists($path)->willReturn(true); + $this->filesystem->exists($path) + ->willReturn(true); } + $suffix = (new Process(['--', ...$paths]))->getCommandLine(); $queue->add( Argument::that(static fn(Process $process): bool => str_ends_with($process->getCommandLine(), $suffix) && str_contains($process->getCommandLine(), '--path-mode=intersection') - && str_contains($process->getCommandLine(), DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer')) + && str_contains( + $process->getCommandLine(), + DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer') + ) && ! str_contains($process->getCommandLine(), '--dry-run')), false, false, @@ -408,10 +416,19 @@ public function executeWillForwardSelectedPathsToBothTools(array $paths): void 'Adding Missing PHPDoc with Rector', )->shouldBeCalledOnce(); $this->filesystem->dumpFile(PhpDocCommand::FILENAME, 'docheader')->shouldBeCalledOnce(); - $queue->run($this->output->reveal())->willReturn(PhpDocCommand::SUCCESS); + $queue->run($this->output->reveal()) + ->willReturn(PhpDocCommand::SUCCESS); $this->logger->log('info', 'Checking and fixing PHPDocs...', Argument::type('array'))->shouldBeCalledOnce(); - $this->logger->log('info', 'Created .docheader from repository template.', Argument::type('array'))->shouldBeCalledOnce(); - $this->logger->log('info', 'PHPDoc checks completed successfully.', Argument::type('array'))->shouldBeCalledOnce(); + $this->logger->log( + 'info', + 'Created .docheader from repository template.', + Argument::type('array') + )->shouldBeCalledOnce(); + $this->logger->log( + 'info', + 'PHPDoc checks completed successfully.', + Argument::type('array') + )->shouldBeCalledOnce(); self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute()); } @@ -433,8 +450,10 @@ public static function selectedPaths(): iterable #[Test] public function executeWillRejectMissingPathsBeforeWritingOrStartingTools(): void { - $this->input->getArgument('path')->willReturn(['missing.php']); - $this->filesystem->exists('missing.php')->willReturn(false); + $this->input->getArgument('path') + ->willReturn(['missing.php']); + $this->filesystem->exists('missing.php') + ->willReturn(false); $this->filesystem->dumpFile(Argument::cetera())->shouldNotBeCalled(); $this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); diff --git a/tests/Console/Command/RefactorCommandTest.php b/tests/Console/Command/RefactorCommandTest.php index 25b25d1047..140e82c022 100644 --- a/tests/Console/Command/RefactorCommandTest.php +++ b/tests/Console/Command/RefactorCommandTest.php @@ -92,7 +92,8 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); - $this->input->getOption('config')->willReturn(RefactorCommand::CONFIG); + $this->input->getOption('config') + ->willReturn(RefactorCommand::CONFIG); $this->input->getOption('fix') ->willReturn(false); $this->input->getOption('progress') @@ -224,9 +225,11 @@ public function executeWillHonorExplicitConfigurationWithSpaces(): void $queue = $this->prophesize(ProcessQueueInterface::class); $this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled(); $this->command = new RefactorCommand($this->fileLocator->reveal(), new ProcessBuilder(), $queue->reveal()); - $this->input->getOption('config')->willReturn('custom rules/limited.php'); + $this->input->getOption('config') + ->willReturn('custom rules/limited.php'); $this->fileLocator->locate('custom rules/limited.php') - ->willReturn('/repo/custom rules/limited.php')->shouldBeCalledOnce(); + ->willReturn('/repo/custom rules/limited.php') + ->shouldBeCalledOnce(); $this->fileLocator->locate(RefactorCommand::CONFIG)->shouldNotBeCalled(); $expected = new Process([ DevToolsPathResolver::getPreferredToolBinaryPath('rector'), @@ -238,14 +241,25 @@ public function executeWillHonorExplicitConfigurationWithSpaces(): void '--dry-run', ]); $queue->add( - Argument::that(static fn(Process $process): bool => $expected->getCommandLine() === $process->getCommandLine()), + Argument::that( + static fn(Process $process): bool => $expected->getCommandLine() === $process->getCommandLine() + ), false, false, 'Refactoring Code with Rector', )->shouldBeCalledOnce(); - $queue->run($this->output->reveal())->willReturn(RefactorCommand::SUCCESS); - $this->logger->log('info', 'Running Rector for code refactoring...', Argument::type('array'))->shouldBeCalledOnce(); - $this->logger->log('info', 'Code refactoring checks completed successfully.', Argument::type('array'))->shouldBeCalledOnce(); + $queue->run($this->output->reveal()) + ->willReturn(RefactorCommand::SUCCESS); + $this->logger->log( + 'info', + 'Running Rector for code refactoring...', + Argument::type('array') + )->shouldBeCalledOnce(); + $this->logger->log( + 'info', + 'Code refactoring checks completed successfully.', + Argument::type('array') + )->shouldBeCalledOnce(); self::assertSame(RefactorCommand::SUCCESS, $this->executeCommand()); } @@ -256,13 +270,18 @@ public function executeWillHonorExplicitConfigurationWithSpaces(): void #[Test] public function executeWillRejectInvalidExplicitConfigurationWithoutDefaultFallback(): void { - $this->input->getOption('config')->willReturn('missing/config.php'); + $this->input->getOption('config') + ->willReturn('missing/config.php'); $this->fileLocator->locate('missing/config.php') ->willThrow(new InvalidArgumentException('Configuration not found: missing/config.php')); $this->fileLocator->locate(RefactorCommand::CONFIG)->shouldNotBeCalled(); $this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->log('info', 'Running Rector for code refactoring...', Argument::type('array'))->shouldBeCalledOnce(); + $this->logger->log( + 'info', + 'Running Rector for code refactoring...', + Argument::type('array') + )->shouldBeCalledOnce(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Configuration not found: missing/config.php'); diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 3030591410..cdb93f6e96 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -51,6 +51,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; + use function Safe\file_put_contents; use function Safe\mkdir; use function Safe\rmdir; @@ -274,20 +275,29 @@ public function executeWillGenerateCoverageForFilesOnlyPackage(): void $composer = $this->prophesize(ComposerJsonInterface::class); $filesystem = $this->prophesize(FilesystemInterface::class); foreach (['psr-4', 'psr-0', 'classmap'] as $autoloadType) { - $composer->getAutoload($autoloadType)->willReturn([]); + $composer->getAutoload($autoloadType) + ->willReturn([]); } - $composer->getAutoload('files')->willReturn(['functions.php']); - $filesystem->getAbsolutePath('functions.php')->willReturn($workspace . '/functions.php'); - $filesystem->exists('./tests')->willReturn(true); - $filesystem->exists('docs')->willReturn(false); - $filesystem->exists('.github/wiki')->willReturn(false); + + $composer->getAutoload('files') + ->willReturn(['functions.php']); + $filesystem->getAbsolutePath('functions.php') + ->willReturn($workspace . '/functions.php'); + $filesystem->exists('./tests') + ->willReturn(true); + $filesystem->exists('docs') + ->willReturn(false); + $filesystem->exists('.github/wiki') + ->willReturn(false); $resolver = new ProjectCapabilitiesResolver($composer->reveal(), $filesystem->reveal()); $capabilities = $resolver->resolve(); self::assertSame([], $capabilities->getApiDirectories()); self::assertTrue($capabilities->canRunTests()); - $this->projectCapabilitiesResolver->resolve()->willReturn($capabilities); + $this->projectCapabilitiesResolver->resolve() + ->willReturn($capabilities); $this->processBuilder->build([DevToolsPathResolver::getBinaryPath(), 'tests']) - ->willReturn($this->process->reveal())->shouldBeCalledOnce(); + ->willReturn($this->process->reveal()) + ->shouldBeCalledOnce(); $this->processQueue->add($this->process->reveal(), false, false, 'Generating Coverage Report') ->shouldBeCalledOnce(); $this->processQueue->add($this->process->reveal(), false, true, 'Generating API Docs Report') @@ -296,7 +306,11 @@ public function executeWillGenerateCoverageForFilesOnlyPackage(): void ->shouldBeCalledOnce(); $this->processQueue->run($this->output->reveal()) ->willReturn(ReportsCommand::SUCCESS)->shouldBeCalledOnce(); - $this->logger->log('info', 'Generating frontpage for Fast Forward documentation...', Argument::type('array')) + $this->logger->log( + 'info', + 'Generating frontpage for Fast Forward documentation...', + Argument::type('array') + ) ->shouldBeCalledOnce(); $this->logger->log('info', 'Documentation reports generated successfully.', Argument::type('array')) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index 75660ef7fa..5f3a29616b 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -149,7 +149,8 @@ public function executeWillReturnFailureWhenAnyCommandFails(int $exitCode, bool { $this->processQueue->add(Argument::type(Process::class), Argument::cetera()) ->shouldBeCalledTimes(4); - $this->input->getOption('json')->willReturn($json); + $this->input->getOption('json') + ->willReturn($json); $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn($exitCode) ->shouldBeCalledOnce(); @@ -159,6 +160,7 @@ public function executeWillReturnFailureWhenAnyCommandFails(int $exitCode, bool )) ->shouldBeCalled(); } + $this->logger->error( 'Code standards checks failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface @@ -251,11 +253,15 @@ public function executeWithNoCacheWillForwardNoCacheOnlyToCacheAwareNestedComman self::assertSame(StandardsCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ #[Test] public function executeWillFailForAnActualSubprocessWithExitCodeTwo(): void { $environment = $this->prophesize(RuntimeEnvironmentInterface::class); - $environment->isGithubActions()->willReturn(false); + $environment->isGithubActions() + ->willReturn(false); $capabilities = $this->prophesize(OutputCapabilityDetectorInterface::class); $capabilities->supportsAnsi(Argument::type(OutputInterface::class))->willReturn(false); $configurator = $this->prophesize(ProcessEnvironmentConfiguratorInterface::class); @@ -268,7 +274,7 @@ public function executeWillFailForAnActualSubprocessWithExitCodeTwo(): void $capabilities->reveal(), ); $this->processBuilder->build(Argument::any()) - ->will(static fn(): Process => new Process([PHP_BINARY, '-r', 'exit(2);'])); + ->will(static fn(): Process => new Process([\PHP_BINARY, '-r', 'exit(2);'])); $this->command = new StandardsCommand($this->processBuilder->reveal(), $queue); $this->logger->log( 'info', @@ -283,6 +289,9 @@ public function executeWillFailForAnActualSubprocessWithExitCodeTwo(): void self::assertSame(StandardsCommand::FAILURE, $this->invokeExecute()); } + /** + * @return iterable + */ public static function failureExitCodes(): iterable { foreach ([1, 2, 127, 143] as $exitCode) { @@ -291,6 +300,9 @@ public static function failureExitCodes(): iterable } } + /** + * @return int + */ private function invokeExecute(): int { return (new ReflectionMethod($this->command, 'execute')) diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index f57934aeea..cc68d7309b 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -193,18 +193,26 @@ public function executeWillPropagateJsonOutputFormatToSubCommands(): void self::assertSame(SyncCommand::SUCCESS, $this->executeCommand()); } + /** + * @param string $mode + * @param int $status + * + * @return void + */ #[Test] #[DataProvider('readOnlyModes')] public function executeWillForwardReadOnlyModeToEveryQueuedCommand(string $mode, int $status): void { - $this->input->getOption($mode)->willReturn(true); + $this->input->getOption($mode) + ->willReturn(true); $commandLines = []; $this->processQueue->add(Argument::type(Process::class), false, false, Argument::cetera()) ->will(static function (array $arguments) use (&$commandLines): void { $commandLines[] = $arguments[0]->getCommandLine(); - })->shouldBeCalledTimes(10); + })->shouldBeCalledTimes(11); $this->processQueue->add(Argument::type(Process::class), false, true, Argument::cetera())->shouldNotBeCalled(); - $this->processQueue->run($this->output->reveal())->willReturn($status); + $this->processQueue->run($this->output->reveal()) + ->willReturn($status); self::assertSame($status, $this->executeCommand()); foreach ($commandLines as $commandLine) { @@ -213,16 +221,29 @@ public function executeWillForwardReadOnlyModeToEveryQueuedCommand(string $mode, self::assertStringNotContainsString("'skills'", $commandLine); self::assertStringNotContainsString("'agents'", $commandLine); } - self::assertCount(3, array_filter($commandLines, static fn(string $line): bool => str_contains($line, "'copy-resource'"))); - self::assertCount(1, array_filter($commandLines, static fn(string $line): bool => str_contains($line, "'git-hooks'"))); + + self::assertCount( + 4, + array_filter($commandLines, static fn(string $line): bool => str_contains($line, "'copy-resource'")) + ); + self::assertCount( + 1, + array_filter($commandLines, static fn(string $line): bool => str_contains($line, "'git-hooks'")) + ); } + /** + * @return iterable + */ public static function readOnlyModes(): iterable { yield 'dry-run' => ['dry-run', SyncCommand::SUCCESS]; yield 'check' => ['check', SyncCommand::FAILURE]; } + /** + * @return int + */ private function executeCommand(): int { return (new ReflectionMethod($this->command, 'execute')) diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 87949c42f2..d99ee21381 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -592,13 +592,20 @@ public function executeWillPropagateTestFailureForFilesOnlyPackage(): void $composer = $this->prophesize(ComposerJsonInterface::class); $filesystem = $this->prophesize(FilesystemInterface::class); foreach (['psr-4', 'psr-0', 'classmap'] as $autoloadType) { - $composer->getAutoload($autoloadType)->willReturn([]); + $composer->getAutoload($autoloadType) + ->willReturn([]); } - $composer->getAutoload('files')->willReturn(['functions.php']); - $filesystem->getAbsolutePath('functions.php')->willReturn($workspace . '/functions.php'); - $filesystem->exists('./tests')->willReturn(true); - $filesystem->exists('docs')->willReturn(false); - $filesystem->exists('.github/wiki')->willReturn(false); + + $composer->getAutoload('files') + ->willReturn(['functions.php']); + $filesystem->getAbsolutePath('functions.php') + ->willReturn($workspace . '/functions.php'); + $filesystem->exists('./tests') + ->willReturn(true); + $filesystem->exists('docs') + ->willReturn(false); + $filesystem->exists('.github/wiki') + ->willReturn(false); $resolver = new ProjectCapabilitiesResolver($composer->reveal(), $filesystem->reveal()); $capabilities = $resolver->resolve(); self::assertSame([], $capabilities->getApiDirectories()); diff --git a/tests/Distribution/PhpCsFixerConfigTest.php b/tests/Distribution/PhpCsFixerConfigTest.php new file mode 100644 index 0000000000..107913f33f --- /dev/null +++ b/tests/Distribution/PhpCsFixerConfigTest.php @@ -0,0 +1,108 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Distribution; + +use FastForward\DevTools\Path\WorkingProjectPathResolver; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +use function Safe\json_decode; + +#[CoversNothing] +final class PhpCsFixerConfigTest extends TestCase +{ + /** + * @param string $runtimePath + * + * @return void + */ + #[Test] + #[DataProvider('runtimeLayouts')] + public function configWillLoadWithoutDevToolsAutoloadAndPreserveExclusions(string $runtimePath): void + { + $filesystem = new Filesystem(); + $workspace = sys_get_temp_dir() . '/fixer-config-' . bin2hex(random_bytes(8)); + $packageRoot = \dirname(__DIR__, 2); + $runtime = $workspace . '/' . $runtimePath; + $consumer = $workspace . '/consumer'; + + try { + $filesystem->mkdir($runtime . '/src/Path'); + $filesystem->copy($packageRoot . '/.php-cs-fixer.dist.php', $runtime . '/.php-cs-fixer.dist.php'); + $filesystem->copy( + $packageRoot . '/src/Path/WorkingProjectPathResolver.php', + $runtime . '/src/Path/WorkingProjectPathResolver.php', + ); + $filesystem->dumpFile($consumer . '/src/Example.php', 'dumpFile($consumer . '/' . $directory . '/Excluded.php', 'dumpFile($consumer . '/probe.php', <<<'PHP' + $paths) { + if (!str_starts_with($namespace, 'FastForward\\')) { + $loader->addPsr4($namespace, $paths); + } + } + $loader->register(); + if (class_exists(FastForward\DevTools\Path\WorkingProjectPathResolver::class)) { + throw new RuntimeException('The consumer must not autoload DevTools.'); + } + $config = require $argv[2]; + $files = []; + foreach ($config->getFinder() as $file) { + $files[] = $file->getRelativePathname(); + } + sort($files); + echo json_encode($files, JSON_THROW_ON_ERROR); + PHP); + $process = new Process([ + \PHP_BINARY, + $consumer . '/probe.php', + $packageRoot, + $runtime . '/.php-cs-fixer.dist.php', + ], $consumer); + $process->run(); + + self::assertSame(0, $process->getExitCode(), $process->getOutput() . $process->getErrorOutput()); + self::assertSame(['probe.php', 'src/Example.php'], json_decode($process->getOutput(), true)); + } finally { + $filesystem->remove($workspace); + } + } + + /** + * @return iterable + */ + public static function runtimeLayouts(): iterable + { + yield 'source checkout' => ['runtime']; + yield 'CPX vendor installation' => ['cpx/vendor/fast-forward/dev-tools']; + } +} diff --git a/tests/GitAttributes/MergerTest.php b/tests/GitAttributes/MergerTest.php index 0a5ec2dd11..142b2fe9fe 100644 --- a/tests/GitAttributes/MergerTest.php +++ b/tests/GitAttributes/MergerTest.php @@ -60,9 +60,13 @@ public static function orderedRules(): iterable { yield 'explicit unset exception' => ["*.txt export-ignore\nkeep.txt -export-ignore"]; yield 'explicit unspecified exception' => ["*.txt export-ignore\nkeep.txt !export-ignore"]; - yield 'repeated positive after exception' => ["*.txt export-ignore\nkeep.txt -export-ignore\n*.txt export-ignore"]; + yield 'repeated positive after exception' => [ + "*.txt export-ignore\nkeep.txt -export-ignore\n*.txt export-ignore", + ]; yield 'repeated custom attributes' => ["* text=auto\n*.txt -text\n* text=auto"]; - yield 'overlapping patterns' => ["docs/** export-ignore\ndocs/keep.* -export-ignore\ndocs/keep.txt export-ignore"]; + yield 'overlapping patterns' => [ + "docs/** export-ignore\ndocs/keep.* -export-ignore\ndocs/keep.txt export-ignore", + ]; yield 'comments and whitespace' => ["# comment\n/docs/ export-ignore\n/tests/ export-ignore"]; yield 'quoted path' => ['"docs with spaces/file.txt" export-ignore']; } diff --git a/tests/GitHooks/PackagedHooksTest.php b/tests/GitHooks/PackagedHooksTest.php index e35d0ea18b..c1a726b75e 100644 --- a/tests/GitHooks/PackagedHooksTest.php +++ b/tests/GitHooks/PackagedHooksTest.php @@ -41,6 +41,25 @@ #[CoversNothing] final class PackagedHooksTest extends TestCase { + private const array ISOLATED_GIT_ENVIRONMENT = [ + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => false, + 'GIT_CONFIG' => false, + 'GIT_CONFIG_PARAMETERS' => false, + 'GIT_CONFIG_COUNT' => false, + 'GIT_OBJECT_DIRECTORY' => false, + 'GIT_DIR' => false, + 'GIT_WORK_TREE' => false, + 'GIT_IMPLICIT_WORK_TREE' => false, + 'GIT_GRAFT_FILE' => false, + 'GIT_INDEX_FILE' => false, + 'GIT_NO_REPLACE_OBJECTS' => false, + 'GIT_REPLACE_REF_BASE' => false, + 'GIT_PREFIX' => false, + 'GIT_INTERNAL_SUPER_PREFIX' => false, + 'GIT_SHALLOW_FILE' => false, + 'GIT_COMMON_DIR' => false, + ]; + private string $project; private Filesystem $filesystem; @@ -173,13 +192,42 @@ public function packagedHookWillFailClearlyWhenNoRunnerIsAvailable(): void self::assertFileDoesNotExist($this->project . '/calls'); } + /** + * @return void + */ + #[Test] + public function fixtureProcessesWillIgnoreInheritedRepositoryAndIndexPaths(): void + { + $foreignIndex = $this->project . '/foreign-index'; + $foreignContents = "Synthetic index MUST remain unchanged.\n"; + file_put_contents($foreignIndex, $foreignContents); + $environment = [ + 'GIT_DIR' => $this->project . '/foreign.git', + 'GIT_WORK_TREE' => $this->project . '/foreign-work-tree', + 'GIT_COMMON_DIR' => $this->project . '/foreign-common', + 'GIT_INDEX_FILE' => $foreignIndex, + 'GIT_OBJECT_DIRECTORY' => $this->project . '/foreign-objects', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => $this->project . '/foreign-alternate-objects', + ]; + $this->writeRunner('bin/cpx'); + + self::assertSame($this->project, trim($this->git(['rev-parse', '--show-toplevel'], $environment))); + $process = $this->executeHook('pre-commit', 0, $environment); + + self::assertSame(0, $process->getExitCode(), $process->getErrorOutput()); + self::assertSame($this->gitDiff(), file_get_contents($this->project . '/stdin')); + self::assertSame($foreignContents, file_get_contents($foreignIndex)); + self::assertSame("called\n", file_get_contents($this->project . '/calls')); + } + /** * @param string $hook * @param int $status + * @param array $environment * * @return Process */ - private function executeHook(string $hook, int $status): Process + private function executeHook(string $hook, int $status, array $environment = []): Process { $contents = file_get_contents(\dirname(__DIR__, 2) . '/resources/git-hooks/' . $hook); file_put_contents($this->project . '/hook', str_replace( @@ -191,11 +239,13 @@ private function executeHook(string $hook, int $status): Process ['/bin/sh', $this->project . '/hook', $this->project . '/commit message.txt'], $this->project, [ + ...$environment, 'PATH' => $this->project . '/bin:/usr/bin:/bin', 'HOOK_CAPTURE_DIR' => $this->project, 'HOOK_EXIT_STATUS' => (string) $status, 'GIT_CONFIG_GLOBAL' => '/dev/null', 'GIT_CONFIG_NOSYSTEM' => '1', + ...self::ISOLATED_GIT_ENVIRONMENT, ], ); $process->run(); @@ -231,7 +281,7 @@ private function writeRunner(string $path): void */ private function expectedArguments(string $hook, bool $config, bool $cpx): string { - $arguments = $cpx ? ['--skip-local', 'phpro/grumphp-shim:2.26.0'] : []; + $arguments = $cpx ? ['--skip-local', 'phpro/grumphp-shim:2.26.0', 'grumphp'] : []; if ($config) { $arguments[] = '--config'; $arguments[] = './grumphp.yml'; @@ -276,12 +326,15 @@ private function gitDiff(): string /** * @param list $arguments + * @param array $environment */ - private function git(array $arguments): string + private function git(array $arguments, array $environment = []): string { $process = new Process(['git', ...$arguments], $this->project, [ + ...$environment, 'GIT_CONFIG_GLOBAL' => '/dev/null', 'GIT_CONFIG_NOSYSTEM' => '1', + ...self::ISOLATED_GIT_ENVIRONMENT, ]); $process->mustRun(); diff --git a/tests/GitHubActions/PagesDeploymentActionTest.php b/tests/GitHubActions/PagesDeploymentActionTest.php index 2c81156ec2..95a20c79b0 100644 --- a/tests/GitHubActions/PagesDeploymentActionTest.php +++ b/tests/GitHubActions/PagesDeploymentActionTest.php @@ -23,6 +23,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; + +use function Safe\proc_close; use function Safe\chmod; use function Safe\fclose; use function Safe\file_get_contents; @@ -36,7 +38,9 @@ #[CoversNothing] final class PagesDeploymentActionTest extends TestCase { - /** @return iterable */ + /** + * @return iterable + */ public static function responses(): iterable { yield 'late propagation' => ['404 404 404 404 404 404 200', 0, '12', '10', 0, 7]; @@ -53,10 +57,26 @@ public static function responses(): iterable yield 'unbounded delay' => ['200', 0, '12', '31', 1, 0]; } + /** + * @param string $responses + * @param int $curlExit + * @param string $attempts + * @param string $delay + * @param int $expectedExit + * @param int $expectedCalls + * + * @return void + */ #[Test] #[DataProvider('responses')] - public function deploymentCheckWillPreserveBoundedFailureSemantics(string $responses, int $curlExit, string $attempts, string $delay, int $expectedExit, int $expectedCalls): void - { + public function deploymentCheckWillPreserveBoundedFailureSemantics( + string $responses, + int $curlExit, + string $attempts, + string $delay, + int $expectedExit, + int $expectedCalls + ): void { [$exit, $output, $calls] = $this->runAction($responses, $curlExit, $attempts, $delay); self::assertSame($expectedExit, $exit, $output); self::assertCount($expectedCalls, $calls); @@ -66,6 +86,9 @@ public function deploymentCheckWillPreserveBoundedFailureSemantics(string $respo } } + /** + * @return void + */ #[Test] public function deploymentCheckWillVerifyEveryConfiguredPath(): void { @@ -75,15 +98,24 @@ public function deploymentCheckWillVerifyEveryConfiguredPath(): void self::assertStringContainsString('https://example.test/coverage/', $calls[1]); } + /** + * @return void + */ #[Test] public function deploymentChecksWillShareOneBudgetAcrossPaths(): void { [$exit, $output, $calls] = $this->runAction('404 200 404', 0, '30', '30', "/|Index\n/coverage/|Coverage"); self::assertSame(1, $exit); self::assertCount(7, $calls); - self::assertStringContainsString('::error title=Deployment failed::Coverage was not verified: deployment health checks exceeded the 180-second total budget.', $output); + self::assertStringContainsString( + '::error title=Deployment failed::Coverage was not verified: deployment health checks exceeded the 180-second total budget.', + $output + ); } + /** + * @return void + */ #[Test] public function finalTransferWillBeLimitedToTheRemainingBudget(): void { @@ -91,42 +123,64 @@ public function finalTransferWillBeLimitedToTheRemainingBudget(): void self::assertSame(1, $exit); self::assertCount(7, $calls); self::assertMatchesRegularExpression('/--max-time [1-6] /', $calls[6]); - self::assertStringContainsString('::error title=Deployment failed::Index was not verified: deployment health checks exceeded the 180-second total budget.', $output); + self::assertStringContainsString( + '::error title=Deployment failed::Index was not verified: deployment health checks exceeded the 180-second total budget.', + $output + ); } - /** @return array{int, string, list} */ - private function runAction(string $responses, int $curlExit, string $attempts, string $delay, string $checks = '/|Index'): array - { + /** + * @param string $responses + * @param int $curlExit + * @param string $attempts + * @param string $delay + * @param string $checks + * + * @return array{int, string, list} + */ + private function runAction( + string $responses, + int $curlExit, + string $attempts, + string $delay, + string $checks = '/|Index' + ): array { $directory = sys_get_temp_dir() . '/pages-check-' . bin2hex(random_bytes(8)); mkdir($directory); try { file_put_contents($directory . '/curl', <<<'BASH' -#!/usr/bin/env bash -set -eu -printf '%s\n' "$*" >> "$FIXTURE/calls" -count=$(wc -l < "$FIXTURE/calls") -read -r -a responses <<< "$RESPONSES" -index=$((count-1)) -if ((index >= ${#responses[@]})); then index=$((${#responses[@]}-1)); fi -printf '%s' "${responses[$index]}" -echo TOKEN_MARKER >&2 -exit "$CURL_EXIT" -BASH); + #!/usr/bin/env bash + set -eu + printf '%s\n' "$*" >> "$FIXTURE/calls" + count=$(wc -l < "$FIXTURE/calls") + read -r -a responses <<< "$RESPONSES" + index=$((count-1)) + if ((index >= ${#responses[@]})); then index=$((${#responses[@]}-1)); fi + printf '%s' "${responses[$index]}" + echo TOKEN_MARKER >&2 + exit "$CURL_EXIT" + BASH); chmod($directory . '/curl', 0o700); file_put_contents($directory . '/calls', ''); $script = __DIR__ . '/../../.github/actions/github-pages/verify-deployment/run.sh'; $command = 'sleep() { SECONDS=$((SECONDS + $1)); }; source ' . escapeshellarg($script); - $process = proc_open('bash -c ' . escapeshellarg($command), [['pipe', 'r'], ['pipe', 'w'], ['redirect', 1]], $pipes, null, array_merge(getenv(), [ - 'PATH' => $directory . ':' . getenv('PATH'), - 'FIXTURE' => $directory, - 'RESPONSES' => $responses, - 'CURL_EXIT' => (string) $curlExit, - 'INPUT_BASE_URL' => 'https://example.test', - 'INPUT_TITLE' => 'Deployment failed', - 'INPUT_CHECKS' => $checks, - 'INPUT_ATTEMPTS' => $attempts, - 'INPUT_DELAY' => $delay, - ])); + $process = proc_open( + 'bash -c ' . escapeshellarg($command), + [['pipe', 'r'], ['pipe', 'w'], ['redirect', 1]], + $pipes, + null, + array_merge(getenv(), [ + 'PATH' => $directory . ':' . getenv('PATH'), + 'FIXTURE' => $directory, + 'RESPONSES' => $responses, + 'CURL_EXIT' => (string) $curlExit, + 'INPUT_BASE_URL' => 'https://example.test', + 'INPUT_TITLE' => 'Deployment failed', + 'INPUT_CHECKS' => $checks, + 'INPUT_ATTEMPTS' => $attempts, + 'INPUT_DELAY' => $delay, + ]) + ); self::assertIsResource($process); fclose($pipes[0]); $output = stream_get_contents($pipes[1]); @@ -134,11 +188,14 @@ private function runAction(string $responses, int $curlExit, string $attempts, s $exit = proc_close($process); $calls = trim(file_get_contents($directory . '/calls')); - return [$exit, $output, $calls === '' ? [] : explode("\n", $calls)]; + return [$exit, $output, '' === $calls ? [] : explode("\n", $calls)]; } finally { foreach (['curl', 'calls'] as $file) { - if (is_file($directory . '/' . $file)) { unlink($directory . '/' . $file); } + if (is_file($directory . '/' . $file)) { + unlink($directory . '/' . $file); + } } + rmdir($directory); } } diff --git a/tests/GitHubActions/PendingTestStatusWorkflowTest.php b/tests/GitHubActions/PendingTestStatusWorkflowTest.php index 99ad67dce4..73bf5ae7fe 100644 --- a/tests/GitHubActions/PendingTestStatusWorkflowTest.php +++ b/tests/GitHubActions/PendingTestStatusWorkflowTest.php @@ -24,6 +24,8 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Symfony\Component\Yaml\Yaml; + +use function Safe\proc_close; use function Safe\fclose; use function Safe\fwrite; use function Safe\proc_open; @@ -37,15 +39,33 @@ final class PendingTestStatusWorkflowTest extends TestCase */ public static function invalidRuns(): iterable { - yield 'wrong ID' => [['id' => 8], 'Workflow run ID does not match the current run.']; - yield 'missing ID' => [['id' => null], 'Workflow run ID does not match the current run.']; - yield 'wrong repository' => [['repository' => ['full_name' => 'other/repo']], 'Workflow repository does not match the current repository.']; - yield 'missing repository' => [['repository' => null], 'Workflow repository does not match the current repository.']; - yield 'string attempt' => [['run_attempt' => '1'], 'Workflow run attempt is not an integer.']; - yield 'missing attempt' => [['run_attempt' => null], 'Workflow run attempt is not an integer.']; - yield 'different attempt' => [['run_attempt' => 2], 'Workflow run attempt does not match the current attempt.']; + yield 'wrong ID' => [[ + 'id' => 8, + ], 'Workflow run ID does not match the current run.']; + yield 'missing ID' => [[ + 'id' => null, + ], 'Workflow run ID does not match the current run.']; + yield 'wrong repository' => [[ + 'repository' => [ + 'full_name' => 'other/repo', + ], + ], 'Workflow repository does not match the current repository.']; + yield 'missing repository' => [[ + 'repository' => null, + ], 'Workflow repository does not match the current repository.']; + yield 'string attempt' => [[ + 'run_attempt' => '1', + ], 'Workflow run attempt is not an integer.']; + yield 'missing attempt' => [[ + 'run_attempt' => null, + ], 'Workflow run attempt is not an integer.']; + yield 'different attempt' => [[ + 'run_attempt' => 2, + ], 'Workflow run attempt does not match the current attempt.']; foreach (['queued', 'completed', 'cancelled', null, "untrusted\nTOKEN_MARKER"] as $status) { - yield 'status ' . ($status ?? 'missing') => [['status' => $status], 'Workflow run status is not in_progress.']; + yield 'status ' . ($status ?? 'missing') => [[ + 'status' => $status, + ], 'Workflow run status is not in_progress.']; } } @@ -64,6 +84,9 @@ public function pendingPublisherWillExplainEachRejectedRunWithoutWriting(array $ self::assertStringNotContainsString('TOKEN_MARKER', $output); } + /** + * @return void + */ #[Test] public function pendingPublisherWillKeepTheValidRunPublishingPath(): void { @@ -75,8 +98,8 @@ public function pendingPublisherWillKeepTheValidRunPublishingPath(): void $expected = [ '--method', 'POST', 'repos/example/repo/statuses/' . str_repeat('a', 40), '-f', 'state=pending', - '-f', "context=Run Tests ($version)", - '-f', "description=PHP $version matrix job is pending for attempt 1.", + '-f', "context=Run Tests ({$version})", + '-f', "description=PHP {$version} matrix job is pending for attempt 1.", '-f', 'target_url=https://github.com/example/repo/actions/runs/7', ]; self::assertStringContainsString(var_export($expected, true), $output); @@ -87,6 +110,7 @@ public function pendingPublisherWillKeepTheValidRunPublishingPath(): void * Execute the actual embedded program with only its GitHub transport substituted. * * @param array $overrides + * * @return array{int, string} */ private function executePublisher(array $overrides): array @@ -96,13 +120,16 @@ private function executePublisher(array $overrides): array self::assertIsString($shell); $program = substr($shell, strpos($shell, ' 7, - 'repository' => ['full_name' => 'example/repo'], + 'repository' => [ + 'full_name' => 'example/repo', + ], 'run_attempt' => 1, 'status' => 'in_progress', ], $overrides); @@ -110,14 +137,20 @@ private function executePublisher(array $overrides): array . 'if ($arguments[0] === "--method") { echo "STATUS_WRITE\\n" . var_export($arguments, true) . "\\n"; return []; }' . 'return ' . var_export($run, true) . '; }' . "\n"; $program = substr($program, 0, $start) . $transport . substr($program, $end); - $process = proc_open(escapeshellarg(PHP_BINARY), [['pipe', 'r'], ['pipe', 'w'], ['redirect', 1]], $pipes, null, array_merge(getenv(), [ - 'GITHUB_REPOSITORY' => 'example/repo', - 'GITHUB_RUN_ID' => '7', - 'GITHUB_RUN_ATTEMPT' => '1', - 'TARGET_SHA' => str_repeat('a', 40), - 'TARGET_URL' => 'https://github.com/example/repo/actions/runs/7', - 'TEST_MATRIX' => '{"php-version":["8.3","8.4","8.5"]}', - ])); + $process = proc_open( + escapeshellarg(\PHP_BINARY), + [['pipe', 'r'], ['pipe', 'w'], ['redirect', 1]], + $pipes, + null, + array_merge(getenv(), [ + 'GITHUB_REPOSITORY' => 'example/repo', + 'GITHUB_RUN_ID' => '7', + 'GITHUB_RUN_ATTEMPT' => '1', + 'TARGET_SHA' => str_repeat('a', 40), + 'TARGET_URL' => 'https://github.com/example/repo/actions/runs/7', + 'TEST_MATRIX' => '{"php-version":["8.3","8.4","8.5"]}', + ]) + ); self::assertIsResource($process); fwrite($pipes[0], $program); fclose($pipes[0]); diff --git a/tests/GitIgnore/GitIgnoreTest.php b/tests/GitIgnore/GitIgnoreTest.php index b27ef53db8..ea269f56bb 100644 --- a/tests/GitIgnore/GitIgnoreTest.php +++ b/tests/GitIgnore/GitIgnoreTest.php @@ -134,6 +134,7 @@ public function fromFileFiltersEmptyLines(): void unlink($tempFile); } } + /** * @return void */ @@ -150,5 +151,4 @@ public function fromFilePreservesSignificantPatternWhitespaceAndZero(): void unlink($tempFile); } } - } diff --git a/tests/GitIgnore/MergerTest.php b/tests/GitIgnore/MergerTest.php index 8f179e69c6..c3d35d775c 100644 --- a/tests/GitIgnore/MergerTest.php +++ b/tests/GitIgnore/MergerTest.php @@ -52,7 +52,9 @@ public static function orderedRules(): iterable yield 'negation' => [['*.log', '!important.log']]; yield 'repeated rule after exception' => [['*.log', '!important.log', '*.log']]; yield 'directory traversal exception' => [['build/*', '!build/keep/', 'build/keep/*', '!build/keep/file.txt']]; - yield 'significant whitespace and escaped markers' => [[' leading', 'trailing\\ ', '\\#literal', '\\!literal', '0']]; + yield 'significant whitespace and escaped markers' => [[ + ' leading', 'trailing\\ ', '\\#literal', '\\!literal', '0'], + ]; } /** diff --git a/tests/Project/ProjectCapabilitiesResolverTest.php b/tests/Project/ProjectCapabilitiesResolverTest.php index 1133b0fcf9..a38f1fef04 100644 --- a/tests/Project/ProjectCapabilitiesResolverTest.php +++ b/tests/Project/ProjectCapabilitiesResolverTest.php @@ -61,7 +61,8 @@ final class ProjectCapabilitiesResolverTest extends TestCase protected function setUp(): void { $this->composer = $this->prophesize(ComposerJsonInterface::class); - $this->composer->getAutoload('files')->willReturn([]); + $this->composer->getAutoload('files') + ->willReturn([]); $this->filesystem = new SymfonyFilesystem(); $this->workingDirectory = getcwd(); $this->workspace = sys_get_temp_dir() . '/dev-tools-project-capabilities-' . uniqid('', true); @@ -190,10 +191,12 @@ public function resolveWillDetectFilesOnlySourceWithoutInventingApiDirectories() file_put_contents($this->workspace . '/functions.php', "composer->getAutoload($type)->willReturn([]); + $this->composer->getAutoload($type) + ->willReturn([]); } - $this->composer->getAutoload('files')->willReturn(['missing.php', 'functions.php']); + $this->composer->getAutoload('files') + ->willReturn(['missing.php', 'functions.php']); $capabilities = $this->resolver->resolve(); @@ -214,10 +217,12 @@ public function resolveWillIgnoreMissingAndNonPhpAutoloadFiles(): void file_put_contents($this->workspace . '/README.md', 'Guide'); foreach (['psr-4', 'psr-0', 'classmap'] as $type) { - $this->composer->getAutoload($type)->willReturn([]); + $this->composer->getAutoload($type) + ->willReturn([]); } - $this->composer->getAutoload('files')->willReturn(['missing.php', 'README.md']); + $this->composer->getAutoload('files') + ->willReturn(['missing.php', 'README.md']); self::assertFalse($this->resolver->resolve()->canRunTests()); } From 4f3cf00517fb64adbaa26689b64431d27b40266d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 24 Oct 2026 08:50:14 -0300 Subject: [PATCH 5/5] chore: align automatic skill sources across Fast Forward packages --- .agents/skills/fast-forward-changelog/LICENSE | 21 --- .../skills/fast-forward-changelog/SKILL.md | 162 ------------------ .../examples/contribution.php | 45 ----- .../references/automation.md | 89 ---------- .../fast-forward-changelog/references/cli.md | 77 --------- .../references/php-api.md | 40 ----- .changelog/consistent-skill-sources.md | 10 ++ .gitignore | 1 + composer.json | 4 + docs/usage/syncing-packaged-skills.rst | 20 ++- skills.json | 8 +- tests/Distribution/SkillsDistributionTest.php | 1 - 12 files changed, 33 insertions(+), 445 deletions(-) delete mode 100644 .agents/skills/fast-forward-changelog/LICENSE delete mode 100644 .agents/skills/fast-forward-changelog/SKILL.md delete mode 100644 .agents/skills/fast-forward-changelog/examples/contribution.php delete mode 100644 .agents/skills/fast-forward-changelog/references/automation.md delete mode 100644 .agents/skills/fast-forward-changelog/references/cli.md delete mode 100644 .agents/skills/fast-forward-changelog/references/php-api.md create mode 100644 .changelog/consistent-skill-sources.md diff --git a/.agents/skills/fast-forward-changelog/LICENSE b/.agents/skills/fast-forward-changelog/LICENSE deleted file mode 100644 index f3209003f1..0000000000 --- a/.agents/skills/fast-forward-changelog/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Felipe Sayao Lobato Abreu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.agents/skills/fast-forward-changelog/SKILL.md b/.agents/skills/fast-forward-changelog/SKILL.md deleted file mode 100644 index e43f1c05a0..0000000000 --- a/.agents/skills/fast-forward-changelog/SKILL.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -name: fast-forward-changelog -description: Adopt Fast Forward Changelog's GitHub lifecycle, write and check independent fragments, review version PRs, and perform authorized release maintenance. Discover the selected release's documentation and CLI capabilities before choosing integration details. -license: MIT -compatibility: Requires access to the application repository and the selected release's documentation. Verify that release's runner or PHP requirements before execution. Local CLI examples use CPX; PHP integration uses the application's Composer installation. GitHub credentials are needed only for authorized remote operations. -metadata: - author: php-fast-forward - source: original - repository: https://github.com/php-fast-forward/changelog - api-baseline: Verify the selected published release; commands and integration details are release-specific. ---- - -# Fast Forward Changelog - -Ordinary contributions add independent `.changelog/*.md` fragments. GitHub -automation checks each contribution, maintains PR feedback and prepares a -reviewable version PR. That PR consolidates the accepted fragments into -`CHANGELOG.md` and deletes only the consumed files. Publication follows a -separate approval for the exact result; a green check or prepared PR is not that -approval. - -## Select the real contract - -1. Read application instructions, Git status and its existing changelog setup. - Use its reviewed version for an ordinary task; do not upgrade it incidentally. - If its ref is a moving channel, resolve the current commit and corresponding - published full tag before selecting a local runtime. `v0` is not Composer `0`. -2. For new adoption or a requested update, discover the latest published stable - release in the [official repository](https://github.com/php-fast-forward/changelog/releases). - Confirm the complete release tag and its commit, exclude drafts/prereleases, - and respect any explicitly selected maintenance line. For CPX or PHP use, - confirm the same version is available on - [Packagist](https://packagist.org/packages/fast-forward/changelog). -3. Read that tag's README and relevant documentation, not `main` or an old copy - of this skill. Before adding a workflow, inspect its - `resources/workflows/changelog.yml`, companion README and the reusable workflow - they reference. Copy the release's caller, review inputs/permissions and set - its product references to that verified full tag or immutable release commit; - do not retain an obsolete embedded pin blindly. After setting the final ref, - inspect the reusable workflow at that exact ref and confirm its inputs, - permissions and runner requirements before saving the application caller. - If that release offers a project `install` command, inspect its help and prefer - its guided preview for a missing integration. Explain the release branch, - publication default and optional skill before accepting writes. - The lifecycle defaults to `publish: true`; confirm this at the selected ref. - Its automatic route publishes a version tag and GitHub Release - after the reviewed managed PR is merged; it does not merge the PR. A - validation-only trial must choose `publish: false` explicitly. Installation must - not publish anything. Preserve an existing caller, its pin and settings; - migration or upgrades need their own task scope. -4. For local CLI work, set `CHANGELOG_VERSION` to the verified exact numeric - release, without the tag's `v`. Discover commands and inspect their arguments: - - ```sh - cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" list --raw - cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" help add - cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" help check - ``` - - Use [CPX's official setup](https://github.com/laravel/cpx) when installation is - authorized; do not pin the executor in these commands. `--skip-local` selects - its separate tool installation. CPX may handle `--help` or `--version` itself, - so use `help ` for changelog help. An existing reviewed project binary - may be used directly instead. Confirm prerequisites from the chosen release. - -Installing this skill installs instructions, independently of the runtime. If -the selected CLI offers skill installation/check/update operations, inspect -their help and use only the supported operation and requested scope. Otherwise -follow that release's skill-installation guide; do not invent a command. - -Native installation can record the source package version, commit and payload -integrity. Compare an installed copy with the selected CLI through its documented -check operation; do not confuse that local comparison with a latest-version -lookup. Preserve NPX or other installer ownership and its lockfile rather than -trying to adopt the copy or rewrite metadata into a merely similar JSON format. - -This procedure avoids a frozen API inventory. A breaking release can still -change the lifecycle or these entrypoints: when documentation and capabilities -disagree, resolve the mismatch before writes rather than guessing compatibility. - -## Write and check a contribution - -Read the change and describe its observable effect. For example, create one -unique `.changelog/preserve-markdown-spaces.md`: - -```markdown ---- -category: fixed -type: patch ---- - -Preserves meaningful Markdown spaces when parsing fragments. -``` - -Keep existing notes and Markdown intact. Use the selected release's fragment -schema. For the established category scheme, `added`, `changed` and `deprecated` -default to `minor`, `removed` to `major`, and `fixed`/`security` to `patch`. -Persist the effective type explicitly; a justified override may express a -compatible dependency change as `changed`/`patch`. Apply the application's -versioning policy deliberately, especially during `0.x` development. - -After help confirms these arguments, the CLI can create and validate that note -from the application directory instead of writing the example manually: - -```sh -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" add "Preserves meaningful Markdown spaces when parsing fragments." --category=fixed --no-interaction -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" check --no-interaction -``` - -Verify that exactly one intended fragment was created and central history stayed -unchanged. Do not run both authoring routes for the same note. Fix diagnostics -and rerun the check. Before a committed PR comparison, inspect `help check`, -fetch the intended baseline under existing authority, then use its supported -comparison option, for example: - -```sh -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" check --since=origin/main --no-interaction -``` - -That comparison requires committed contribution changes. A local uncommitted -fragment can pass local validation without satisfying the PR comparison. - -Optional PR/issue/author metadata must come from verified evidence. When the -origin PR is already known and supported, use `--pull-request=` or -`pull_request: ` after confirming its repository and exact head. Leave -unknown fields absent; do not invent a number or require a second fragment -commit after opening a PR. If the selected release resolves PR origins during -compilation, follow its documented policy consistently across preparation and -publication. No qualifying PR should become an invented commit or author link. - -Ordinary work preserves inherited fragments and published history. Do not add a -new `Unreleased` section. Editorial replacement, history maintenance and release -consolidation require their own task scope. - -## Verify the lifecycle - -For adoption, open a small pilot PR and inspect the exact head's required status, -sticky comment and independent application checks. Exercise a missing or -malformed note and any enabled bot route. Inspect the generated version PR: -planned version/impact, exact notes, central-history changes and only the accepted -fragment deletions. Review again after its head changes. The workflow does not -merge its own version PR. - -For the selected release, verify how committed history and ancestral tags define -the version baseline. The current lifecycle advances from the greatest stable -version in either source: tag `v1.7.0`, committed history through `1.7.1` and a -new patch produce `1.7.2`. Skipping publication does not block the next PR or -authorize retroactive publication of `1.7.1`. Preserve earlier history and review -the proposed next version; do not create missing tags as an incidental repair. - -Load only the reference needed for the task: - -- [Automation](references/automation.md): selected caller, optional settings, - credentials, maintenance lines and pilot verification. -- [CLI maintenance](references/cli.md): previews, consolidation, history, - interrupted transactions and separately authorized publication. -- [PHP integration](references/php-api.md): release-specific application - services and the bundled example, only when programmatic integration is needed. - -Retain authority already given, but do not infer commit, push, merge, installation -elsewhere or publication authority from an ordinary fragment task. Report changed -paths, executed checks and the verified version/PR/SHA relevant to the result. diff --git a/.agents/skills/fast-forward-changelog/examples/contribution.php b/.agents/skills/fast-forward-changelog/examples/contribution.php deleted file mode 100644 index 1fdd8de344..0000000000 --- a/.agents/skills/fast-forward-changelog/examples/contribution.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - */ - -use FastForward\Changelog\Container\ServiceProvider\ChangelogServiceProvider; -use FastForward\Changelog\Fragment\FragmentWriterInterface; -use FastForward\Changelog\Release\Factory\ReleaseOptionsFactoryInterface; -use FastForward\Changelog\Validation\CheckServiceInterface; - -use function FastForward\Container\container; - -$autoload = $argv[1] ?? ''; -$directory = realpath($argv[2] ?? ''); -if ('' === $autoload || !is_file($autoload) || !isset($argv[2]) || false === $directory || !is_dir($directory)) { - fwrite(STDERR, "Usage: php contribution.php /consumer/vendor/autoload.php /existing/project\n"); - exit(2); -} -require $autoload; - -$container = container(ChangelogServiceProvider::class); -$options = $container->get(ReleaseOptionsFactoryInterface::class)->create([ - 'workingDirectory' => $directory, - 'source' => 'tags', -]); -$path = $container->get(FragmentWriterInterface::class)->add( - $options, - 'Preserves meaningful Markdown spaces when parsing fragments.', - category: 'fixed', - name: 'php-api-example.md', -); -$expected = "---\ncategory: fixed\ntype: patch\n---\n\nPreserves meaningful Markdown spaces when parsing fragments.\n"; -if ($expected !== file_get_contents($path)) { - throw new RuntimeException('The fragment does not match the expected application output.'); -} -if (!$container->get(CheckServiceInterface::class)->check($options)->isValid()) { - throw new RuntimeException('The pending fragment inventory is invalid.'); -} -fwrite(STDOUT, "Contribution example PASS\n"); diff --git a/.agents/skills/fast-forward-changelog/references/automation.md b/.agents/skills/fast-forward-changelog/references/automation.md deleted file mode 100644 index 46fe275314..0000000000 --- a/.agents/skills/fast-forward-changelog/references/automation.md +++ /dev/null @@ -1,89 +0,0 @@ -# GitHub lifecycle adoption - -Load when installing or changing workflows, configuring credentials or runners, -selecting maintenance lines, or diagnosing feedback. Discover the latest stable -published release for new adoption, respecting an explicitly selected release -line. For ordinary work, retain the application's reviewed implementation. - -## Use the selected release's caller - -From the verified full tag, read `README.md`, -`resources/workflows/changelog.yml`, its companion README, and the reusable -workflow referenced by that caller. Consult that tag's relevant guides, including -`docs/reusable-lifecycle.md`, `docs/github-app.md` and `docs/automation-map.md` when -present. If paths moved, follow that release's README rather than guessing. -Do not combine a caller from one version with settings copied from `main`. - -If that release lists `install`, inspect its command help and installation guide -before choosing a route. Guided setup can ask for the release branch, publication -choice, advanced overrides and optional project skill, then preview a missing -caller. It must preserve an existing caller and refuse ambiguous or competing -automation. Use read-only preview first; noninteractive application requires its -documented confirmation option. For a new caller, select the published release -through that command's supported release option; its installer can pin `uses` -to the verified commit and retain the version as a readable comment. Confirm -both identities in the preview. Preserve the selected pin of an existing caller. -The workflow and bundled skill may have separate -provenance: a workflow ref does not change the executing CLI's instructions. - -For manual setup, copy the caller into the application's `.github/workflows/`, review its events, -inputs and effective permissions, then set its product references to the selected -published complete tag or verified immutable release commit. Review older pins -embedded in the copied caller. After replacing a pin, inspect the reusable -workflow at the final selected ref and verify its actual inputs, permissions and -runner requirements before saving the application caller. -GitHub `uses` references the executable reusable -workflow under `.github/workflows`; the resource caller is a copyable asset. -Internal `repository-*` workflows belong to the library, not the consumer. - -Explain the selected reusable's publication default before installing its caller. -The lifecycle defaults to `publish: true`; verify this at the final selected ref. -The automatic route publishes the exact version tag and GitHub Release after a -reviewed managed version PR is merged. The workflow does not merge the PR. -Setup itself does not publish anything. -Choose `publish: false` explicitly when the task is a validation-only trial; -do not silently carry that override into an installation meant to publish. -Choose base and managed branches, -fragment/history paths, historical sources and PR-reference policy consistently -across enabled operations. For parallel maintenance, select a separate managed -branch per release line and verify its actual planned version; a branch's name -does not enforce a SemVer range. Use individual workflows or Docker actions only -when their release-specific contracts suit the application's orchestration. - -Full tags identify concrete releases; an immutable commit pin also keeps execution -bound to the reviewed code if an upstream tag is changed. A moving major channel -is an explicit update policy and must be documented by the selected product release; -do not assume it exists. In particular `@v0` may cross breaking `0.x` minors and -does not mean Composer `^0.1`. Changing a workflow ref is a deliberate update. - -## Credentials and acceptance - -Use the selected workflow's documented token/App route and the application's -approved secret storage. Preserve the verified automation identity and narrow -permissions. Never check out untrusted PR code in a privileged custom job. -Token-generated events may behave differently from human/App events; consult -the selected release and GitHub's current rules before promising downstream CI. - -For an explicitly validation-only pilot, use `publish: false`. Verify a valid -fragment, missing or malformed notes, the exact PR head's required status and sticky-comment recovery, -any enabled bot operation, and the generated version PR. Check the actual runner -requirements and access when changing Linux/self-hosted selection. Test custom -policy changes relevant to the application rather than assuming a default-branch -green run proves its generated head. - -The version PR must preserve exact notes and consume only its accepted fragment -set, with no technical plan files or new `Unreleased` section. Head changes need -fresh review and checks. Successful preparation does not authorize merging or -publication. Retain existing publication authority for the verified result, -using the selected release's committed evidence and recovery procedure. - -## Consolidation without publication - -Confirm the selected release's version-baseline rule. The current lifecycle uses -the greatest stable version in committed source history or ancestral stable -tags. With tag `v1.7.0` and committed history through `1.7.1`, a new patch prepares -`1.7.2` while preserving the earlier history. An explicit `publish: false` -creates neither tag nor GitHub Release, but does not block subsequent version -PRs. Enabling publication for a later merge does not publish older consolidated -versions retroactively. Do not remove history or manufacture a missing tag merely -to make another PR possible. diff --git a/.agents/skills/fast-forward-changelog/references/cli.md b/.agents/skills/fast-forward-changelog/references/cli.md deleted file mode 100644 index 55eea75592..0000000000 --- a/.agents/skills/fast-forward-changelog/references/cli.md +++ /dev/null @@ -1,77 +0,0 @@ -# CLI maintenance - -Load for release previews, history maintenance, recovery or authorized -publication. First select the real release and inspect `list --raw` and -`help ` as described in the skill. Read `docs/cli-fragments.md` and the -relevant maintenance/publication guide from that complete release tag. Verify -that its PHP and Git requirements match the environment. Do not infer arguments -or current behavior from an older installed skill. - -## Execute the selected CLI - -Run from the application's directory. `CHANGELOG_VERSION` is the verified exact -numeric product version, not a range frozen into this skill: - -```sh -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" list --raw -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" help status -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" help version -``` - -CPX keeps tool dependencies separate from the application's Composer project; -the command still operates on application files. It is not a filesystem sandbox -or a PHP runtime. CPX's own update/help/version commands concern the executor. -An existing reviewed `vendor/bin/changelog` may run the same supported arguments -without changing the application's dependencies. - -When `list` offers instruction-management operations, inspect their help before -using install, check or update options. Use the requested project/global scope -and preserve local changes. Their existence is a discovered capability, not a -promise that every published changelog version contains them. Runtime updates, -instruction installation and host-session activation remain separate results. - -## Preview before consolidation - -For releases offering these arguments, inspect the read-only plan first: - -```sh -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" status --json --source=tags --no-interaction -cpx --skip-local "fast-forward/changelog:$CHANGELOG_VERSION" version --dry-run --source=tags --no-interaction -``` - -Review the baseline/current/next version, effective impact, accepted fragment -paths, notes and affected files. Choose the relevant maintenance branch and -historical source explicitly. A branch name alone does not establish a release -range. Check command help before treating an exit code as success or failure; -check modes may use a nonzero result for drift as well as errors. - -The normal GitHub lifecycle prepares the version PR. Applying a local version -transaction requires existing consolidation authority and the same reviewed -options as the preview. Preserve exact committed input bytes, custom-template -trust requirements and any PR-resolution setting required by that release. -Verify only central history and consumed-fragment deletions are changed; no -technical plan file or new `Unreleased` section belongs in the version PR. - -If interrupted, preserve the recovery journal and remaining files. Follow that -release's recovery procedure with the same inputs. Do not edit a journal, delete -newer notes or replace history merely to force a retry. - -## Additional maintenance and publication - -Backfill, format, notes extraction and custom templates are optional operations. -Discover their actual commands and read that release's guide before using them. -Keep published descriptions, unknown dates, existing references and pending -fragments intact as required by the operation. Executable PHP templates require -explicit trust. Preview mutations and review the resulting diff. - -Publication requires separate authority for an exact approved full commit SHA. -Read that release's publication guide and command help, preview when supported, -and supply the verified target explicitly. Use the application's approved -credential mechanism. Verify the approved version, notes, consumed fragments, -tag and source commit; PR text or a moving branch is not transaction evidence. -Preparation and publication must use the same policies that affect the output. - -Do not move conflicting concrete tags or overwrite divergent published notes. -After an uncertain response, inspect remote evidence and follow the selected -release's idempotent recovery route under the existing authority. Report what -was actually published or recovered separately from successful validation. diff --git a/.agents/skills/fast-forward-changelog/references/php-api.md b/.agents/skills/fast-forward-changelog/references/php-api.md deleted file mode 100644 index 2dc88e429d..0000000000 --- a/.agents/skills/fast-forward-changelog/references/php-api.md +++ /dev/null @@ -1,40 +0,0 @@ -# Optional PHP integration - -Load only when the application needs injected services instead of the CLI or -GitHub lifecycle. CPX executes a separate tool; it does not provide an application -autoloader. Inspect the application's Composer installation with -`composer show fast-forward/changelog`, then read the PHP integration/API guide, -manifest requirements and migration notes from that exact release tag. - -Use the installed namespaces and method signatures. Keep the application's -existing command loader, container precedence and secret mechanism. Expose only -the needed domain services and inject external boundaries in application tests. -Resolving a service or writing a fragment does not authorize commits or releases. - -## Bundled contribution example - -The complete [contribution program](../examples/contribution.php) illustrates the -`0.1.0` PHP API: explicit consumer autoloader and target directory, provider -composition, fragment creation and validation. It is an optional example, not -the contract for every later release. Before using it with another version, -compare its imports/signatures with that release's documented API or use the -example shipped with the selected release. - -For a matching API, run it first in a disposable target: - -```sh -php .agents/skills/fast-forward-changelog/examples/contribution.php ./vendor/autoload.php /absolute/project -vendor/bin/changelog check --no-interaction -``` - -Success prints `Contribution example PASS`; exactly one -`.changelog/php-api-example.md` is created, with no commit or central-history -change. A repeated run must expose the filename collision rather than overwrite -the file. Missing inputs and runtime failures remain nonzero. Do not report -success after a failed write or ignore the validation report. - -Keep PR/issue/author fields absent unless verified. Service access follows the -same fragment, version-transaction and publication boundaries as the selected -release's CLI. Learn optional configuration, planner/history services and -approved-commit publication from that version's documentation when the task -requires them; do not load an API inventory for an ordinary fragment task. diff --git a/.changelog/consistent-skill-sources.md b/.changelog/consistent-skill-sources.md new file mode 100644 index 0000000000..8a95a103ce --- /dev/null +++ b/.changelog/consistent-skill-sources.md @@ -0,0 +1,10 @@ +--- +category: changed +type: patch +--- + +Synchronize the bundled local skills automatically through development-only +LLM Skills and trust Fast Forward Composer skill donors through `fast-forward/*`. +Resolve Changelog contributor skills from stable `0.4.x` releases without a +redundant skill allowlist or a package override for the local directory source. +Restore ignored generated copies instead of tracking the external skill payload. diff --git a/.gitignore b/.gitignore index 5cd40fb58a..0c272d4732 100755 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ vendor/ *.cache .DS_Store composer.lock +/.agents/skills/ diff --git a/composer.json b/composer.json index 4e341a9eb0..3640779ee0 100644 --- a/composer.json +++ b/composer.json @@ -80,6 +80,9 @@ "thecodingmachine/safe": "^3.4", "twig/twig": "^3.24" }, + "require-dev": { + "llm/skills": "^1.15" + }, "suggest": { "llm/skills": "Synchronize bundled skills from Composer dependencies or explicit GitHub sources." }, @@ -101,6 +104,7 @@ "config": { "allow-plugins": { "ergebnis/composer-normalize": true, + "llm/skills": true, "phpdocumentor/shim": true, "phpro/grumphp-shim": true, "pyrech/composer-changelogs": true diff --git a/docs/usage/syncing-packaged-skills.rst b/docs/usage/syncing-packaged-skills.rst index 54b66df902..a3eec02324 100644 --- a/docs/usage/syncing-packaged-skills.rst +++ b/docs/usage/syncing-packaged-skills.rst @@ -59,22 +59,32 @@ payload rather than adding a second GitHub owner for the same skill: { "dependencies": { "composer": { - "trusted": ["fast-forward/config", "fast-forward/container"], + "trusted": ["fast-forward/*"], "trusted-replace": true } } } -This is an example for projects that really install those packages. Trust does +This trusts installed Composer donors in the ``fast-forward/*`` namespace. +``trusted-replace`` replaces built-in and implicit direct-dependency trust, so +packages outside that namespace stay untrusted. Trust does not install a missing library or fetch a GitHub replacement. A missing donor payload must fail review until a compatible donor or explicit source migration -is selected. ``trusted-replace`` permits a trusted package's canonical skill -replacement; review ownership changes and remove the prior source explicitly. +is selected. Review ownership changes and remove the prior source explicitly. Self development and extraction ------------------------------- -The DevTools checkout uses a ``dir`` source for its own ``resources/skills``. +The DevTools checkout uses a ``dir`` source for its own ``resources/skills``, +without a package-name override. The directory adapter derives its identity. +The Changelog GitHub source selects stable ``0.4.x`` releases through +``^0.4.0`` and discovers its skill through that package's Composer metadata, +without a redundant skill allowlist. Development-only ``llm/skills`` and its +specific plugin permission enable ``auto-sync: true`` after normal Composer +install/update operations. ``composer skills:update`` refreshes the configured +sources explicitly; ``--no-scripts`` suppresses automatic synchronization. +Generated ``.agents/skills`` copies remain ignored; canonical DevTools payloads +stay under ``resources/skills`` and repository role prompts remain local. Consumers use GitHub when DevTools is absent from their Composer graph. As code style, quality or another domain moves out, retain the ``fast-forward-`` skill name, publish its new owner first, diff --git a/skills.json b/skills.json index d32a2e4f21..47b322e320 100644 --- a/skills.json +++ b/skills.json @@ -8,7 +8,7 @@ "dependencies": { "composer": { "trusted": [ - "fast-forward/changelog" + "fast-forward/*" ], "trusted-replace": true } @@ -16,14 +16,12 @@ "sources": [ { "from": "dir", - "path": "resources/skills", - "package": "fast-forward/dev-tools" + "path": "resources/skills" }, { "from": "github", "package": "php-fast-forward/changelog", - "ref": "v0.3.0", - "skills": ["fast-forward-changelog"] + "ref": "^0.4.0" } ] } diff --git a/tests/Distribution/SkillsDistributionTest.php b/tests/Distribution/SkillsDistributionTest.php index 52bd0c10f5..e999adef85 100644 --- a/tests/Distribution/SkillsDistributionTest.php +++ b/tests/Distribution/SkillsDistributionTest.php @@ -84,7 +84,6 @@ public function selfConsumptionWillUseCanonicalDirectoryWithoutCacheAliases(): v self::assertContains([ 'from' => 'dir', 'path' => 'resources/skills', - 'package' => 'fast-forward/dev-tools', ], $config['sources']); } }