From 5528e5666ec4d59929f41f207596ae50406e98c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Thu, 30 Apr 2026 02:17:40 -0300 Subject: [PATCH 1/5] Support workflow consumers without local DevTools --- .../changelog/create-dependabot-entry/run.sh | 8 +- .../changelog/render-release-notes/run.sh | 2 +- .../actions/changelog/resolve-version/run.sh | 2 +- .../resolve-changelog.php | 2 +- .github/actions/php/setup-composer/action.yml | 69 ++++- .../detect-dev-tools-runtime.sh | 20 ++ .../setup-composer/dev-tools-runtime-lib.sh | 65 +++++ .../expose-dev-tools-runtime.sh | 36 +++ .github/workflows/auto-resolve-conflicts.yml | 2 - .github/workflows/changelog.yml | 10 +- .github/workflows/reports.yml | 4 +- .github/workflows/tests.yml | 8 +- .github/workflows/wiki-preview.yml | 4 +- CHANGELOG.md | 1 + .../branch-protection-and-bot-commits.rst | 3 +- docs/advanced/consumer-automation.rst | 7 +- docs/troubleshooting.rst | 8 + docs/usage/github-actions.rst | 23 +- .../GitHubActions/SetupComposerActionTest.php | 243 ++++++++++++++++++ 19 files changed, 463 insertions(+), 54 deletions(-) create mode 100755 .github/actions/php/setup-composer/detect-dev-tools-runtime.sh create mode 100755 .github/actions/php/setup-composer/dev-tools-runtime-lib.sh create mode 100755 .github/actions/php/setup-composer/expose-dev-tools-runtime.sh create mode 100644 tests/GitHubActions/SetupComposerActionTest.php diff --git a/.github/actions/changelog/create-dependabot-entry/run.sh b/.github/actions/changelog/create-dependabot-entry/run.sh index 393c038844..9a7795a712 100755 --- a/.github/actions/changelog/create-dependabot-entry/run.sh +++ b/.github/actions/changelog/create-dependabot-entry/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -entry_message="$(php -r 'require "vendor/autoload.php"; $resolver = new \FastForward\DevTools\Changelog\DependabotChangelogEntryMessageResolver(); echo $resolver->resolve(getenv("INPUT_PULL_REQUEST_TITLE") ?: "", (int) (getenv("INPUT_PULL_REQUEST_NUMBER") ?: 0));')" +entry_message="$(php -r 'require getenv("DEV_TOOLS_AUTOLOAD") ?: "vendor/autoload.php"; $resolver = new \FastForward\DevTools\Changelog\DependabotChangelogEntryMessageResolver(); echo $resolver->resolve(getenv("INPUT_PULL_REQUEST_TITLE") ?: "", (int) (getenv("INPUT_PULL_REQUEST_NUMBER") ?: 0));')" git fetch --no-tags --depth=1 origin "+refs/heads/${INPUT_BASE_REF}:refs/remotes/origin/${INPUT_BASE_REF}" git fetch --no-tags --depth=1 origin "+refs/heads/${INPUT_HEAD_REF}:refs/remotes/origin/${INPUT_HEAD_REF}" @@ -9,7 +9,7 @@ git switch -C "${INPUT_HEAD_REF}" "refs/remotes/origin/${INPUT_HEAD_REF}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" -if composer dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then +if dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then { echo "created=false" echo "status=already-present" @@ -19,7 +19,7 @@ if composer dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --agai exit 0 fi -composer dev-tools changelog:entry -- --type=changed --file="${INPUT_CHANGELOG_FILE}" "${entry_message}" +dev-tools changelog:entry -- --type=changed --file="${INPUT_CHANGELOG_FILE}" "${entry_message}" git add "${INPUT_CHANGELOG_FILE}" if git diff --cached --quiet -- "${INPUT_CHANGELOG_FILE}"; then @@ -35,7 +35,7 @@ fi git commit -m "Add changelog entry for Dependabot PR #${INPUT_PULL_REQUEST_NUMBER}" git push origin "HEAD:${INPUT_HEAD_REF}" -if ! composer dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then +if ! dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then { echo "created=false" echo "status=missing" diff --git a/.github/actions/changelog/render-release-notes/run.sh b/.github/actions/changelog/render-release-notes/run.sh index 07ac166d08..9a8dd57c51 100755 --- a/.github/actions/changelog/render-release-notes/run.sh +++ b/.github/actions/changelog/render-release-notes/run.sh @@ -2,4 +2,4 @@ set -euo pipefail mkdir -p "$(dirname "${INPUT_OUTPUT_FILE}")" -composer dev-tools changelog:show -- "${INPUT_VERSION}" --file="${INPUT_CHANGELOG_FILE}" > "${INPUT_OUTPUT_FILE}" +dev-tools changelog:show -- "${INPUT_VERSION}" --file="${INPUT_CHANGELOG_FILE}" > "${INPUT_OUTPUT_FILE}" diff --git a/.github/actions/changelog/resolve-version/run.sh b/.github/actions/changelog/resolve-version/run.sh index 0650e5e5d5..dac01a59c6 100755 --- a/.github/actions/changelog/resolve-version/run.sh +++ b/.github/actions/changelog/resolve-version/run.sh @@ -5,7 +5,7 @@ if [ -n "${INPUT_VERSION}" ]; then version="${INPUT_VERSION}" source="input" else - version="$(composer dev-tools changelog:next-version -- --file="${INPUT_CHANGELOG_FILE}")" + version="$(dev-tools changelog:next-version -- --file="${INPUT_CHANGELOG_FILE}")" source="inferred" fi diff --git a/.github/actions/github/resolve-predictable-conflicts/resolve-changelog.php b/.github/actions/github/resolve-predictable-conflicts/resolve-changelog.php index 7ae7af94b6..732c6f3fb2 100644 --- a/.github/actions/github/resolve-predictable-conflicts/resolve-changelog.php +++ b/.github/actions/github/resolve-predictable-conflicts/resolve-changelog.php @@ -6,7 +6,7 @@ use FastForward\DevTools\Changelog\Parser\ChangelogParser; use FastForward\DevTools\Changelog\Renderer\MarkdownRenderer; -$autoload = getenv('DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD') ?: getcwd() . '/vendor/autoload.php'; +$autoload = getenv('DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD') ?: getenv('DEV_TOOLS_AUTOLOAD') ?: getcwd() . '/vendor/autoload.php'; if (! is_file($autoload)) { fwrite(STDERR, sprintf("Composer autoload file not found: %s\n", $autoload)); diff --git a/.github/actions/php/setup-composer/action.yml b/.github/actions/php/setup-composer/action.yml index d9f6e514b3..5a9fe5ff3b 100644 --- a/.github/actions/php/setup-composer/action.yml +++ b/.github/actions/php/setup-composer/action.yml @@ -1,5 +1,5 @@ name: Setup PHP and Composer Dependencies -description: Setup PHP, warm Composer cache, mark safe directories, and install dependencies. +description: Setup PHP, install Composer dependencies when available, and expose a deterministic DevTools runtime. inputs: php-version: @@ -29,6 +29,21 @@ inputs: description: Additional newline-separated directories to mark as safe for git. 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. + required: false + default: .dev-tools-actions + +outputs: + dev-tools-binary: + description: Absolute path to the resolved DevTools binary. + value: ${{ steps.expose-dev-tools.outputs.binary }} + dev-tools-autoload: + 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`. + value: ${{ steps.expose-dev-tools.outputs.source }} runs: using: composite @@ -40,15 +55,6 @@ runs: extensions: ${{ inputs.extensions }} coverage: ${{ inputs.coverage }} - - name: Cache Composer dependencies - uses: actions/cache@v5 - with: - path: ${{ inputs.cache-dir }} - key: ${{ runner.os }}-composer-${{ inputs.php-version }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer-${{ inputs.php-version }}- - ${{ runner.os }}-composer- - - name: Mark workspace as safe for git shell: bash env: @@ -64,11 +70,48 @@ runs: done fi - - name: Install dependencies + - name: Detect consumer Composer manifest + id: consumer-composer shell: bash + run: | + if [ -f composer.json ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install consumer dependencies + if: steps.consumer-composer.outputs.present == 'true' + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 env: COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"} }' COMPOSER_CACHE_DIR: ${{ inputs.cache-dir }} COMPOSER_ROOT_VERSION: ${{ inputs.root-version }} - INPUT_INSTALL_OPTIONS: ${{ inputs.install-options }} - run: composer install ${INPUT_INSTALL_OPTIONS} + with: + composer-options: ${{ inputs.install-options }} + + - name: Resolve DevTools runtime source + id: resolve-dev-tools + shell: bash + env: + 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: 'true' + 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 }} + 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 new file mode 100755 index 0000000000..13666f1e31 --- /dev/null +++ b/.github/actions/php/setup-composer/detect-dev-tools-runtime.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +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 new file mode 100755 index 0000000000..88ca1ecf9b --- /dev/null +++ b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh @@ -0,0 +1,65 @@ +#!/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#./}" +} + +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_BINARY="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/bin/dev-tools" + DEV_TOOLS_LOCAL_AUTOLOAD="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/autoload.php" + + if [ -x "${DEV_TOOLS_LOCAL_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ]; then + DEV_TOOLS_RUNTIME_SOURCE='local' + DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_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 + fi + + if [ ! -f "${DEV_TOOLS_SOURCE_DIRECTORY}/composer.json" ]; then + echo "The DevTools workflow source directory does not contain composer.json: ${DEV_TOOLS_SOURCE_DIRECTORY}" >&2 + echo "Checkout the full php-fast-forward/dev-tools source into ${source_directory_input} before using this action." >&2 + + return 1 + fi + + DEV_TOOLS_RUNTIME_SOURCE='workflow' + DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_SOURCE_DIRECTORY}/vendor/bin/dev-tools" + DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_SOURCE_DIRECTORY}/vendor/autoload.php" +} + +runtime_requires_workflow_fallback() { + [ "${DEV_TOOLS_RUNTIME_SOURCE}" = 'workflow' ] +} + +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 new file mode 100755 index 0000000000..164e26c405 --- /dev/null +++ b/.github/actions/php/setup-composer/expose-dev-tools-runtime.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(dirname "$0")/dev-tools-runtime-lib.sh" + +resolve_dev_tools_runtime +ensure_resolved_runtime_is_available + +runtime_directory="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/dev-tools-runtime/bin" +wrapper_path="${runtime_directory}/dev-tools" + +mkdir -p "${runtime_directory}" + +{ + printf '#!/usr/bin/env bash\n' + printf 'set -euo pipefail\n' + printf 'exec %q "$@"\n' "${DEV_TOOLS_RUNTIME_BINARY}" +} > "${wrapper_path}" + +chmod +x "${wrapper_path}" + +{ + printf 'DEV_TOOLS_BINARY=%s\n' "${DEV_TOOLS_RUNTIME_BINARY}" + printf 'DEV_TOOLS_AUTOLOAD=%s\n' "${DEV_TOOLS_RUNTIME_AUTOLOAD}" + printf 'DEV_TOOLS_AUTO_RESOLVE_AUTOLOAD=%s\n' "${DEV_TOOLS_RUNTIME_AUTOLOAD}" + printf 'DEV_TOOLS_RUNTIME_SOURCE=%s\n' "${DEV_TOOLS_RUNTIME_SOURCE}" +} >> "${GITHUB_ENV}" + +printf '%s\n' "${runtime_directory}" >> "${GITHUB_PATH}" + +{ + printf 'binary=%s\n' "${DEV_TOOLS_RUNTIME_BINARY}" + printf 'autoload=%s\n' "${DEV_TOOLS_RUNTIME_AUTOLOAD}" + printf 'source=%s\n' "${DEV_TOOLS_RUNTIME_SOURCE}" + printf 'command=%s\n' "${wrapper_path}" +} >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/auto-resolve-conflicts.yml b/.github/workflows/auto-resolve-conflicts.yml index 2a4148037e..781af55017 100644 --- a/.github/workflows/auto-resolve-conflicts.yml +++ b/.github/workflows/auto-resolve-conflicts.yml @@ -63,8 +63,6 @@ jobs: repository: php-fast-forward/dev-tools ref: ${{ github.repository == 'php-fast-forward/dev-tools' && github.sha || 'main' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 78fa79a589..d4d2f42a57 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -108,8 +108,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -133,7 +131,7 @@ jobs: pull-request-title: ${{ env.PULL_REQUEST_TITLE }} - name: Verify changelog update - run: composer dev-tools changelog:check -- --file="${CHANGELOG_FILE}" --against="origin/${BASE_REF}" + run: dev-tools changelog:check -- --file="${CHANGELOG_FILE}" --against="origin/${BASE_REF}" - uses: ./.dev-tools-actions/.github/actions/summary/write with: @@ -199,8 +197,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -221,7 +217,7 @@ jobs: RELEASE_VERSION: ${{ steps.version.outputs.value }} run: | release_date="$(date -u +%F)" - composer dev-tools changelog:promote -- "${RELEASE_VERSION}" --file="${CHANGELOG_FILE}" --date="${release_date}" + dev-tools changelog:promote -- "${RELEASE_VERSION}" --file="${CHANGELOG_FILE}" --date="${release_date}" - name: Render release notes preview uses: ./.dev-tools-actions/.github/actions/changelog/render-release-notes @@ -308,8 +304,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 52ce53fd7e..d71c1886c3 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -77,8 +77,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -91,7 +89,7 @@ jobs: - name: Generate reports env: COMPOSER_ROOT_VERSION: ${{ env.REPORTS_ROOT_VERSION }} - run: composer dev-tools reports -- --target="${REPORTS_TARGET}" --coverage="${REPORTS_TARGET}/coverage" --metrics="${REPORTS_TARGET}/metrics" + run: dev-tools reports -- --target="${REPORTS_TARGET}" --coverage="${REPORTS_TARGET}/coverage" --metrics="${REPORTS_TARGET}/metrics" - name: Fix permissions run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39ba8f40ea..e116cd2663 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,8 +92,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -118,7 +116,7 @@ jobs: - name: Run PHPUnit tests env: COMPOSER_ROOT_VERSION: ${{ env.TESTS_ROOT_VERSION }} - run: composer dev-tools tests -- --coverage=.dev-tools/coverage --min-coverage=${{ steps.minimum-coverage.outputs.value }} + run: dev-tools tests -- --coverage=.dev-tools/coverage --min-coverage=${{ steps.minimum-coverage.outputs.value }} - name: Publish required test status if: ${{ always() && inputs.publish-required-statuses }} @@ -159,8 +157,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -172,7 +168,7 @@ jobs: - name: Run dependency health check env: COMPOSER_ROOT_VERSION: ${{ env.TESTS_ROOT_VERSION }} - run: composer dev-tools dependencies -- --max-outdated=${{ inputs.max-outdated || -1 }} + run: dev-tools dependencies -- --max-outdated=${{ inputs.max-outdated || -1 }} summarize: if: ${{ always() }} diff --git a/.github/workflows/wiki-preview.yml b/.github/workflows/wiki-preview.yml index 49a71aff7d..2241501086 100644 --- a/.github/workflows/wiki-preview.yml +++ b/.github/workflows/wiki-preview.yml @@ -60,8 +60,6 @@ jobs: 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' }} path: .dev-tools-actions - sparse-checkout: | - .github/actions - name: Setup PHP and install dependencies uses: ./.dev-tools-actions/.github/actions/php/setup-composer @@ -79,7 +77,7 @@ jobs: - name: Create Docs Markdown env: COMPOSER_ROOT_VERSION: dev-${{ github.event.pull_request.head.ref }} - run: composer dev-tools wiki -- --target=.github/wiki + run: dev-tools wiki -- --target=.github/wiki - name: Commit & push wiki preview branch id: wiki_commit diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af6c6449a..2e4c77158d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Render managed GrumPHP hook fallback paths relative to the consumer project so global `dev-tools:sync` installs keep local GrumPHP hook execution working (#305) +- Let reusable GitHub Actions workflows prefer a consumer-local `vendor/bin/dev-tools` and otherwise expose a deterministic runtime from the checked-out workflow source, so workflow-only consumers no longer need a direct DevTools dependency (#303) ## [1.24.4] - 2026-04-30 diff --git a/docs/advanced/branch-protection-and-bot-commits.rst b/docs/advanced/branch-protection-and-bot-commits.rst index 5653510dd9..767132818f 100644 --- a/docs/advanced/branch-protection-and-bot-commits.rst +++ b/docs/advanced/branch-protection-and-bot-commits.rst @@ -30,7 +30,8 @@ Instead, each pull request receives a dedicated wiki branch. For pull request ``123`` the workflow: -1. runs ``composer dev-tools wiki -- --target=.github/wiki``; +1. runs ``dev-tools wiki -- --target=.github/wiki`` through the shared + workflow runtime bootstrap; 2. commits the generated wiki content to the wiki branch ``pr-123``; 3. updates the parent repository submodule pointer at ``.github/wiki``; 4. commits that pointer update back to the pull request branch. diff --git a/docs/advanced/consumer-automation.rst b/docs/advanced/consumer-automation.rst index 29190534be..06cf7488fe 100644 --- a/docs/advanced/consumer-automation.rst +++ b/docs/advanced/consumer-automation.rst @@ -29,8 +29,8 @@ implementation in this repository is increasingly composed from local actions in - Small consumer stubs that call the reusable workflows through ``workflow_call``. * - ``.github/actions/php/*`` - - Shared PHP helpers such as workflow PHP-version resolution and Composer - setup. + - Shared PHP helpers such as workflow PHP-version resolution, Composer + setup, and local-vs-workflow DevTools runtime bootstrap. * - ``.github/actions/changelog/*`` - Changelog-specific building blocks for release version resolution, release-notes rendering, and GitHub release publication. @@ -70,7 +70,8 @@ implementation in this repository is increasingly composed from local actions in How GitHub Pages Publishing Works --------------------------------- -- ``.github/workflows/reports.yml`` runs ``composer dev-tools reports``. +- ``.github/workflows/reports.yml`` runs ``dev-tools reports`` through the + shared workflow bootstrap. - The workflow delegates repeated GitHub Pages tasks to ``.github/actions/github-pages/*`` instead of keeping that shell logic inline. - Pull requests publish previews under ``previews/pr-/``. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 4dd33ab0fa..3b47c39afd 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -67,6 +67,14 @@ 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. + Branch Protection or Bot Commit Blocks -------------------------------------- diff --git a/docs/usage/github-actions.rst b/docs/usage/github-actions.rst index 0ed8797583..ad6a2d9989 100644 --- a/docs/usage/github-actions.rst +++ b/docs/usage/github-actions.rst @@ -22,10 +22,11 @@ The automation model now has three layers: ``php-fast-forward/dev-tools``. * **Workflow action source checkout** inside the reusable workflows when they need local action implementations from ``.github/actions/``. The reusable - workflow performs a sparse checkout of that directory into a dedicated - ``.dev-tools-actions`` workspace path, which keeps the consumer - repository thin while still letting the reusable workflow resolve action - paths from the upstream ``php-fast-forward/dev-tools`` repository. + 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. Wrapper Workflows ----------------- @@ -59,6 +60,11 @@ The packaged wrappers currently include: For the protected-branch-safe preview and publish model, see :doc:`../advanced/branch-protection-and-bot-commits`. +Workflow-only consumers do not need to declare ``fast-forward/dev-tools`` as a +local 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. + Fast Forward Reports -------------------- @@ -106,7 +112,8 @@ wrappers: * **Pull Request Preview**: ``wiki.yml`` updates a dedicated preview branch in the wiki repository named ``pr-{number}``. * **Preview Generation**: The preview workflow resolves the PHP version, - installs dependencies, runs ``composer dev-tools wiki -- --target=.github/wiki``, + installs dependencies, exposes the shared ``dev-tools`` runtime, runs + ``dev-tools wiki -- --target=.github/wiki``, commits the generated Markdown into the wiki submodule, and then updates the parent repository's submodule pointer when needed. * **Preview Summary**: The preview workflow appends the preview branch name @@ -165,7 +172,7 @@ wrapper in ``resources/github-actions/changelog.yml``. * For same-repository Dependabot pull requests, creates and pushes a minimal ``Unreleased`` changelog entry derived from the pull request title when the branch has not added one yet. - * Runs ``composer dev-tools changelog:check -- --against=`` against the base ref. + * Runs ``dev-tools changelog:check -- --against=`` against the base ref. * Fails when a normal non-release branch does not add a meaningful ``Unreleased`` change. * Skips the validation job for pull requests whose head branch matches the configured ``release-branch-prefix``, because release-preparation branches intentionally leave ``Unreleased`` empty after promotion. * Publishes the aggregate changelog check for every active pull request. @@ -181,7 +188,7 @@ wrapper in ``resources/github-actions/changelog.yml``. * Resolves the next version from ``Unreleased`` unless a version input is provided. * Promotes ``Unreleased`` into the selected version with the current UTC release date. * Writes a release-notes preview file to ``.dev-tools/release-notes.md`` with - ``composer dev-tools changelog:show -- ``. + ``dev-tools changelog:show -- ``. * Opens or updates a release-preparation pull request instead of committing directly to ``main``. * Dispatches ``tests.yml`` for the release branch with required-status mirroring enabled, because release branches are written by the workflow @@ -192,7 +199,7 @@ wrapper in ``resources/github-actions/changelog.yml``. to create pull requests and dispatch workflows. * **Merged Release Pull Requests**: * Detects merged branches that match the configured release branch prefix. - * Renders the released changelog section with ``composer dev-tools changelog:show -- ``. + * Renders the released changelog section with ``dev-tools changelog:show -- ``. * Creates or updates the Git tag and GitHub release with the rendered changelog section as the release body. * Appends a run summary with the published tag and release URL. * Does **not** run for ordinary feature or fix pull requests merged into ``main``. diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php new file mode 100644 index 0000000000..cd46487f53 --- /dev/null +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -0,0 +1,243 @@ + + * @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 FilesystemIterator; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use RecursiveDirectoryIterator; +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; +use function Safe\mkdir; +use function Safe\realpath; +use function Safe\rmdir; +use function Safe\unlink; + +#[CoversNothing] +final class SetupComposerActionTest extends TestCase +{ + private const string ACTION_PATH = __DIR__ . '/../../.github/actions/php/setup-composer'; + + private string $workspace; + + /** + * @return void + */ + protected function setUp(): void + { + $this->workspace = sys_get_temp_dir() . '/setup-composer-action-test-' . bin2hex(random_bytes(4)); + mkdir($this->workspace, 0o777, true); + } + + /** + * @return 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); + } + + /** + * @return void + */ + #[Test] + public function detectRuntimeWillPreferTheConsumerLocalInstallation(): void + { + $this->createRuntimeFiles($this->workspace . '/vendor'); + $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 . '/vendor/bin/dev-tools', $outputs['binary']); + self::assertSame($resolvedWorkspace . '/vendor/autoload.php', $outputs['autoload']); + } + + /** + * @return void + */ + #[Test] + public function detectRuntimeWillFallbackToTheWorkflowSourceWhenTheConsumerDoesNotInstallDevTools(): void + { + mkdir($this->workspace . '/.dev-tools-actions', 0o777, true); + file_put_contents($this->workspace . '/.dev-tools-actions/composer.json', "{}\n"); + $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/vendor/bin/dev-tools', $outputs['binary']); + self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); + } + + /** + * @return void + */ + #[Test] + public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWorkflowFallback(): void + { + mkdir($this->workspace . '/.dev-tools-actions', 0o777, true); + file_put_contents($this->workspace . '/.dev-tools-actions/composer.json', "{}\n"); + $this->createRuntimeFiles($this->workspace . '/.dev-tools-actions/vendor'); + $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/vendor/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()); + } + + /** + * @param string $runtimeVendorDirectory + * + * @return void + */ + private function createRuntimeFiles(string $runtimeVendorDirectory): void + { + mkdir($runtimeVendorDirectory . '/bin', 0o777, true); + file_put_contents( + $runtimeVendorDirectory . '/bin/dev-tools', + "#!/usr/bin/env bash\nprintf 'dev-tools:%s\\n' \"\$*\"\n", + ); + chmod($runtimeVendorDirectory . '/bin/dev-tools', 0o755); + file_put_contents($runtimeVendorDirectory . '/autoload.php', "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 + */ + private function runActionScript(string $script, array $environment = []): void + { + $process = new Process( + ['bash', self::ACTION_PATH . '/' . $script], + $this->workspace, + $environment + [ + 'INPUT_DEV_TOOLS_SOURCE_DIRECTORY' => '.dev-tools-actions', + ], + ); + + $process->mustRun(); + } + + /** + * @param string $path + * + * @return array + */ + private function parseKeyValueFile(string $path): array + { + if (! is_file($path)) { + return []; + } + + $entries = []; + + foreach (array_filter(explode("\n", trim(file_get_contents($path)))) as $line) { + [$key, $value] = explode('=', $line, 2); + $entries[$key] = $value; + } + + return $entries; + } +} From b9702f63aab4e2247ed298b28be02f443929ed08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Thu, 30 Apr 2026 02:28:09 -0300 Subject: [PATCH 2/5] Fix workflow runtime resolution for repository checkouts --- .../setup-composer/dev-tools-runtime-lib.sh | 17 ++++-- .../GitHubActions/SetupComposerActionTest.php | 60 +++++++++++++++---- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh index 88ca1ecf9b..e61fd59f47 100755 --- a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh +++ b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh @@ -17,12 +17,21 @@ resolve_dev_tools_runtime() { DEV_TOOLS_WORKSPACE_ROOT="$(pwd)" DEV_TOOLS_SOURCE_DIRECTORY="$(resolve_dev_tools_workspace_path "${source_directory_input}")" - DEV_TOOLS_LOCAL_BINARY="${DEV_TOOLS_WORKSPACE_ROOT}/vendor/bin/dev-tools" 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_REPOSITORY_BINARY="${DEV_TOOLS_WORKSPACE_ROOT}/bin/dev-tools" - if [ -x "${DEV_TOOLS_LOCAL_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ]; then + if [ -x "${DEV_TOOLS_LOCAL_INSTALLED_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ]; then DEV_TOOLS_RUNTIME_SOURCE='local' - DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_BINARY}" + DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_INSTALLED_BINARY}" + DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_LOCAL_AUTOLOAD}" + + return 0 + fi + + if [ -x "${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ]; 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 @@ -42,7 +51,7 @@ resolve_dev_tools_runtime() { fi DEV_TOOLS_RUNTIME_SOURCE='workflow' - DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_SOURCE_DIRECTORY}/vendor/bin/dev-tools" + DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_SOURCE_DIRECTORY}/bin/dev-tools" DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_SOURCE_DIRECTORY}/vendor/autoload.php" } diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php index cd46487f53..cd88674423 100644 --- a/tests/GitHubActions/SetupComposerActionTest.php +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -86,7 +86,7 @@ protected function tearDown(): void #[Test] public function detectRuntimeWillPreferTheConsumerLocalInstallation(): void { - $this->createRuntimeFiles($this->workspace . '/vendor'); + $this->createInstalledRuntimeFiles($this->workspace); $resolvedWorkspace = realpath($this->workspace); $files = $this->createGitHubActionFiles(); @@ -123,10 +123,33 @@ public function detectRuntimeWillFallbackToTheWorkflowSourceWhenTheConsumerDoesN self::assertSame('workflow', $outputs['source']); self::assertSame('true', $outputs['needs-fallback']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/bin/dev-tools', $outputs['binary']); + self::assertSame($resolvedWorkspace . '/.dev-tools-actions/bin/dev-tools', $outputs['binary']); self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); } + /** + * @return void + */ + #[Test] + public function detectRuntimeWillPreferTheWorkspaceRootRepositoryCheckout(): 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']); + } + /** * @return void */ @@ -135,7 +158,7 @@ public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWork { mkdir($this->workspace . '/.dev-tools-actions', 0o777, true); file_put_contents($this->workspace . '/.dev-tools-actions/composer.json', "{}\n"); - $this->createRuntimeFiles($this->workspace . '/.dev-tools-actions/vendor'); + $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); $resolvedWorkspace = realpath($this->workspace); $files = $this->createGitHubActionFiles(); @@ -154,7 +177,7 @@ public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWork $pathEntries = array_filter(explode("\n", trim(file_get_contents($files['path'])))); self::assertSame('workflow', $outputs['source']); - self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/bin/dev-tools', $outputs['binary']); + 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']); @@ -170,19 +193,36 @@ public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWork } /** - * @param string $runtimeVendorDirectory + * @param string $runtimeRoot + * + * @return void + */ + private function createInstalledRuntimeFiles(string $runtimeRoot): void + { + mkdir($runtimeRoot . '/vendor/bin', 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/autoload.php', " Date: Thu, 30 Apr 2026 02:41:42 -0300 Subject: [PATCH 3/5] Fix workflow runtime resolution and CLI invocations --- .../changelog/create-dependabot-entry/run.sh | 6 ++-- .../changelog/render-release-notes/run.sh | 2 +- .../actions/changelog/resolve-version/run.sh | 2 +- .../setup-composer/dev-tools-runtime-lib.sh | 25 +++++++++++-- .github/workflows/changelog.yml | 4 +-- .github/workflows/reports.yml | 2 +- .github/workflows/tests.yml | 4 +-- .github/workflows/wiki-preview.yml | 2 +- .../GitHubActions/SetupComposerActionTest.php | 35 ++++++++++++++++--- 9 files changed, 64 insertions(+), 18 deletions(-) diff --git a/.github/actions/changelog/create-dependabot-entry/run.sh b/.github/actions/changelog/create-dependabot-entry/run.sh index 9a7795a712..cdcfe4e44e 100755 --- a/.github/actions/changelog/create-dependabot-entry/run.sh +++ b/.github/actions/changelog/create-dependabot-entry/run.sh @@ -9,7 +9,7 @@ git switch -C "${INPUT_HEAD_REF}" "refs/remotes/origin/${INPUT_HEAD_REF}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" -if dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then +if dev-tools changelog:check --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then { echo "created=false" echo "status=already-present" @@ -19,7 +19,7 @@ if dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="orig exit 0 fi -dev-tools changelog:entry -- --type=changed --file="${INPUT_CHANGELOG_FILE}" "${entry_message}" +dev-tools changelog:entry --type=changed --file="${INPUT_CHANGELOG_FILE}" "${entry_message}" git add "${INPUT_CHANGELOG_FILE}" if git diff --cached --quiet -- "${INPUT_CHANGELOG_FILE}"; then @@ -35,7 +35,7 @@ fi git commit -m "Add changelog entry for Dependabot PR #${INPUT_PULL_REQUEST_NUMBER}" git push origin "HEAD:${INPUT_HEAD_REF}" -if ! dev-tools changelog:check -- --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then +if ! dev-tools changelog:check --file="${INPUT_CHANGELOG_FILE}" --against="origin/${INPUT_BASE_REF}" >/dev/null 2>&1; then { echo "created=false" echo "status=missing" diff --git a/.github/actions/changelog/render-release-notes/run.sh b/.github/actions/changelog/render-release-notes/run.sh index 9a8dd57c51..d234bb66c1 100755 --- a/.github/actions/changelog/render-release-notes/run.sh +++ b/.github/actions/changelog/render-release-notes/run.sh @@ -2,4 +2,4 @@ set -euo pipefail mkdir -p "$(dirname "${INPUT_OUTPUT_FILE}")" -dev-tools changelog:show -- "${INPUT_VERSION}" --file="${INPUT_CHANGELOG_FILE}" > "${INPUT_OUTPUT_FILE}" +dev-tools changelog:show "${INPUT_VERSION}" --file="${INPUT_CHANGELOG_FILE}" > "${INPUT_OUTPUT_FILE}" diff --git a/.github/actions/changelog/resolve-version/run.sh b/.github/actions/changelog/resolve-version/run.sh index dac01a59c6..179ba3bbc4 100755 --- a/.github/actions/changelog/resolve-version/run.sh +++ b/.github/actions/changelog/resolve-version/run.sh @@ -5,7 +5,7 @@ if [ -n "${INPUT_VERSION}" ]; then version="${INPUT_VERSION}" source="input" else - version="$(dev-tools changelog:next-version -- --file="${INPUT_CHANGELOG_FILE}")" + version="$(dev-tools changelog:next-version --file="${INPUT_CHANGELOG_FILE}")" source="inferred" fi diff --git a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh index e61fd59f47..6c7b7e3d4a 100755 --- a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh +++ b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh @@ -12,6 +12,25 @@ resolve_dev_tools_workspace_path() { 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}" +} + resolve_dev_tools_runtime() { local source_directory_input="${INPUT_DEV_TOOLS_SOURCE_DIRECTORY:-.dev-tools-actions}" @@ -29,7 +48,7 @@ resolve_dev_tools_runtime() { return 0 fi - if [ -x "${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ]; then + if [ -x "${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" ] && [ -f "${DEV_TOOLS_LOCAL_AUTOLOAD}" ] && workspace_is_dev_tools_repository "${DEV_TOOLS_WORKSPACE_ROOT}"; then DEV_TOOLS_RUNTIME_SOURCE='local' DEV_TOOLS_RUNTIME_BINARY="${DEV_TOOLS_LOCAL_REPOSITORY_BINARY}" DEV_TOOLS_RUNTIME_AUTOLOAD="${DEV_TOOLS_LOCAL_AUTOLOAD}" @@ -43,8 +62,8 @@ resolve_dev_tools_runtime() { return 1 fi - if [ ! -f "${DEV_TOOLS_SOURCE_DIRECTORY}/composer.json" ]; then - echo "The DevTools workflow source directory does not contain composer.json: ${DEV_TOOLS_SOURCE_DIRECTORY}" >&2 + 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 return 1 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index d4d2f42a57..7507af68ad 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -131,7 +131,7 @@ jobs: pull-request-title: ${{ env.PULL_REQUEST_TITLE }} - name: Verify changelog update - run: dev-tools changelog:check -- --file="${CHANGELOG_FILE}" --against="origin/${BASE_REF}" + run: dev-tools changelog:check --file="${CHANGELOG_FILE}" --against="origin/${BASE_REF}" - uses: ./.dev-tools-actions/.github/actions/summary/write with: @@ -217,7 +217,7 @@ jobs: RELEASE_VERSION: ${{ steps.version.outputs.value }} run: | release_date="$(date -u +%F)" - dev-tools changelog:promote -- "${RELEASE_VERSION}" --file="${CHANGELOG_FILE}" --date="${release_date}" + dev-tools changelog:promote "${RELEASE_VERSION}" --file="${CHANGELOG_FILE}" --date="${release_date}" - name: Render release notes preview uses: ./.dev-tools-actions/.github/actions/changelog/render-release-notes diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index d71c1886c3..a26d13c319 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -89,7 +89,7 @@ jobs: - name: Generate reports env: COMPOSER_ROOT_VERSION: ${{ env.REPORTS_ROOT_VERSION }} - run: dev-tools reports -- --target="${REPORTS_TARGET}" --coverage="${REPORTS_TARGET}/coverage" --metrics="${REPORTS_TARGET}/metrics" + run: dev-tools reports --target="${REPORTS_TARGET}" --coverage="${REPORTS_TARGET}/coverage" --metrics="${REPORTS_TARGET}/metrics" - name: Fix permissions run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e116cd2663..69f2a7b5b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,7 +116,7 @@ jobs: - name: Run PHPUnit tests env: COMPOSER_ROOT_VERSION: ${{ env.TESTS_ROOT_VERSION }} - run: dev-tools tests -- --coverage=.dev-tools/coverage --min-coverage=${{ steps.minimum-coverage.outputs.value }} + run: dev-tools tests --coverage=.dev-tools/coverage --min-coverage=${{ steps.minimum-coverage.outputs.value }} - name: Publish required test status if: ${{ always() && inputs.publish-required-statuses }} @@ -168,7 +168,7 @@ jobs: - name: Run dependency health check env: COMPOSER_ROOT_VERSION: ${{ env.TESTS_ROOT_VERSION }} - run: dev-tools dependencies -- --max-outdated=${{ inputs.max-outdated || -1 }} + run: dev-tools dependencies --max-outdated=${{ inputs.max-outdated || -1 }} summarize: if: ${{ always() }} diff --git a/.github/workflows/wiki-preview.yml b/.github/workflows/wiki-preview.yml index 2241501086..2218ffebe4 100644 --- a/.github/workflows/wiki-preview.yml +++ b/.github/workflows/wiki-preview.yml @@ -77,7 +77,7 @@ jobs: - name: Create Docs Markdown env: COMPOSER_ROOT_VERSION: dev-${{ github.event.pull_request.head.ref }} - run: dev-tools wiki -- --target=.github/wiki + run: dev-tools wiki --target=.github/wiki - name: Commit & push wiki preview branch id: wiki_commit diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php index cd88674423..16426c65c0 100644 --- a/tests/GitHubActions/SetupComposerActionTest.php +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -109,8 +109,7 @@ public function detectRuntimeWillPreferTheConsumerLocalInstallation(): void #[Test] public function detectRuntimeWillFallbackToTheWorkflowSourceWhenTheConsumerDoesNotInstallDevTools(): void { - mkdir($this->workspace . '/.dev-tools-actions', 0o777, true); - file_put_contents($this->workspace . '/.dev-tools-actions/composer.json', "{}\n"); + $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); $resolvedWorkspace = realpath($this->workspace); $files = $this->createGitHubActionFiles(); @@ -150,6 +149,30 @@ public function detectRuntimeWillPreferTheWorkspaceRootRepositoryCheckout(): voi self::assertSame($resolvedWorkspace . '/vendor/autoload.php', $outputs['autoload']); } + /** + * @return void + */ + #[Test] + public function detectRuntimeWillIgnoreAnUnrelatedWorkspaceRepositoryBinary(): 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']); + } + /** * @return void */ @@ -209,12 +232,15 @@ private function createInstalledRuntimeFiles(string $runtimeRoot): void } /** + * @param string $packageName * @param string $runtimeRoot * * @return void */ - private function createRepositoryRuntimeFiles(string $runtimeRoot): void - { + private function createRepositoryRuntimeFiles( + string $runtimeRoot, + string $packageName = 'fast-forward/dev-tools' + ): void { mkdir($runtimeRoot . '/bin', 0o777, true); mkdir($runtimeRoot . '/vendor', 0o777, true); file_put_contents( @@ -222,6 +248,7 @@ private function createRepositoryRuntimeFiles(string $runtimeRoot): void "#!/usr/bin/env bash\nprintf 'dev-tools:%s\\n' \"\$*\"\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', " Date: Thu, 30 Apr 2026 05:44:04 +0000 Subject: [PATCH 4/5] Update wiki submodule pointer for PR #307 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index d3bec40285..9cb08e1c2e 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit d3bec40285be65e5502a5b522c7b395c87fae7a1 +Subproject commit 9cb08e1c2e3410025304ec2a90e50aa8962b84c4 From 1fa24cc2aabcaf5d3dea23875b42d155ae699f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Thu, 30 Apr 2026 02:58:07 -0300 Subject: [PATCH 5/5] Validate local DevTools runtime package metadata --- .../setup-composer/dev-tools-runtime-lib.sh | 3 +- .../GitHubActions/SetupComposerActionTest.php | 38 +++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh index 6c7b7e3d4a..3a137fdc78 100755 --- a/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh +++ b/.github/actions/php/setup-composer/dev-tools-runtime-lib.sh @@ -38,9 +38,10 @@ resolve_dev_tools_runtime() { 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}" ]; then + 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}" diff --git a/tests/GitHubActions/SetupComposerActionTest.php b/tests/GitHubActions/SetupComposerActionTest.php index 16426c65c0..8aea5ebbcb 100644 --- a/tests/GitHubActions/SetupComposerActionTest.php +++ b/tests/GitHubActions/SetupComposerActionTest.php @@ -173,14 +173,36 @@ public function detectRuntimeWillIgnoreAnUnrelatedWorkspaceRepositoryBinary(): v self::assertSame($resolvedWorkspace . '/.dev-tools-actions/vendor/autoload.php', $outputs['autoload']); } + /** + * @return void + */ + #[Test] + public function detectRuntimeWillIgnoreAnUnrelatedInstalledBinary(): 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'], + ]); + + $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']); + } + /** * @return void */ #[Test] public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWorkflowFallback(): void { - mkdir($this->workspace . '/.dev-tools-actions', 0o777, true); - file_put_contents($this->workspace . '/.dev-tools-actions/composer.json', "{}\n"); $this->createRepositoryRuntimeFiles($this->workspace . '/.dev-tools-actions'); $resolvedWorkspace = realpath($this->workspace); @@ -217,17 +239,25 @@ public function exposeRuntimeWillPublishWrapperAndEnvironmentVariablesForTheWork /** * @param string $runtimeRoot + * @param string $packageName * * @return void */ - private function createInstalledRuntimeFiles(string $runtimeRoot): 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', "