diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bb74813..e7a6f34 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,9 @@ updates: prefix: build include: scope open-pull-requests-limit: 20 - directory: / + directories: + - / + - /actions/**/* schedule: interval: weekly day: friday @@ -41,6 +43,7 @@ updates: open-pull-requests-limit: 20 versioning-strategy: increase directories: + - /actions/**/* - /tests/php-project - /tests/php-project/tools schedule: diff --git a/.github/renovate.json b/.github/renovate.json index e9b65c2..0001638 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -38,7 +38,7 @@ }, { "customType": "regex", - "managerFilePatterns": ["/^actions/setup-php/action\\.yml$/"], + "managerFilePatterns": ["/^actions/.*?/action\\.yml$/"], "matchStrings": ["tools: composer:(?\\d+\\.\\d+\\.\\d+)"], "datasourceTemplate": "github-releases", "depNameTemplate": "composer/composer" diff --git a/.github/workflows/__greetings.yml b/.github/workflows/__greetings.yml index 8771559..f4252d6 100644 --- a/.github/workflows/__greetings.yml +++ b/.github/workflows/__greetings.yml @@ -12,7 +12,7 @@ permissions: {} jobs: greetings: if: ${{ github.event_name == 'issues' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }} - uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 permissions: contents: read issues: write diff --git a/.github/workflows/__need-fix-to-issue.yml b/.github/workflows/__need-fix-to-issue.yml index a855f70..a8358b3 100644 --- a/.github/workflows/__need-fix-to-issue.yml +++ b/.github/workflows/__need-fix-to-issue.yml @@ -18,7 +18,7 @@ permissions: {} jobs: main: - uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 permissions: contents: read issues: write diff --git a/.github/workflows/__semantic-pull-request.yml b/.github/workflows/__semantic-pull-request.yml index fc73858..edfa7d0 100644 --- a/.github/workflows/__semantic-pull-request.yml +++ b/.github/workflows/__semantic-pull-request.yml @@ -14,4 +14,4 @@ jobs: main: permissions: contents: read - uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index 6b2dbd1..f9f9048 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -4,7 +4,7 @@ on: permissions: {} jobs: linter: - uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 permissions: contents: read statuses: write @@ -24,6 +24,11 @@ jobs: permissions: contents: read id-token: write + test-action-get-php-versions: + uses: $/.github/workflows/__test-action-get-php-versions.yml + permissions: + contents: read + id-token: write test-action-setup-php: uses: $/.github/workflows/__test-action-setup-php.yml permissions: diff --git a/.github/workflows/__stale.yml b/.github/workflows/__stale.yml index 75286d3..0cec0ee 100644 --- a/.github/workflows/__stale.yml +++ b/.github/workflows/__stale.yml @@ -8,7 +8,7 @@ permissions: {} jobs: main: - uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 permissions: issues: write pull-requests: write diff --git a/.github/workflows/__test-action-get-php-versions.yml b/.github/workflows/__test-action-get-php-versions.yml new file mode 100644 index 0000000..fcc5217 --- /dev/null +++ b/.github/workflows/__test-action-get-php-versions.yml @@ -0,0 +1,137 @@ +name: Internal - Test Composer PHP version discovery +on: + workflow_call: +permissions: {} +jobs: + unit: + uses: $/.github/workflows/continuous-integration.yml + permissions: + contents: read + id-token: write + with: + working-directory: actions/get-php-versions + lint: "false" + dependency-review: false + integration: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Arrange - Composer is the only version declaration + run: | + rm .php-version + php -r 'echo PHP_VERSION;' > "$RUNNER_TEMP/php-version-before" + chmod 600 tests/php-project/composer.json + - name: Act - Discover releases for a nested Composer project + id: versions + uses: $/actions/get-php-versions + with: + working-directory: tests/php-project + - name: Assert - Discovery preserves the runner PHP installation + run: test "$(php -r 'echo PHP_VERSION;')" = "$(cat "$RUNNER_TEMP/php-version-before")" + - name: Act - Set up the highest compatible release + id: setup + uses: $/actions/setup-php + with: + working-directory: ${{ github.workspace }}/tests/php-project + install-dependencies: "false" + - name: Assert - Setup and discovery agree + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + VERSIONS: ${{ steps.versions.outputs.php-versions }} + HIGHEST: ${{ steps.versions.outputs.php-version }} + INSTALLED: ${{ steps.setup.outputs.php-version }} + with: + script: | + const assert = require('node:assert/strict'); + const versions = JSON.parse(process.env.VERSIONS); + assert.ok(versions.length > 1, 'Discover every compatible stable minor, not only one default'); + assert.equal(versions.at(-1), process.env.HIGHEST); + assert.equal(process.env.INSTALLED, process.env.HIGHEST.split('.').slice(0, 2).join('.')); + const {stdout} = await exec.getExecOutput('php', ['-r', 'echo PHP_VERSION;'], {silent: true}); + assert.equal(stdout.trim(), process.env.HIGHEST); + - name: Arrange - Project version file overrides automatic selection + run: echo '8.2' > tests/php-project/.php-version + - name: Act - Honor the version file + id: file + uses: $/actions/setup-php + with: + working-directory: tests/php-project + install-dependencies: "false" + - name: Act - Explicit version overrides the version file + id: explicit + uses: $/actions/setup-php + with: + working-directory: tests/php-project + php-version: "8.3" + install-dependencies: "false" + - name: Assert - Overrides are preserved + env: + FILE_VERSION: ${{ steps.file.outputs.php-version }} + EXPLICIT_VERSION: ${{ steps.explicit.outputs.php-version }} + run: | + test "$FILE_VERSION" = '8.2' + test "$EXPLICIT_VERSION" = '8.3' + - name: Arrange - Composer platform override + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const fs = require('node:fs'); + fs.rmSync('tests/php-project/.php-version'); + fs.rmSync('tests/php-project/composer.lock'); + const manifest = JSON.parse(fs.readFileSync('tests/php-project/composer.json', 'utf8')); + manifest.config = {...manifest.config, platform: {php: '8.2.0'}}; + fs.writeFileSync('tests/php-project/composer.json', JSON.stringify(manifest)); + - name: Act - Honor Composer platform selection + id: platform + uses: $/actions/setup-php + with: + working-directory: tests/php-project + install-dependencies: "false" + - name: Assert - Composer platform override is preserved + env: + PLATFORM_VERSION: ${{ steps.platform.outputs.php-version }} + run: test "$PLATFORM_VERSION" = '8.2' + - name: Act - Resolve the local Docker build runtime + id: docker + run: | + docker build --tag php-version-resolver actions/get-php-versions + test "$(docker run --rm --entrypoint id php-version-resolver -u)" -ne 0 + version=$(docker run --rm --user "$(id -u):$(id -g)" --volume "$GITHUB_WORKSPACE/tests/php-project:/project:ro" php-version-resolver) + printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT" + - name: Assert - Local builds use the same Composer constraint + env: + DOCKER_VERSION: ${{ steps.docker.outputs.version }} + ACTION_VERSION: ${{ steps.versions.outputs.php-version }} + run: test "$DOCKER_VERSION" = "$ACTION_VERSION" + - name: Arrange - Project symlink escapes the checkout + run: ln -s /tmp tests/php-project/outside + - name: Act - Reject a directory outside the checkout + id: outside + continue-on-error: true + uses: $/actions/get-php-versions + with: + working-directory: .. + - name: Act - Reject a symlink outside the checkout + id: symlink + continue-on-error: true + uses: $/actions/get-php-versions + with: + working-directory: tests/php-project/outside + - name: Assert - Invalid paths fail without outputs + env: + OUTSIDE_OUTCOME: ${{ steps.outside.outcome }} + SYMLINK_OUTCOME: ${{ steps.symlink.outcome }} + OUTSIDE_OUTPUTS: ${{ toJSON(steps.outside.outputs) }} + SYMLINK_OUTPUTS: ${{ toJSON(steps.symlink.outputs) }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const assert = require('node:assert/strict'); + for (const scenario of ['OUTSIDE', 'SYMLINK']) { + assert.equal(process.env[`${scenario}_OUTCOME`], 'failure'); + assert.ok(Object.values(JSON.parse(process.env[`${scenario}_OUTPUTS`])).every(value => value === '')); + } diff --git a/.github/workflows/__test-workflow-continuous-integration.yml b/.github/workflows/__test-workflow-continuous-integration.yml index dcbd1e5..61e8023 100644 --- a/.github/workflows/__test-workflow-continuous-integration.yml +++ b/.github/workflows/__test-workflow-continuous-integration.yml @@ -115,7 +115,6 @@ jobs: id-token: write with: working-directory: tests/php-project - php-versions: '["8.2", ""]' test: '{"cache":".phpunit.cache"}' runs-on: '["ubuntu-24.04"]' dependency-review: false diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index 754f9b7..8051f3c 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -25,8 +25,6 @@ jobs: permissions: contents: read id-token: write - with: - php-versions: '["8.2", "8.3", "8.4", "8.5"]' ``` Define `lint:ci` and `test:ci` in `composer.json`. These are explicit project commands; @@ -36,15 +34,22 @@ they execute even when dependency installation scripts are disabled. An empty Use `runs-on` for a JSON array of Linux runner labels, for example `runs-on: '["self-hosted", "linux"]'`. Every job uses those labels. +Docker must be available for the PHP version discovery action. Set `checks: false` to skip lint, tests, dependency review and distribution checks. ### Runtime and dependencies -With no `php-versions` input, setup reads the project's `.php-version`, then the -repository's `.php-version`, then delegates platform detection to `setup-php`. -An explicit version matrix tests all listed versions. The last entry runs lint -and package checks; only that version publishes coverage. An empty entry -in the matrix uses the default version selection. +With no `php-versions` input, CI reads `composer.json`'s `require.php` and +discovers released stable versions from PHP.net using Composer's constraint parser. +Discovery runs in its own container without changing the runner's PHP installation. +It tests the newest compatible patch of every matching minor, including end-of-life +versions still allowed by the project. Prereleases are excluded. Missing or invalid +constraints and release-catalog failures stop discovery instead of choosing an arbitrary runtime. +The highest discovered version runs lint and package checks and publishes coverage. +Set `php-versions` explicitly to override the matrix, for example `'["8.2", "8.5"]'`. +The last explicit entry is primary; an empty entry delegates to the setup action, +which honors `.php-version` and Composer platform overrides before automatic discovery. +See [PHP version selection](../../actions/get-php-versions/README.md#local-docker-builds) for local Docker builds. Composer projects with or without a committed lockfile are supported. Each project is installed independently. By default, setup installs the root manifest @@ -116,7 +121,7 @@ disabled and add one final job using the common reporting workflow: reports: needs: [lint, test] if: always() - uses: hoverkraft-tech/ci-github-common/.github/workflows/ci-reports.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/.github/workflows/ci-reports.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 permissions: contents: read pull-requests: write @@ -228,7 +233,7 @@ on: permissions: {} jobs: continuous-integration: - uses: hoverkraft-tech/ci-github-php/.github/workflows/continuous-integration.yml@7b1303edc25e97da2a297dd285a54cf4b6ae76a1 # 1.1.2 + uses: hoverkraft-tech/ci-github-php/.github/workflows/continuous-integration.yml@cd42dba540a7f3ff20ebeeb67e7710d65c6ec7c5 # feat/composer-php-version-discovery permissions: contents: read id-token: write @@ -245,7 +250,7 @@ jobs: # Default: `.` working-directory: . - # Optional JSON array of PHP versions to test. The last entry runs lint and package checks and publishes coverage. An empty entry uses .php-version or setup-php platform detection. Empty input runs that default once. + # Optional JSON array overriding Composer-based PHP discovery. Empty discovers the newest stable compatible patch per minor. The last version runs lint/package checks and publishes coverage. An empty entry uses the setup action default. php-versions: "" # PHP extensions forwarded to setup-php. @@ -300,31 +305,31 @@ jobs: ### Workflow Call Inputs -| **Input** | **Description** | **Required** | **Type** | **Default** | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | ------------------------------------------------ | -| **`runs-on`** | JSON array of Linux runner labels. Applies to every job, including the configuration job. | **false** | **string** | `["ubuntu-latest"]` | -| **`checks`** | Enable lint, tests, dependency review and optional distribution checks. | **false** | **boolean** | `true` | -| **`working-directory`** | Composer project directory relative to the checkout. | **false** | **string** | `.` | -| **`php-versions`** | Optional JSON array of PHP versions to test. The last entry runs lint and package checks and publishes coverage. An empty entry uses .php-version or setup-php platform detection. Empty input runs that default once. | **false** | **string** | - | -| **`extensions`** | PHP extensions forwarded to setup-php. | **false** | **string** | `mbstring, dom, xml, xmlwriter, curl, zip, intl` | -| **`composer-directories`** | Optional JSON array of Composer directories relative to working-directory. Empty installs the root project and tools/composer.json when present, without recursive discovery. | **false** | **string** | - | -| **`ignore-scripts`** | Disable Composer installation scripts. Explicit project commands still run. | **false** | **boolean** | `true` | -| **`ignore-plugins`** | Disable Composer plugins during installation. When false, Composer allow-plugins configuration still applies. | **false** | **boolean** | `true` | -| **`lint`** | Composer lint script name or a JSON object with `command` (default `lint:ci`). Pass "false", "null" or an empty string to disable. | **false** | **string** | `true` | -| **`test`** | Run the Composer `test:ci` script unchanged, or provide another script name. | **false** | **string** | `true` | -| | Pass "false", "null" or an empty string to disable tests. | | | | -| | A JSON object groups test options: `command` (default `test:ci`), | | | | -| | `coverage` (`github`, `codecov` or empty), and `cache` (one directory relative to working-directory). | | | | -| | Configure report/coverage generation in the Composer script itself. | | | | -| | Coverage is published on the last PHP version only; the command controls collection on every version. Codecov requires id-token: write. | | | | -| | Example: `{"coverage":"codecov","cache":".cache"}`. | | | | -| **`package`** | Validate a public Composer source distribution from the Git repository root. | **false** | **string** | `false` | -| | Pass "false" (the default) to disable, "true" for archive and autoload checks, | | | | -| | or a JSON object with `tests` for additional commands in an isolated Composer consumer. | | | | -| | Quote booleans because this input has type string. Example: `{"tests":"my-cli --help"}`. | | | | -| | vendor/bin is on PATH; PACKAGE_NAME, PACKAGE_INSTALL_PATH and PACKAGE_ARCHIVE_PATH identify the package. | | | | -| | Unknown options and non-string tests are rejected before distribution checks. | | | | -| **`dependency-review`** | Review dependency changes on pull requests. | **false** | **boolean** | `true` | +| **Input** | **Description** | **Required** | **Type** | **Default** | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | ----------- | ------------------------------------------------ | +| **`runs-on`** | JSON array of Linux runner labels. Applies to every job, including the configuration job. | **false** | **string** | `["ubuntu-latest"]` | +| **`checks`** | Enable lint, tests, dependency review and optional distribution checks. | **false** | **boolean** | `true` | +| **`working-directory`** | Composer project directory relative to the checkout. | **false** | **string** | `.` | +| **`php-versions`** | Optional JSON array overriding Composer-based PHP discovery. Empty discovers the newest stable compatible patch per minor. The last version runs lint/package checks and publishes coverage. An empty entry uses the setup action default. | **false** | **string** | - | +| **`extensions`** | PHP extensions forwarded to setup-php. | **false** | **string** | `mbstring, dom, xml, xmlwriter, curl, zip, intl` | +| **`composer-directories`** | Optional JSON array of Composer directories relative to working-directory. Empty installs the root project and tools/composer.json when present, without recursive discovery. | **false** | **string** | - | +| **`ignore-scripts`** | Disable Composer installation scripts. Explicit project commands still run. | **false** | **boolean** | `true` | +| **`ignore-plugins`** | Disable Composer plugins during installation. When false, Composer allow-plugins configuration still applies. | **false** | **boolean** | `true` | +| **`lint`** | Composer lint script name or a JSON object with `command` (default `lint:ci`). Pass "false", "null" or an empty string to disable. | **false** | **string** | `true` | +| **`test`** | Run the Composer `test:ci` script unchanged, or provide another script name. | **false** | **string** | `true` | +| | Pass "false", "null" or an empty string to disable tests. | | | | +| | A JSON object groups test options: `command` (default `test:ci`), | | | | +| | `coverage` (`github`, `codecov` or empty), and `cache` (one directory relative to working-directory). | | | | +| | Configure report/coverage generation in the Composer script itself. | | | | +| | Coverage is published on the last PHP version only; the command controls collection on every version. Codecov requires id-token: write. | | | | +| | Example: `{"coverage":"codecov","cache":".cache"}`. | | | | +| **`package`** | Validate a public Composer source distribution from the Git repository root. | **false** | **string** | `false` | +| | Pass "false" (the default) to disable, "true" for archive and autoload checks, | | | | +| | or a JSON object with `tests` for additional commands in an isolated Composer consumer. | | | | +| | Quote booleans because this input has type string. Example: `{"tests":"my-cli --help"}`. | | | | +| | vendor/bin is on PATH; PACKAGE_NAME, PACKAGE_INSTALL_PATH and PACKAGE_ARCHIVE_PATH identify the package. | | | | +| | Unknown options and non-string tests are rejected before distribution checks. | | | | +| **`dependency-review`** | Review dependency changes on pull requests. | **false** | **boolean** | `true` | diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index d1a28a9..ad310dd 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -10,8 +10,6 @@ # permissions: # contents: read # id-token: write -# with: -# php-versions: '["8.2", "8.3", "8.4", "8.5"]' # ``` # # Define `lint:ci` and `test:ci` in `composer.json`. These are explicit project commands; @@ -21,15 +19,22 @@ # # Use `runs-on` for a JSON array of Linux runner labels, for example # `runs-on: '["self-hosted", "linux"]'`. Every job uses those labels. +# Docker must be available for the PHP version discovery action. # Set `checks: false` to skip lint, tests, dependency review and distribution checks. # # ### Runtime and dependencies # -# With no `php-versions` input, setup reads the project's `.php-version`, then the -# repository's `.php-version`, then delegates platform detection to `setup-php`. -# An explicit version matrix tests all listed versions. The last entry runs lint -# and package checks; only that version publishes coverage. An empty entry -# in the matrix uses the default version selection. +# With no `php-versions` input, CI reads `composer.json`'s `require.php` and +# discovers released stable versions from PHP.net using Composer's constraint parser. +# Discovery runs in its own container without changing the runner's PHP installation. +# It tests the newest compatible patch of every matching minor, including end-of-life +# versions still allowed by the project. Prereleases are excluded. Missing or invalid +# constraints and release-catalog failures stop discovery instead of choosing an arbitrary runtime. +# The highest discovered version runs lint and package checks and publishes coverage. +# Set `php-versions` explicitly to override the matrix, for example `'["8.2", "8.5"]'`. +# The last explicit entry is primary; an empty entry delegates to the setup action, +# which honors `.php-version` and Composer platform overrides before automatic discovery. +# See [PHP version selection](../../actions/get-php-versions/README.md#local-docker-builds) for local Docker builds. # # Composer projects with or without a committed lockfile are supported. Each # project is installed independently. By default, setup installs the root manifest @@ -101,7 +106,7 @@ # reports: # needs: [lint, test] # if: always() -# uses: hoverkraft-tech/ci-github-common/.github/workflows/ci-reports.yml@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 +# uses: hoverkraft-tech/ci-github-common/.github/workflows/ci-reports.yml@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 # permissions: # contents: read # pull-requests: write @@ -210,7 +215,7 @@ on: type: string default: . php-versions: - description: Optional JSON array of PHP versions to test. The last entry runs lint and package checks and publishes coverage. An empty entry uses .php-version or setup-php platform detection. Empty input runs that default once. + description: Optional JSON array overriding Composer-based PHP discovery. Empty discovers the newest stable compatible patch per minor. The last version runs lint/package checks and publishes coverage. An empty entry uses the setup action default. type: string default: "" extensions: @@ -262,7 +267,8 @@ permissions: {} jobs: configuration: runs-on: ${{ fromJSON(inputs.runs-on) }} - permissions: {} + permissions: + contents: read outputs: versions: ${{ steps.config.outputs.versions }} primary-version: ${{ steps.config.outputs.primary-version }} @@ -271,6 +277,15 @@ jobs: package-enabled: ${{ steps.package-config.outputs.enabled }} package-tests: ${{ steps.package-config.outputs.tests }} steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: inputs.checks && inputs.php-versions == '' + with: + persist-credentials: false + - id: versions + if: inputs.checks && inputs.php-versions == '' + uses: $/actions/get-php-versions + with: + working-directory: ${{ inputs.working-directory }} - id: package-config uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: @@ -297,7 +312,7 @@ jobs: - id: config uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - VERSIONS: ${{ inputs.php-versions }} + VERSIONS: ${{ inputs.php-versions || steps.versions.outputs.php-versions }} LINT: ${{ inputs.lint }} TEST: ${{ inputs.test }} with: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 70898ec..f774caa 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -35,7 +35,7 @@ jobs: with: persist-credentials: false - id: directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/working-directory@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ inputs.working-directory }} - name: Generate API documentation diff --git a/Makefile b/Makefile index 04e0b1b..233fab1 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,13 @@ deps-audit: ## Audit action and fixture dependencies composer --working-dir="$$directory" --no-interaction --no-plugins audit; \ done -test: ## Run fixture lint and PHPUnit +test: ## Run fixture lint and Composer project tests composer --working-dir=tests/php-project lint:ci - composer --working-dir=tests/php-project test + @set -e; for directory in $(COMPOSER_DIRECTORIES); do \ + if php -r 'exit(isset(json_decode(file_get_contents($$argv[1]), true)["scripts"]["test"]) ? 0 : 1);' "$$directory/composer.json"; then \ + composer --working-dir="$$directory" test; \ + fi; \ + done ci: ## Install, audit, test and lint without rewriting source files $(MAKE) deps-install diff --git a/README.md b/README.md index c94d65e..10b6b25 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ optional configuration. ### Environment setup +- [Get PHP versions](actions/get-php-versions/README.md): discover stable versions allowed by Composer. - [Setup PHP](actions/setup-php/README.md): install PHP and Composer dependencies. ## Reusable workflows and guides diff --git a/actions/get-php-versions/.dockerignore b/actions/get-php-versions/.dockerignore new file mode 100644 index 0000000..8c7e73e --- /dev/null +++ b/actions/get-php-versions/.dockerignore @@ -0,0 +1,5 @@ +* +!composer.json +!composer.lock +!src/PhpVersions.php +!src/cli.php diff --git a/actions/get-php-versions/Dockerfile b/actions/get-php-versions/Dockerfile new file mode 100644 index 0000000..56ad720 --- /dev/null +++ b/actions/get-php-versions/Dockerfile @@ -0,0 +1,12 @@ +FROM composer:2.11.0@sha256:278dfc7bd35169965d03f87194816f43bf4e6b6ea61707c2114e52ad12fe24ce + +COPY composer.json composer.lock /opt/php-versions/ +RUN composer --working-dir=/opt/php-versions install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist --no-progress +COPY src/ /opt/php-versions/src/ + +USER 65534:65534 + +HEALTHCHECK --interval=5m --timeout=10s --retries=3 CMD ["php", "--version"] + +ENTRYPOINT ["php", "/opt/php-versions/src/cli.php"] +CMD ["/project", "--latest"] diff --git a/actions/get-php-versions/README.md b/actions/get-php-versions/README.md new file mode 100644 index 0000000..71ba8eb --- /dev/null +++ b/actions/get-php-versions/README.md @@ -0,0 +1,129 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItbGlzdCIgY29sb3I9ImJsdWUiPjxsaW5lIHgxPSI4IiB5MT0iNiIgeDI9IjIxIiB5Mj0iNiI+PC9saW5lPjxsaW5lIHgxPSI4IiB5MT0iMTIiIHgyPSIyMSIgeTI9IjEyIj48L2xpbmU+PGxpbmUgeDE9IjgiIHkxPSIxOCIgeDI9IjIxIiB5Mj0iMTgiPjwvbGluZT48bGluZSB4MT0iMyIgeTE9IjYiIHgyPSIzLjAxIiB5Mj0iNiI+PC9saW5lPjxsaW5lIHgxPSIzIiB5MT0iMTIiIHgyPSIzLjAxIiB5Mj0iMTIiPjwvbGluZT48bGluZSB4MT0iMyIgeTE9IjE4IiB4Mj0iMy4wMSIgeTI9IjE4Ij48L2xpbmU+PC9zdmc+) GitHub Action: Get PHP versions + + + + +## Overview + +Discover stable PHP releases matching composer.json require.php using Composer constraints. Runs in Docker on Linux without changing the runner's PHP installation. + +### PHP version selection + +Declare supported PHP versions in `composer.json`: + +```json +{ + "require": { + "php": "^8.2" + } +} +``` + +The CI workflow discovers stable releases allowed by that constraint and tests +the newest matching patch of each minor. Its highest version runs lint and package +checks and publishes coverage. A new stable minor is included when the constraint +already allows it. Explicitly restricted ranges require a deliberate Composer change +before CI adds another minor. End-of-life versions remain tested when still allowed. + +Discovery uses PHP.net release metadata and Composer's constraint parser. It handles +patch bounds and disjoint ranges without approximating them as npm SemVer. +Prereleases are excluded. A missing or invalid `require.php`, an unavailable release +catalog, or a constraint with no matching stable release causes an error. Dependency +installation still validates the project and tooling against the selected runtime. + +The discovery action builds and runs its Docker image on Linux runners with Docker, +using the runner's UID/GID and a read-only checkout mount. +CI and local builds use the same container runtime and locked dependencies. Discovery +does not change PHP on the runner. The setup action then installs the selected PHP +version when the job needs it. + +### Overrides + +`php-versions` overrides the CI matrix. Its last entry is primary. Automatic matrix +discovery uses `require.php` even when a `.php-version` file exists. + +The setup action selects its runtime in this order: + +1. Its explicit `php-version` input. +2. The project `.php-version`, then the repository `.php-version`. +3. Composer platform overrides, as supported by `shivammathur/setup-php`. +4. The highest stable PHP release allowed by `require.php`. + +Version files are optional. Ordinary consumers need only their Composer constraint. +The standalone `get-php-versions` action exposes both the full matrix and its highest +version for custom jobs; discovery does not install project dependencies or run hooks. + +### Local Docker builds + +Local builds read the resolver revision from the existing `ci-github-php` workflow +pins in the checkout. Dependency updates to those workflow pins also update local +builds. No separate resolver revision needs configuration. Its Docker entry point +reads the project through a read-only mount and requires no PHP on the host. + +```sh +set -eu +workflow_ref=$(grep -hE '^[[:space:]]*(-[[:space:]]*)?uses:' .github/workflows/*.y*ml | + grep -oE 'hoverkraft-tech/ci-github-php/[^@[:space:]]+@[a-f0-9]{40}' | + cut -d@ -f2 | sort -u) +if [ "${#workflow_ref}" -ne 40 ]; then + echo 'Expected one consistent ci-github-php commit pin in .github/workflows.' >&2 + exit 1 +fi +docker build --tag php-version-resolver \ + "https://github.com/hoverkraft-tech/ci-github-php.git#${workflow_ref}:actions/get-php-versions" +PHP_VERSION=$(docker run --rm --volume "$PWD:/project:ro" php-version-resolver) +docker build --build-arg "VERSION=$PHP_VERSION" . +``` + +Automatic selection requires workflow files with one consistent commit pin for +`ci-github-php`. Missing or conflicting pins stop the build before fetching the +resolver. Explicit `PHP_VERSION` overrides in the consumer Makefiles skip discovery. + +Docker caches the resolver image's dependencies. Each invocation consults the release +catalog, so newly available compatible releases need no Makefile version edit. The +resolver prints the highest matching patch by default. Pass `/project` as its only +argument to print the complete JSON result instead. + +Local Makefiles can keep a `PHP_VERSION` override for explicit compatibility testing. +When no override is supplied, use the resolver for the build and a stable local +development image name for subsequent commands. + + + + +## Usage + +```yaml +- uses: hoverkraft-tech/ci-github-php/actions/get-php-versions@871b3c4cd7fe8904e49e612ec072c9df3b4fe853 + with: + # Project directory relative to the checkout. + # Default: `.` + working-directory: . +``` + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| ----------------------- | ------------------------------------------- | ------------ | ----------- | +| **`working-directory`** | Project directory relative to the checkout. | **false** | `.` | + + + + + + +## Outputs + +| **Output** | **Description** | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`php-versions`** | JSON array of matching releases, with the newest compatible patch per minor, sorted ascending. Includes end-of-life versions allowed by require.php; excludes prereleases. | +| **`php-version`** | Highest stable PHP release matching require.php. | + + + + diff --git a/actions/get-php-versions/action.yml b/actions/get-php-versions/action.yml new file mode 100644 index 0000000..3c07444 --- /dev/null +++ b/actions/get-php-versions/action.yml @@ -0,0 +1,122 @@ +# ### PHP version selection +# +# Declare supported PHP versions in `composer.json`: +# +# ```json +# { +# "require": { +# "php": "^8.2" +# } +# } +# ``` +# +# The CI workflow discovers stable releases allowed by that constraint and tests +# the newest matching patch of each minor. Its highest version runs lint and package +# checks and publishes coverage. A new stable minor is included when the constraint +# already allows it. Explicitly restricted ranges require a deliberate Composer change +# before CI adds another minor. End-of-life versions remain tested when still allowed. +# +# Discovery uses PHP.net release metadata and Composer's constraint parser. It handles +# patch bounds and disjoint ranges without approximating them as npm SemVer. +# Prereleases are excluded. A missing or invalid `require.php`, an unavailable release +# catalog, or a constraint with no matching stable release causes an error. Dependency +# installation still validates the project and tooling against the selected runtime. +# +# The discovery action builds and runs its Docker image on Linux runners with Docker, +# using the runner's UID/GID and a read-only checkout mount. +# CI and local builds use the same container runtime and locked dependencies. Discovery +# does not change PHP on the runner. The setup action then installs the selected PHP +# version when the job needs it. +# +# ### Overrides +# +# `php-versions` overrides the CI matrix. Its last entry is primary. Automatic matrix +# discovery uses `require.php` even when a `.php-version` file exists. +# +# The setup action selects its runtime in this order: +# +# 1. Its explicit `php-version` input. +# 2. The project `.php-version`, then the repository `.php-version`. +# 3. Composer platform overrides, as supported by `shivammathur/setup-php`. +# 4. The highest stable PHP release allowed by `require.php`. +# +# Version files are optional. Ordinary consumers need only their Composer constraint. +# The standalone `get-php-versions` action exposes both the full matrix and its highest +# version for custom jobs; discovery does not install project dependencies or run hooks. +# +# ### Local Docker builds +# +# Local builds read the resolver revision from the existing `ci-github-php` workflow +# pins in the checkout. Dependency updates to those workflow pins also update local +# builds. No separate resolver revision needs configuration. Its Docker entry point +# reads the project through a read-only mount and requires no PHP on the host. +# +# ```sh +# set -eu +# workflow_ref=$(grep -hE '^[[:space:]]*(-[[:space:]]*)?uses:' .github/workflows/*.y*ml | +# grep -oE 'hoverkraft-tech/ci-github-php/[^@[:space:]]+@[a-f0-9]{40}' | +# cut -d@ -f2 | sort -u) +# if [ "${#workflow_ref}" -ne 40 ]; then +# echo 'Expected one consistent ci-github-php commit pin in .github/workflows.' >&2 +# exit 1 +# fi +# docker build --tag php-version-resolver \ +# "https://github.com/hoverkraft-tech/ci-github-php.git#${workflow_ref}:actions/get-php-versions" +# PHP_VERSION=$(docker run --rm --volume "$PWD:/project:ro" php-version-resolver) +# docker build --build-arg "VERSION=$PHP_VERSION" . +# ``` +# +# Automatic selection requires workflow files with one consistent commit pin for +# `ci-github-php`. Missing or conflicting pins stop the build before fetching the +# resolver. Explicit `PHP_VERSION` overrides in the consumer Makefiles skip discovery. +# +# Docker caches the resolver image's dependencies. Each invocation consults the release +# catalog, so newly available compatible releases need no Makefile version edit. The +# resolver prints the highest matching patch by default. Pass `/project` as its only +# argument to print the complete JSON result instead. +# +# Local Makefiles can keep a `PHP_VERSION` override for explicit compatibility testing. +# When no override is supplied, use the resolver for the build and a stable local +# development image name for subsequent commands. +name: Get PHP versions +description: Discover stable PHP releases matching composer.json require.php using Composer constraints. Runs in Docker on Linux without changing the runner's PHP installation. +author: hoverkraft +branding: + icon: list + color: blue +inputs: + working-directory: + description: Project directory relative to the checkout. + default: . +outputs: + php-versions: + description: JSON array of matching releases, with the newest compatible patch per minor, sorted ascending. Includes end-of-life versions allowed by require.php; excludes prereleases. + value: ${{ steps.versions.outputs.php-versions }} + php-version: + description: Highest stable PHP release matching require.php. + value: ${{ steps.versions.outputs.php-version }} +# FIXME: Replace this PHP bridge with the native Node.js action once a stable +# composer-semver release includes https://github.com/shyim/composer-semver-ts/pull/1 +# and passes the Composer constraint compatibility tests in the migration PR. +runs: + using: composite + steps: + - id: versions + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + script: | + const image = await exec.getExecOutput('docker', ['build', '--quiet', process.env.ACTION_PATH]); + const {stdout} = await exec.getExecOutput('docker', [ + 'run', '--rm', + '--user', `${process.getuid()}:${process.getgid()}`, + '--volume', `${process.env.GITHUB_WORKSPACE}:/project:ro`, + '--workdir', '/project', + '--env', 'GITHUB_WORKSPACE=/project', + image.stdout.trim(), process.env.WORKING_DIRECTORY, + ]); + const versions = JSON.parse(stdout); + core.setOutput('php-versions', versions['php-versions']); + core.setOutput('php-version', versions['php-version']); diff --git a/actions/get-php-versions/composer.json b/actions/get-php-versions/composer.json new file mode 100644 index 0000000..57962d1 --- /dev/null +++ b/actions/get-php-versions/composer.json @@ -0,0 +1,26 @@ +{ + "name": "hoverkraft/get-php-versions", + "description": "Resolve released PHP versions using Composer project constraints", + "type": "project", + "license": "MIT", + "require": { + "php": "^8.4.1", + "composer/semver": "^3.6" + }, + "require-dev": { + "phpunit/phpunit": "^13.4" + }, + "autoload": { + "psr-4": { + "Hoverkraft\\PhpVersions\\": "src/" + } + }, + "scripts": { + "test": "phpunit --bootstrap vendor/autoload.php src/PhpVersionsTest.php", + "test:ci": "@test --log-junit=junit.xml --coverage-cobertura=cobertura.xml --coverage-filter=src" + }, + "config": { + "sort-packages": true, + "allow-plugins": false + } +} diff --git a/actions/get-php-versions/composer.lock b/actions/get-php-versions/composer.lock new file mode 100644 index 0000000..a218eee --- /dev/null +++ b/actions/get-php-versions/composer.lock @@ -0,0 +1,1993 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ef0845c23dddebb720768971efdb2f7b", + "packages": [ + { + "name": "composer/semver", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/d92543b7ede9c4ec6b59853cefb061e39866d2cf", + "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.6.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-10-29T09:13:34+00:00", + "published-time": "2026-10-29T09:14:11+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00", + "published-time": "2026-08-11T10:29:20+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00", + "published-time": "2024-03-03T12:39:44+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00", + "published-time": "2022-02-21T01:04:30+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "14.5.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2f122b9b48af71ad9b4448368febc26af86534c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2f122b9b48af71ad9b4448368febc26af86534c9", + "reference": "2f122b9b48af71ad9b4448368febc26af86534c9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0.1", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.3", + "sebastian/git-state": "^1.1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.4.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "14.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.5.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-11-09T05:24:08+00:00", + "published-time": "2026-11-09T05:25:09+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:47:43+00:00", + "published-time": "2026-08-25T14:48:49+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00", + "published-time": "2026-02-06T04:35:29+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "97107691cb2b6c743b5e0c44be19e983cb337a02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/97107691cb2b6c743b5e0c44be19e983cb337a02", + "reference": "97107691cb2b6c743b5e0c44be19e983cb337a02", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-10-03T04:54:24+00:00", + "published-time": "2026-10-03T04:54:39+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00", + "published-time": "2026-02-06T04:38:39+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.4.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "4821376960d0e1e5a7b7b700955ebcab33ecd71b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4821376960d0e1e5a7b7b700955ebcab33ecd71b", + "reference": "4821376960d0e1e5a7b7b700955ebcab33ecd71b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.5.0", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.1", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0.1", + "sebastian/environment": "^9.3.3", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.1", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.1", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.4-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.4.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-11-02T07:12:28+00:00", + "published-time": "2026-11-02T07:13:08+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-08-01T04:27:14+00:00", + "published-time": "2026-08-01T04:29:01+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:23:13+00:00", + "published-time": "2026-08-07T07:23:29+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00", + "published-time": "2026-02-06T04:41:49+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00", + "published-time": "2026-08-25T15:39:44+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d4dd10267a11055066a2a78ae04583d8ddd4f595", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-10-01T12:37:56+00:00", + "published-time": "2026-10-01T12:38:48+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:22:06+00:00", + "published-time": "2026-08-07T07:23:47+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00", + "published-time": "2026-04-22T07:20:25+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "31a565f463686f0eefa85f899549256bf5805985" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/31a565f463686f0eefa85f899549256bf5805985", + "reference": "31a565f463686f0eefa85f899549256bf5805985", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-10-21T05:59:52+00:00", + "published-time": "2026-10-21T06:01:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:11:33+00:00", + "published-time": "2026-06-01T15:20:45+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-07-09T08:42:34+00:00", + "published-time": "2026-07-09T08:44:06+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-08-13T07:05:05+00:00", + "published-time": "2026-08-13T07:07:58+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-08-13T06:34:36+00:00", + "published-time": "2026-08-13T06:39:22+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-03T05:58:12+00:00", + "published-time": "2026-08-03T05:59:59+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "bd1df467864cb95140414059a535b2d906173fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-08-10T08:00:57+00:00", + "published-time": "2026-08-10T08:01:43+00:00" + }, + { + "name": "sebastian/version", + "version": "7.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "08e41d0c7e146bfc8cef7e63989b1294453a6d6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/08e41d0c7e146bfc8cef7e63989b1294453a6d6a", + "reference": "08e41d0c7e146bfc8cef7e63989b1294453a6d6a", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-11-09T06:31:27+00:00", + "published-time": "2026-11-09T06:31:50+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00", + "published-time": "2024-10-20T05:09:39+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.4.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.11.0" +} diff --git a/actions/get-php-versions/src/PhpVersions.php b/actions/get-php-versions/src/PhpVersions.php new file mode 100644 index 0000000..e7d9c0c --- /dev/null +++ b/actions/get-php-versions/src/PhpVersions.php @@ -0,0 +1,80 @@ +fetch = $fetch ?? self::fetch(...); + } + + /** @return array{php-versions: list, php-version: string} */ + public function resolve(string $directory): array + { + $file = rtrim($directory, '/\\') . '/composer.json'; + if (!is_file($file)) { + throw new RuntimeException('Missing composer.json in ' . $directory); + } + $manifest = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); + $constraint = $manifest['require']['php'] ?? null; + if (!is_string($constraint) || trim($constraint) === '') { + throw new UnexpectedValueException('Declare require.php in composer.json or select PHP explicitly.'); + } + // Validate before requesting the release catalog, using Composer's own parser. + (new VersionParser())->parseConstraints($constraint); + $releases = []; + foreach (array_keys(($this->fetch)(self::RELEASES_URL)) as $major) { + if (!ctype_digit((string) $major) || (int) $major < 5) { + continue; + } + $catalog = ($this->fetch)(self::RELEASES_URL . '&version=' . $major . '&max=1000'); + foreach (array_keys($catalog) as $version) { + if (preg_match('/^\d+\.\d+\.\d+$/D', (string) $version) && version_compare($version, '5.3.0', '>=')) { + $releases[] = $version; + } + } + } + $matches = Semver::sort(Semver::satisfiedBy($releases, $constraint)); + $versions = []; + foreach ($matches as $version) { + $minor = substr($version, 0, strrpos($version, '.')); + $versions[$minor] = $version; + } + if ($versions === []) { + throw new RuntimeException('No stable PHP release matches require.php: ' . $constraint); + } + return ['php-versions' => array_values($versions), 'php-version' => end($versions)]; + } + + private static function fetch(string $url): array + { + $context = stream_context_create(['http' => ['timeout' => 20, 'user_agent' => 'hoverkraft-php-versions']]); + for ($attempt = 0; $attempt < 3; $attempt++) { + $body = @file_get_contents($url, false, $context); + if ($body !== false) { + $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($data) || $data === []) { + throw new UnexpectedValueException('Invalid PHP release catalog: ' . $url); + } + return $data; + } + if ($attempt < 2) { + usleep(500000 * ($attempt + 1)); + } + } + throw new RuntimeException('Cannot retrieve the PHP release catalog: ' . $url); + } +} diff --git a/actions/get-php-versions/src/PhpVersionsTest.php b/actions/get-php-versions/src/PhpVersionsTest.php new file mode 100644 index 0000000..df767d8 --- /dev/null +++ b/actions/get-php-versions/src/PhpVersionsTest.php @@ -0,0 +1,123 @@ +directory = sys_get_temp_dir() . '/php-versions-' . bin2hex(random_bytes(8)); + mkdir($this->directory); + $this->resolver = new PhpVersions(static function (string $url): array { + if (!str_contains($url, '&version=')) { + return ['8' => [], '7' => [], '4' => []]; + } + $versions = str_contains($url, 'version=7') ? ['7.4.33'] : [ + '8.2.1', '8.2.10', '8.3.0', '8.3.12', '8.4.1', '8.5.0', '8.6.0RC1', '9.0.0-dev', + ]; + return array_fill_keys($versions, []); + }); + } + + protected function tearDown(): void + { + if (is_file($this->directory . '/composer.json')) { + unlink($this->directory . '/composer.json'); + } + rmdir($this->directory); + } + + public static function constraints(): array + { + return [ + 'caret' => ['^8.2', ['8.2.10', '8.3.12', '8.4.1', '8.5.0']], + 'Composer tilde allows later minors' => ['~8.2', ['8.2.10', '8.3.12', '8.4.1', '8.5.0']], + 'tilde with patch' => ['~8.2.0', ['8.2.10']], + 'patch minimum' => ['>=8.2.5 <8.3', ['8.2.10']], + 'patch maximum' => ['>=8.2 <8.2.10', ['8.2.1']], + 'union skips unsupported minor' => ['~8.2.0 || ~8.4.0', ['8.2.10', '8.4.1']], + 'single pipe' => ['8.2.* | 8.4.*', ['8.2.10', '8.4.1']], + 'excluded patch' => ['>=8.2 <8.5 !=8.3.12', ['8.2.10', '8.3.0', '8.4.1']], + 'end of life remains explicit support' => ['^7.4', ['7.4.33']], + 'prereleases stay excluded' => ['>=8.5@dev', ['8.5.0']], + 'exact patch' => ['8.3.0', ['8.3.0']], + ]; + } + + #[DataProvider('constraints')] + public function testComposerConstraints(string $constraint, array $expected): void + { + file_put_contents($this->directory . '/composer.json', json_encode(['require' => ['php' => $constraint]])); + self::assertSame(['php-versions' => $expected, 'php-version' => end($expected)], $this->resolver->resolve($this->directory)); + } + + public function testNewStableMinorAppearsWithoutEditingTheProject(): void + { + file_put_contents($this->directory . '/composer.json', '{"require":{"php":"^8.2"}}'); + $resolver = new PhpVersions(static fn (string $url): array => str_contains($url, '&version=') + ? ['8.5.1' => [], '8.6.0' => [], '8.7.0RC1' => []] + : ['8' => []]); + self::assertSame(['8.5.1', '8.6.0'], $resolver->resolve($this->directory)['php-versions']); + } + + public function testMissingManifestFails(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Missing composer.json'); + $this->resolver->resolve($this->directory); + } + + public function testMissingConstraintFails(): void + { + file_put_contents($this->directory . '/composer.json', '{}'); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('require.php'); + $this->resolver->resolve($this->directory); + } + + public function testMalformedManifestFails(): void + { + file_put_contents($this->directory . '/composer.json', '{'); + $this->expectException(JsonException::class); + $this->resolver->resolve($this->directory); + } + + public function testInvalidConstraintFailsBeforeNetworkAccess(): void + { + file_put_contents($this->directory . '/composer.json', '{"require":{"php":"garbage"}}'); + $resolver = new PhpVersions(static function (): never { + self::fail('Invalid input must fail before accessing the network.'); + }); + $this->expectException(UnexpectedValueException::class); + $resolver->resolve($this->directory); + } + + public function testNoMatchFailsInsteadOfUsingLatest(): void + { + file_put_contents($this->directory . '/composer.json', '{"require":{"php":"^9.0"}}'); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No stable PHP release matches'); + $this->resolver->resolve($this->directory); + } + + public function testCatalogFailureIsNotSilentlyIgnored(): void + { + file_put_contents($this->directory . '/composer.json', '{"require":{"php":"^8.2"}}'); + $resolver = new PhpVersions(static function (): never { + throw new RuntimeException('Catalog unavailable'); + }); + $this->expectExceptionMessage('Catalog unavailable'); + $resolver->resolve($this->directory); + } +} diff --git a/actions/get-php-versions/src/cli.php b/actions/get-php-versions/src/cli.php new file mode 100644 index 0000000..33ab6ea --- /dev/null +++ b/actions/get-php-versions/src/cli.php @@ -0,0 +1,29 @@ + 3 || ($argc === 3 && $argv[2] !== '--latest')) { + throw new InvalidArgumentException('Usage: php src/cli.php PROJECT_DIRECTORY [--latest]'); + } + $directory = realpath($argv[1]); + if ($directory === false || !is_dir($directory)) { + throw new InvalidArgumentException('Project directory does not exist: ' . $argv[1]); + } + $workspace = getenv('GITHUB_WORKSPACE'); + if ($workspace !== false && $workspace !== '') { + $workspace = realpath($workspace); + if ($workspace === false || ($directory !== $workspace && !str_starts_with($directory, $workspace . '/'))) { + throw new InvalidArgumentException('Project directory must be inside GITHUB_WORKSPACE.'); + } + } + $versions = (new PhpVersions())->resolve($directory); + echo (($argv[2] ?? '') === '--latest' ? $versions['php-version'] : json_encode($versions, JSON_THROW_ON_ERROR)) . PHP_EOL; +} catch (Throwable $error) { + fwrite(STDERR, $error->getMessage() . PHP_EOL); + exit(1); +} diff --git a/actions/lint/action.yml b/actions/lint/action.yml index 919c724..06792e8 100644 --- a/actions/lint/action.yml +++ b/actions/lint/action.yml @@ -40,7 +40,7 @@ runs: using: composite steps: - id: directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/working-directory@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ inputs.working-directory }} - id: run @@ -58,7 +58,7 @@ runs: - name: Report lint results id: reports if: ${{ !cancelled() && steps.directory.outcome == 'success' }} - uses: hoverkraft-tech/ci-github-common/actions/quality/parse-ci-reports@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/quality/parse-ci-reports@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ steps.directory.outputs.absolute-path }} report-paths: auto:lint @@ -66,7 +66,7 @@ runs: output-format: annotations,summary,markdown - name: Publish individual report comment if: ${{ !cancelled() && inputs.comment == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.reports.outputs.lint-markdown != '' }} - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: title: PHP lint results body: ${{ steps.reports.outputs.lint-markdown }} diff --git a/actions/package/test/action.yml b/actions/package/test/action.yml index 2bb9786..d0d2c60 100644 --- a/actions/package/test/action.yml +++ b/actions/package/test/action.yml @@ -15,7 +15,7 @@ runs: using: composite steps: - id: directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/working-directory@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ inputs.working-directory }} - id: test diff --git a/actions/setup-php/README.md b/actions/setup-php/README.md index ba25bbb..b8db98a 100644 --- a/actions/setup-php/README.md +++ b/actions/setup-php/README.md @@ -21,13 +21,13 @@ Set up PHP and Composer, cache downloads and install project dependencies with o ## Usage ```yaml -- uses: hoverkraft-tech/ci-github-php/actions/setup-php@7b1303edc25e97da2a297dd285a54cf4b6ae76a1 # 1.1.2 +- uses: hoverkraft-tech/ci-github-php/actions/setup-php@1943175e66ec469d305a36f839048a98191476a8 # feat/composer-php-version-discovery with: # Project directory relative to the checkout. # Default: `.` working-directory: . - # Explicit PHP version. Empty reads the project or repository .php-version, then setup-php platform detection. + # Explicit PHP version. Empty honors .php-version and Composer platform overrides, then discovers the highest stable release matching composer.json require.php. php-version: "" # PHP extensions forwarded to shivammathur/setup-php. @@ -62,7 +62,7 @@ Set up PHP and Composer, cache downloads and install project dependencies with o | **Input** | **Description** | **Required** | **Default** | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------ | | **`working-directory`** | Project directory relative to the checkout. | **false** | `.` | -| **`php-version`** | Explicit PHP version. Empty reads the project or repository .php-version, then setup-php platform detection. | **false** | - | +| **`php-version`** | Explicit PHP version. Empty honors .php-version and Composer platform overrides, then discovers the highest stable release matching composer.json require.php. | **false** | - | | **`extensions`** | PHP extensions forwarded to shivammathur/setup-php. | **false** | `mbstring, dom, xml, xmlwriter, curl, zip, intl` | | **`coverage`** | Coverage driver: none, pcov or xdebug. | **false** | `pcov` | | **`composer-directories`** | JSON array of Composer project directories relative to working-directory. Empty installs the root project and tools/composer.json when present, without recursive discovery. | **false** | - | diff --git a/actions/setup-php/action.yml b/actions/setup-php/action.yml index 78a5198..170b221 100644 --- a/actions/setup-php/action.yml +++ b/actions/setup-php/action.yml @@ -9,7 +9,7 @@ inputs: description: Project directory relative to the checkout. default: . php-version: - description: Explicit PHP version. Empty reads the project or repository .php-version, then setup-php platform detection. + description: Explicit PHP version. Empty honors .php-version and Composer platform overrides, then discovers the highest stable release matching composer.json require.php. default: "" extensions: description: PHP extensions forwarded to shivammathur/setup-php. @@ -37,25 +37,39 @@ runs: using: composite steps: - id: directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/working-directory@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ inputs.working-directory }} - id: version-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: DIRECTORY: ${{ steps.directory.outputs.absolute-path }} + PHP_VERSION: ${{ inputs.php-version }} with: script: |- const fs = require('node:fs'); const path = require('node:path'); const local = path.join(process.env.DIRECTORY, '.php-version'); - core.setOutput('path', fs.existsSync(local) ? local : path.join(process.env.GITHUB_WORKSPACE, '.php-version')); + const versionFile = fs.existsSync(local) ? local : path.join(process.env.GITHUB_WORKSPACE, '.php-version'); + core.setOutput('path', fs.existsSync(versionFile) ? versionFile : ''); + if (process.env.PHP_VERSION || fs.existsSync(versionFile)) return; + const read = name => { + const file = path.join(process.env.DIRECTORY, name); + return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}; + }; + const platform = read('composer.lock')['platform-overrides']?.php || read('composer.json').config?.platform?.php; + core.setOutput('discover', !fs.existsSync(versionFile) && !platform); + - id: versions + if: inputs.php-version == '' && steps.version-file.outputs.discover == 'true' + uses: $/actions/get-php-versions + with: + working-directory: ${{ steps.directory.outputs.workspace-relative-path }} - id: php uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 env: COMPOSER_PROJECT_DIR: ${{ steps.directory.outputs.absolute-path }} with: - php-version: ${{ inputs.php-version }} + php-version: ${{ inputs.php-version || steps.versions.outputs.php-version }} php-version-file: ${{ steps.version-file.outputs.path }} tools: composer:2.11.0 extensions: ${{ inputs.extensions }} diff --git a/actions/test/action.yml b/actions/test/action.yml index 005b776..6689054 100644 --- a/actions/test/action.yml +++ b/actions/test/action.yml @@ -42,7 +42,7 @@ runs: using: composite steps: - id: directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/working-directory@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ inputs.working-directory }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 @@ -65,7 +65,7 @@ runs: - name: Report test results id: reports if: ${{ !cancelled() && steps.directory.outcome == 'success' }} - uses: hoverkraft-tech/ci-github-common/actions/quality/parse-ci-reports@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/quality/parse-ci-reports@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ steps.directory.outputs.absolute-path }} report-paths: ${{ inputs.coverage == 'github' && 'auto:test,auto:coverage' || 'auto:test' }} @@ -73,7 +73,7 @@ runs: output-format: annotations,summary,markdown - name: Publish individual report comment if: ${{ !cancelled() && inputs.comment == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (steps.reports.outputs.test-markdown != '' || steps.reports.outputs.coverage-markdown != '') }} - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: title: PHP test results body: | @@ -135,7 +135,7 @@ runs: core.setOutput('files', files.sort().join(',')); - name: Upload coverage if: "!cancelled() && steps.coverage-reports.outcome == 'success'" - uses: hoverkraft-tech/ci-github-common/actions/quality/codecov@66882b24f9f13e45a6541896a5a7c638cb91c94c # 4.0.0 + uses: hoverkraft-tech/ci-github-common/actions/quality/codecov@d07ee49b460ae2f3815804b5153fe918092aa321 # 4.2.0 with: working-directory: ${{ steps.directory.outputs.absolute-path }} files: ${{ steps.coverage-reports.outputs.files }} diff --git a/docs/README.md b/docs/README.md index 379578f..0babbad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,6 @@ # Documentation - [Continuous integration](../.github/workflows/continuous-integration.md): configure PHP and Composer checks. +- [PHP version selection](../actions/get-php-versions/README.md#php-version-selection): Composer constraints, overrides, and local Docker builds. - [Development](development/README.md): fixtures, validation, and documentation generation. - [Architecture decision](adr/0001-php-package-workflows.md): scope, trade-offs, and candidate findings. diff --git a/docs/adr/0001-php-package-workflows.md b/docs/adr/0001-php-package-workflows.md index ed02e5f..bf42a04 100644 --- a/docs/adr/0001-php-package-workflows.md +++ b/docs/adr/0001-php-package-workflows.md @@ -70,6 +70,46 @@ The initial package scope is one public Composer package at a Git repository roo Private registry authentication, splitting monorepos, prerelease channels, and application deployment targets need concrete consumers before adding interfaces. +## Composer-based PHP version discovery + +The 2026-11-11 review replaces the default single-runtime CI selection with a +matrix derived from `composer.json`'s `require.php`. The same standalone resolver +selects the highest matching runtime for setup and local Docker builds. PHP.net +supplies released versions; Composer's own SemVer library handles constraints, +including tilde semantics, disjunctions, exclusions and patch bounds. Keep the +newest matching patch for each minor and exclude prereleases. End-of-life minors +remain in the matrix when the project still declares support for them. + +Explicit workflow matrices, setup inputs, version files and Composer platform +overrides remain available. A version file affects default setup, not the +automatically discovered CI compatibility matrix. Projects need not copy their +supported versions into a matrix, a default-runtime file and a Makefile. + +Discovery requires network access and fails clearly when the catalog or constraint +is unavailable. It never falls back to an unconstrained latest release. The +resolver uses the PHP runtime and locked Composer dependency in its Docker image; +that tooling runtime does not change the versions supported by consumer packages. +The action and local Docker entry point share the exact resolver and require no host PHP. + +Docker execution replaces the initial PHP/Composer bootstrap on the runner. It +requires Linux with Docker. The entry point validates paths against the mounted +workspace; project dependencies and hooks are never executed. + +The same-day security review supersedes native Docker-action execution, which +required the root user and scanner exceptions for GitHub's workspace/output mounts. +The image defaults to an unprivileged user. A composite step uses `github-script`'s +`exec` helper to build it and run it with the runner's UID/GID and a read-only +checkout mount. `core` publishes the JSON outputs from the host, so the container +needs no writable output mount. Trivy and Checkov retain their default-user checks +without exceptions. Dependabot's recursive action directories cover the resolver's +Composer manifest and lockfile as well as its Docker base image. + +The same review rejects a separately configured resolver revision in each Makefile. +Local builds derive it from the existing workflow commit pins, keeping dependency +updates effective for both CI and local tooling. Missing or conflicting pins fail +before fetching the resolver. This couples automatic local selection to a checkout +with consistent workflow pins; an explicit PHP version bypasses discovery. + ## Alternatives Copying npm's version-update step would introduce source churn that Composer does diff --git a/docs/development/README.md b/docs/development/README.md index 1fdc722..e89022f 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -2,7 +2,11 @@ Actions are standalone. Prefer `actions/github-script` and its supplied `core`, `exec`, `io`, and `glob` helpers. Composer metadata validation needs no dependency -installation. Release planning and creation use `ci-github-publish`; the release +installation. The standalone version resolver uses Composer's constraint parser and +PHP.net release metadata; its Dockerfile installs only its own locked dependencies. +The action builds and runs that image with the runner's UID/GID and a read-only +checkout mount. `github-script` publishes the resolver's JSON as action outputs. +Release planning and creation use `ci-github-publish`; the release workflow validates Composer metadata and tag identity before publication. ## Validation @@ -26,7 +30,7 @@ actual preparation and publication scripts: event restrictions, no-change status prepared tag handoff, retry context and Composer metadata. Shared planner and verifier tests in `ci-github-publish` cover force and tag recovery. They never create a tag or release. -With PHP, Composer and Git installed, run: +With PHP 8.4.1 or newer, Composer and Git installed, run: ```sh make setup @@ -34,7 +38,7 @@ make test ``` `setup` installs dependencies for tracked Composer fixtures with scripts and plugins -disabled. `test` runs fixture lint and PHPUnit. `deps-update` refreshes their +disabled. `test` runs fixture lint and the Composer projects that declare a test script. `deps-update` refreshes their lockfiles, with `deps-update-interactive` for selecting updates; `deps-audit` reports dependency advisories without changing files. `make ci` installs, audits, tests and lints; it does not automatically fix files. @@ -52,13 +56,20 @@ through `ci-github-publish/.github/workflows/release-actions.yml`. Update metada then regenerate using that tool. The CI and release workflows include their authored guides in leading comments; regeneration writes them to the corresponding `.github/workflows/*.md` files. -Dependabot updates GitHub Actions, Composer fixtures, containers, and development -containers. Each ecosystem explicitly uses the `build` commit prefix with a +Dependabot updates GitHub Actions, action Composer manifests and lockfiles, Composer +fixtures, containers, and development containers. Recursive action directory patterns +cover standalone action dependencies without a per-action list. Each ecosystem explicitly uses the `build` commit prefix with a dependency scope. The root `.commitlintrc.json` configures the same rules for contributors and Dependabot. Conventional types and required subjects remain blocking; casing and line lengths produce warnings. Local linting, commit checks and the semantic pull request workflow read that repository configuration. +The resolver has unit coverage for Composer constraints, patch bounds, exclusions, +prereleases and discovery failures. Its integration workflow checks default setup, +explicit overrides, workspace containment, unchanged runner PHP, and the same +resolver in local Docker builds. Public CI exercises automatic +matrix discovery against the nested Composer fixture. + Renovate follows the common Hoverkraft maintenance workflow for Docker action references, the root `.php-version`, and the Composer bootstrap version. The maintenance workflow receives its GitHub App credentials from this