diff --git a/.github/workflows/reusable-functional.yml b/.github/workflows/reusable-functional.yml index 3bf0cc6..d1ab1a8 100644 --- a/.github/workflows/reusable-functional.yml +++ b/.github/workflows/reusable-functional.yml @@ -25,37 +25,26 @@ on: required: false default: false os: - description: 'Runner to use. Defaults to ubuntu-24.04, or the RUNNERS_NAME repository variable when set.' + description: 'Runner to use. Defaults to ubuntu-22.04, or the RUNNERS_NAME repository variable when set.' type: string required: false default: '' - grouped: - description: 'Set when the calling job name already states the suite and PHP version, so this workflow leaves them out instead of repeating them inside the group.' - type: boolean - required: false - default: false permissions: contents: read jobs: functional: - # When the caller wraps this workflow, its own name is not surfaced in the run - # view, so this name has to carry everything. When the caller fans out, its job - # name becomes the group header and already states the suite and PHP version, - # so repeating them here reads as noise. `grouped` picks between the two. - # - # Written as `!grouped && || ''` rather than `grouped && '' || ` - # on purpose: an empty string is falsy, so the latter would fall through to the - # prefix in both cases. - # - # The database version is spelled out because "MySQL" alone rendered the - # mysql-8.0 and mysql-8.4 legs identically. - name: ${{ !inputs.grouped && format('Behat | PHP {0} | ', inputs.php) || '' }}WP ${{ inputs.wp }} | ${{ inputs.dbtype == 'sqlite' && 'SQLite' || inputs.mysql || 'MySQL' }}${{ inputs.object_cache == 'sqlite' && ' (Obj Cache)' || '' }}${{ inputs.coverage && ' (with coverage)' || '' }}${{ startsWith( inputs.os, 'windows' ) && ' (Windows)' || '' }}${{ startsWith( inputs.os, 'macos' ) && ' (macOS)' || '' }} + # This name has to stand on its own. The run view labels a nested job with its + # own name only; the calling job's name is not surfaced at this depth, so + # anything omitted here is not shown anywhere. The database version is spelled + # out because "MySQL" alone rendered the mysql-8.0 and mysql-8.4 legs + # identically. + name: Behat | PHP ${{ inputs.php }} | WP ${{ inputs.wp }} | ${{ inputs.dbtype == 'sqlite' && 'SQLite' || inputs.mysql || 'MySQL' }}${{ inputs.object_cache == 'sqlite' && ' (Obj Cache)' || '' }}${{ inputs.coverage && ' (with coverage)' || '' }}${{ startsWith( inputs.os, 'windows' ) && ' (Windows)' || '' }}${{ startsWith( inputs.os, 'macos' ) && ' (macOS)' || '' }} # Repositories with a heavy Behat suite can point the default Linux legs at a # larger runner by setting the `RUNNERS_NAME` repository variable, without # having to fork this workflow. Explicit macOS/Windows legs are unaffected. - runs-on: ${{ inputs.os || vars.RUNNERS_NAME || 'ubuntu-24.04' }} + runs-on: ${{ inputs.os || vars.RUNNERS_NAME || 'ubuntu-22.04' }} continue-on-error: ${{ inputs.dbtype == 'mariadb' || inputs.php == 'nightly' || startsWith( inputs.os, 'windows' ) || startsWith( inputs.os, 'macos' ) }} @@ -107,7 +96,7 @@ jobs: # image drops it. - name: Install Ghostscript # Keyed on the actual runner rather than the `os` input: an empty input no - # longer implies the default image now that RUNNERS_NAME can select it. + # longer implies ubuntu-22.04 now that RUNNERS_NAME can select the image. if: ${{ runner.os == 'Linux' }} run: | if command -v gs > /dev/null 2>&1; then @@ -142,22 +131,16 @@ jobs: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Change ImageMagick policy to allow pdf->png conversion. - # Keyed on the actual runner rather than the `os` input, and matched by glob - # rather than a hardcoded ImageMagick-6 path, so this keeps working if an - # image ships ImageMagick 7. `sed -i` on a missing file exits non-zero and - # would fail the leg. + # Keyed on the actual runner rather than the `os` input, and tolerant of + # images that ship a different ImageMagick layout — `sed -i` on a missing + # file exits non-zero and would fail the leg. if: ${{ runner.os == 'Linux' }} run: | - shopt -s nullglob - POLICIES=(/etc/ImageMagick-*/policy.xml) - if [ ${#POLICIES[@]} -eq 0 ]; then - echo 'No ImageMagick policy file found; nothing to relax.' - exit 0 + if [ -f /etc/ImageMagick-6/policy.xml ]; then + sudo sed -i 's/^.*policy.*coder.*none.*PDF.*//' /etc/ImageMagick-6/policy.xml + else + echo 'No ImageMagick 6 policy file found; nothing to relax.' fi - for policy in "${POLICIES[@]}"; do - echo "Relaxing the PDF coder policy in ${policy}." - sudo sed -i 's/^.*policy.*coder.*none.*PDF.*//' "$policy" - done # WP-CLI packages do not commit a lock file, so `composer update` resolves # dependencies on every run while the cache key stays pinned to composer.json. diff --git a/.github/workflows/reusable-prepare-matrix.yml b/.github/workflows/reusable-prepare-matrix.yml deleted file mode 100644 index ad82704..0000000 --- a/.github/workflows/reusable-prepare-matrix.yml +++ /dev/null @@ -1,520 +0,0 @@ -## -# Computes the unit and functional test matrices for a package. -# -# Split out of reusable-testing.yml so a caller can own the fan-out itself. A -# matrix only becomes a collapsible group in the Actions run view when it sits on -# a job declared in the workflow the run belongs to; a matrix one level down is -# not surfaced. Calling this workflow directly and fanning out from the caller is -# therefore the only way to group the legs by PHP version. -# -# reusable-testing.yml still wraps this for callers that do not need that. -## -name: Prepare test matrices - -on: - workflow_call: - inputs: - minimum-php: - description: 'Minimum PHP version to test against.' - type: string - required: false - default: '7.2' - minimum-wp: - description: 'Minimum WP version to test against.' - type: string - required: false - default: '4.9' - with-coverage: - description: 'Include coverage tests.' - type: boolean - required: false - default: true - matrix: - description: 'Additional matrix entries to include or exclude.' - type: string - required: false - default: '{ "include": [], "exclude": [] }' - outputs: - unit: - description: 'Matrix for the unit test legs as a JSON string, or empty when the package has no PHPUnit setup or the change is documentation only.' - value: ${{ jobs.prepare.outputs.unit }} - functional: - description: 'Matrix for the Behat legs as a JSON string, or empty when the package has no Behat setup or the change is documentation only.' - value: ${{ jobs.prepare.outputs.functional }} - -permissions: - contents: read - -jobs: - prepare: - name: Prepare test matrices - runs-on: ubuntu-24.04 - timeout-minutes: 10 - outputs: - unit: ${{ steps.unit.outputs.matrix }} - functional: ${{ steps.functional.outputs.matrix }} - steps: - - name: Check out source code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - # Enough history to diff the change against its base. Other events do - # not inspect a diff and do not need it. The operands are quoted because - # an unquoted 0 is falsy, which would make `&& 0 || 1` always yield 1. - fetch-depth: ${{ ( github.event_name == 'pull_request' || github.event_name == 'push' ) && '0' || '1' }} - - # A change that only touches documentation cannot affect the test result, so - # there is no reason to spend 50 jobs on it. This is deliberately a deny list - # rather than an allow list: the reusable workflow cannot know how any given - # package lays out its source, so anything not provably irrelevant still runs - # the full suite. Every failure path below also falls back to testing. - - name: Determine whether the change is documentation only - id: docs-only - env: - EVENT_NAME: ${{ github.event_name }} - BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - # Scheduled and manually dispatched runs always test everything. - if [ "$EVENT_NAME" != 'pull_request' ] && [ "$EVENT_NAME" != 'push' ]; then - echo 'Not a pull request or push; testing everything.' - echo "value=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # A newly created branch reports an all-zero base. - if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = '0000000000000000000000000000000000000000' ]; then - echo 'No usable base commit; testing everything.' - echo "value=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if ! CHANGED=$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}" 2>/dev/null); then - echo "Could not diff ${BASE_SHA}...${HEAD_SHA}; testing everything." - echo "value=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo 'Changed files:' - printf '%s\n' "$CHANGED" - - RELEVANT=$(printf '%s\n' "$CHANGED" | grep -Ev \ - -e '\.md$' \ - -e '^\.github/ISSUE_TEMPLATE/' \ - -e '^\.github/(CODEOWNERS|FUNDING\.yml)$' \ - -e '^(LICENSE|\.editorconfig|\.gitattributes|\.gitignore)$' \ - || true) - - if [ -z "$RELEVANT" ]; then - echo 'Only documentation and repository metadata changed; skipping the test matrix.' - echo "value=true" >> "$GITHUB_OUTPUT" - else - echo 'Test-relevant files changed:' - printf '%s\n' "$RELEVANT" - echo "value=false" >> "$GITHUB_OUTPUT" - fi - - # Entries flagged with "nightly": true are the ones the test jobs run with - # `continue-on-error`. They can never gate a merge, so running them on every - # pull request spends runner time without producing a signal. They run on the - # nightly schedule and on manual dispatch instead. - - name: Build the base matrix - id: base - env: - ADDITIONAL_MATRIX: ${{ inputs.matrix }} - INCLUDE_NIGHTLY: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} - run: | - MATRIX=$(cat << 'EOF' - { - "include": [ - { - "php": "7.2", - "wp": "4.9", - "mysql": "mysql-5.6" - }, - { - "php": "7.2", - "wp": "6.9", - "mysql": "mysql-8.0" - }, - { - "php": "7.2", - "wp": "6.9", - "dbtype": "sqlite" - }, - { - "php": "7.3", - "wp": "6.9", - "mysql": "mysql-8.0" - }, - { - "php": "7.3", - "wp": "6.9", - "dbtype": "sqlite" - }, - { - "php": "7.4", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "7.4", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.0", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.0", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.1", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.1", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.2", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.2", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.3", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.3", - "wp": "latest", - "mysql": "mysql-8.4" - }, - { - "php": "8.3", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.3", - "wp": "latest", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "8.4", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.4", - "wp": "latest", - "mysql": "mysql-8.4" - }, - { - "php": "8.4", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.4", - "wp": "latest", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "8.5", - "wp": "latest", - "mysql": "mysql-8.0", - "coverage": true - }, - { - "php": "8.5", - "wp": "latest", - "mysql": "mysql-8.4" - }, - { - "php": "8.5", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.5", - "wp": "latest", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "7.4", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "7.4", - "wp": "trunk", - "mysql": "mysql-5.7" - }, - { - "php": "7.4", - "wp": "trunk", - "mysql": "mysql-5.6" - }, - { - "php": "8.0", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.0", - "wp": "trunk", - "mysql": "mysql-5.7" - }, - { - "php": "8.0", - "wp": "trunk", - "mysql": "mysql-5.6" - }, - { - "php": "8.1", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.2", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.3", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.3", - "wp": "trunk", - "mysql": "mysql-8.4" - }, - { - "php": "8.3", - "wp": "trunk", - "dbtype": "sqlite" - }, - { - "php": "8.3", - "wp": "trunk", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "8.4", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.4", - "wp": "trunk", - "mysql": "mysql-8.4" - }, - { - "php": "8.4", - "wp": "trunk", - "dbtype": "sqlite" - }, - { - "php": "8.4", - "wp": "trunk", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "8.5", - "wp": "trunk", - "mysql": "mysql-8.0" - }, - { - "php": "8.5", - "wp": "trunk", - "mysql": "mysql-8.4" - }, - { - "php": "8.5", - "wp": "trunk", - "dbtype": "sqlite" - }, - { - "php": "8.5", - "wp": "trunk", - "mysql": "mariadb-11.4", - "dbtype": "mariadb", - "nightly": true - }, - { - "php": "nightly", - "wp": "trunk", - "mysql": "mysql-8.4", - "nightly": true - }, - { - "php": "nightly", - "wp": "trunk", - "dbtype": "sqlite", - "nightly": true - }, - { - "php": "8.5", - "wp": "latest", - "dbtype": "sqlite", - "object_cache": "sqlite" - }, - { - "php": "8.5", - "wp": "trunk", - "dbtype": "sqlite", - "object_cache": "sqlite" - }, - { - "php": "8.5", - "wp": "trunk", - "dbtype": "sqlite", - "os": "macos-latest", - "nightly": true - }, - { - "php": "8.5", - "wp": "trunk", - "dbtype": "sqlite", - "os": "windows-2022", - "nightly": true - } - ] - } - EOF - ) - MERGED_MATRIX=$(printf '%s\n%s\n' "$MATRIX" "$ADDITIONAL_MATRIX" | jq -sc \ - --argjson include_nightly "$INCLUDE_NIGHTLY" ' - . as $root | - (($root[0].exclude // []) + ($root[1].exclude // [])) as $excludes | - { - include: ( - (.[0].include + .[1].include | map(if .os == null then .os = "" else . end)) | - map(. as $item | select($excludes | any(. as $rule | all($rule|keys[]; $item[.] == $rule[.])) | not)) | - unique | - - # Drop the soft-failing entries unless this is a nightly or manual run. - # Entries supplied through the `matrix` input have no `nightly` key and - # are therefore always kept. - map(select($include_nightly or (.nightly // false) == false)) - ) - } - ') - echo "matrix=${MERGED_MATRIX}" >> "$GITHUB_OUTPUT" - - - name: Check existence of composer.json & phpunit.xml.dist files - id: check_unit_files - run: echo "files_exists=$([ -f composer.json ] && [ -f phpunit.xml.dist ] && echo true || echo false)" >> "$GITHUB_OUTPUT" - - - name: Check existence of composer.json & behat.yml files - id: check_functional_files - run: echo "files_exists=$([ -f composer.json ] && [ -f behat.yml ] && echo true || echo false)" >> "$GITHUB_OUTPUT" - - - name: Set unit test matrix - id: unit - env: - BASE_MATRIX: ${{ steps.base.outputs.matrix }} - FILE_EXISTS: ${{ steps.check_unit_files.outputs.files_exists }} - DOCS_ONLY: ${{ steps.docs-only.outputs.value }} - INPUTS_MINIMUM_PHP: ${{ inputs.minimum-php }} - INPUTS_MINIMUM_WP: ${{ inputs.minimum-wp }} - WITH_COVERAGE: ${{ inputs.with-coverage }} - run: | - if [[ $FILE_EXISTS == 'true' && $DOCS_ONLY != 'true' ]]; then - echo "matrix=$(jq -c \ - --argjson with_coverage_flag "${WITH_COVERAGE}" \ - --arg minimum_php "${INPUTS_MINIMUM_PHP}" \ - --arg minimum_wp "${INPUTS_MINIMUM_WP}" \ - ' - .include |= ( - map( - # First, select only the versions that meet all minimum requirements - select( - (.php >= $minimum_php) and - (.wp == "latest" or .wp >= $minimum_wp) - ) | - - # Next, update the coverage flag on the remaining items - if $with_coverage_flag == false and .coverage == true then - .coverage = false - else - . - end - ) | - - # Finally, get the unique entries - unique_by([.php, .os]) - ) - ' <<< "$BASE_MATRIX")" >> "$GITHUB_OUTPUT" - else - echo "matrix=" >> "$GITHUB_OUTPUT" - fi - - - name: Set functional test matrix - id: functional - env: - BASE_MATRIX: ${{ steps.base.outputs.matrix }} - FILE_EXISTS: ${{ steps.check_functional_files.outputs.files_exists }} - DOCS_ONLY: ${{ steps.docs-only.outputs.value }} - INPUTS_MINIMUM_PHP: ${{ inputs.minimum-php }} - INPUTS_MINIMUM_WP: ${{ inputs.minimum-wp }} - WITH_COVERAGE: ${{ inputs.with-coverage }} - run: | - if [[ $FILE_EXISTS == 'true' && $DOCS_ONLY != 'true' ]]; then - echo "matrix=$(jq -c \ - --argjson with_coverage_flag "${WITH_COVERAGE}" \ - --arg minimum_php "${INPUTS_MINIMUM_PHP}" \ - --arg minimum_wp "${INPUTS_MINIMUM_WP}" \ - ' - # First, select only the versions that meet all minimum requirements - .include |= ( - map( - select( - .php >= $minimum_php - ) | - # Next, update the coverage flag on the remaining items - if $with_coverage_flag == false and .coverage == true then - .coverage = false - else - . - end - ) - ) | - - # Reassign WP4.9 to minimum_wp - .include |= ( - map( - select( - .wp == "4.9" - ).wp |= $minimum_wp - ) - ) - ' <<< "$BASE_MATRIX" )" >> "$GITHUB_OUTPUT" - else - echo "matrix=" >> "$GITHUB_OUTPUT" - fi diff --git a/.github/workflows/reusable-testing.yml b/.github/workflows/reusable-testing.yml index 8caa474..339f994 100644 --- a/.github/workflows/reusable-testing.yml +++ b/.github/workflows/reusable-testing.yml @@ -37,16 +37,478 @@ concurrency: cancel-in-progress: true jobs: - # The matrix computation lives in its own reusable workflow so a caller can - # invoke it directly and fan out from its own top-level jobs, which is what - # makes the legs group by PHP version in the run view. See the README. prepare: - uses: ./.github/workflows/reusable-prepare-matrix.yml - with: - minimum-php: ${{ inputs.minimum-php }} - minimum-wp: ${{ inputs.minimum-wp }} - with-coverage: ${{ inputs.with-coverage }} - matrix: ${{ inputs.matrix }} + name: Prepare test matrices + runs-on: ubuntu-22.04 + timeout-minutes: 10 + outputs: + unit: ${{ steps.unit.outputs.matrix }} + functional: ${{ steps.functional.outputs.matrix }} + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + # Enough history to diff the change against its base. Other events do + # not inspect a diff and do not need it. The operands are quoted because + # an unquoted 0 is falsy, which would make `&& 0 || 1` always yield 1. + fetch-depth: ${{ ( github.event_name == 'pull_request' || github.event_name == 'push' ) && '0' || '1' }} + + # A change that only touches documentation cannot affect the test result, so + # there is no reason to spend 50 jobs on it. This is deliberately a deny list + # rather than an allow list: the reusable workflow cannot know how any given + # package lays out its source, so anything not provably irrelevant still runs + # the full suite. Every failure path below also falls back to testing. + - name: Determine whether the change is documentation only + id: docs-only + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + # Scheduled and manually dispatched runs always test everything. + if [ "$EVENT_NAME" != 'pull_request' ] && [ "$EVENT_NAME" != 'push' ]; then + echo 'Not a pull request or push; testing everything.' + echo "value=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # A newly created branch reports an all-zero base. + if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = '0000000000000000000000000000000000000000' ]; then + echo 'No usable base commit; testing everything.' + echo "value=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! CHANGED=$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}" 2>/dev/null); then + echo "Could not diff ${BASE_SHA}...${HEAD_SHA}; testing everything." + echo "value=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo 'Changed files:' + printf '%s\n' "$CHANGED" + + RELEVANT=$(printf '%s\n' "$CHANGED" | grep -Ev \ + -e '\.md$' \ + -e '^\.github/ISSUE_TEMPLATE/' \ + -e '^\.github/(CODEOWNERS|FUNDING\.yml)$' \ + -e '^(LICENSE|\.editorconfig|\.gitattributes|\.gitignore)$' \ + || true) + + if [ -z "$RELEVANT" ]; then + echo 'Only documentation and repository metadata changed; skipping the test matrix.' + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo 'Test-relevant files changed:' + printf '%s\n' "$RELEVANT" + echo "value=false" >> "$GITHUB_OUTPUT" + fi + + # Entries flagged with "nightly": true are the ones the test jobs run with + # `continue-on-error`. They can never gate a merge, so running them on every + # pull request spends runner time without producing a signal. They run on the + # nightly schedule and on manual dispatch instead. + - name: Build the base matrix + id: base + env: + ADDITIONAL_MATRIX: ${{ inputs.matrix }} + INCLUDE_NIGHTLY: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + run: | + MATRIX=$(cat << 'EOF' + { + "include": [ + { + "php": "7.2", + "wp": "4.9", + "mysql": "mysql-5.6" + }, + { + "php": "7.2", + "wp": "6.9", + "mysql": "mysql-8.0" + }, + { + "php": "7.2", + "wp": "6.9", + "dbtype": "sqlite" + }, + { + "php": "7.3", + "wp": "6.9", + "mysql": "mysql-8.0" + }, + { + "php": "7.3", + "wp": "6.9", + "dbtype": "sqlite" + }, + { + "php": "7.4", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "7.4", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.0", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.0", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.1", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.1", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.2", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.2", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.3", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.3", + "wp": "latest", + "mysql": "mysql-8.4" + }, + { + "php": "8.3", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.3", + "wp": "latest", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.4" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "8.5", + "wp": "latest", + "mysql": "mysql-8.0", + "coverage": true + }, + { + "php": "8.5", + "wp": "latest", + "mysql": "mysql-8.4" + }, + { + "php": "8.5", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.5", + "wp": "latest", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "7.4", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "7.4", + "wp": "trunk", + "mysql": "mysql-5.7" + }, + { + "php": "7.4", + "wp": "trunk", + "mysql": "mysql-5.6" + }, + { + "php": "8.0", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.0", + "wp": "trunk", + "mysql": "mysql-5.7" + }, + { + "php": "8.0", + "wp": "trunk", + "mysql": "mysql-5.6" + }, + { + "php": "8.1", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.2", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.3", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.3", + "wp": "trunk", + "mysql": "mysql-8.4" + }, + { + "php": "8.3", + "wp": "trunk", + "dbtype": "sqlite" + }, + { + "php": "8.3", + "wp": "trunk", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "8.4", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.4", + "wp": "trunk", + "mysql": "mysql-8.4" + }, + { + "php": "8.4", + "wp": "trunk", + "dbtype": "sqlite" + }, + { + "php": "8.4", + "wp": "trunk", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "8.5", + "wp": "trunk", + "mysql": "mysql-8.0" + }, + { + "php": "8.5", + "wp": "trunk", + "mysql": "mysql-8.4" + }, + { + "php": "8.5", + "wp": "trunk", + "dbtype": "sqlite" + }, + { + "php": "8.5", + "wp": "trunk", + "mysql": "mariadb-11.4", + "dbtype": "mariadb", + "nightly": true + }, + { + "php": "nightly", + "wp": "trunk", + "mysql": "mysql-8.4", + "nightly": true + }, + { + "php": "nightly", + "wp": "trunk", + "dbtype": "sqlite", + "nightly": true + }, + { + "php": "8.5", + "wp": "latest", + "dbtype": "sqlite", + "object_cache": "sqlite" + }, + { + "php": "8.5", + "wp": "trunk", + "dbtype": "sqlite", + "object_cache": "sqlite" + }, + { + "php": "8.5", + "wp": "trunk", + "dbtype": "sqlite", + "os": "macos-latest", + "nightly": true + }, + { + "php": "8.5", + "wp": "trunk", + "dbtype": "sqlite", + "os": "windows-2022", + "nightly": true + } + ] + } + EOF + ) + MERGED_MATRIX=$(printf '%s\n%s\n' "$MATRIX" "$ADDITIONAL_MATRIX" | jq -sc \ + --argjson include_nightly "$INCLUDE_NIGHTLY" ' + . as $root | + (($root[0].exclude // []) + ($root[1].exclude // [])) as $excludes | + { + include: ( + (.[0].include + .[1].include | map(if .os == null then .os = "" else . end)) | + map(. as $item | select($excludes | any(. as $rule | all($rule|keys[]; $item[.] == $rule[.])) | not)) | + unique | + + # Drop the soft-failing entries unless this is a nightly or manual run. + # Entries supplied through the `matrix` input have no `nightly` key and + # are therefore always kept. + map(select($include_nightly or (.nightly // false) == false)) + ) + } + ') + echo "matrix=${MERGED_MATRIX}" >> "$GITHUB_OUTPUT" + + - name: Check existence of composer.json & phpunit.xml.dist files + id: check_unit_files + run: echo "files_exists=$([ -f composer.json ] && [ -f phpunit.xml.dist ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + + - name: Check existence of composer.json & behat.yml files + id: check_functional_files + run: echo "files_exists=$([ -f composer.json ] && [ -f behat.yml ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + + - name: Set unit test matrix + id: unit + env: + BASE_MATRIX: ${{ steps.base.outputs.matrix }} + FILE_EXISTS: ${{ steps.check_unit_files.outputs.files_exists }} + DOCS_ONLY: ${{ steps.docs-only.outputs.value }} + INPUTS_MINIMUM_PHP: ${{ inputs.minimum-php }} + INPUTS_MINIMUM_WP: ${{ inputs.minimum-wp }} + WITH_COVERAGE: ${{ inputs.with-coverage }} + run: | + if [[ $FILE_EXISTS == 'true' && $DOCS_ONLY != 'true' ]]; then + echo "matrix=$(jq -c \ + --argjson with_coverage_flag "${WITH_COVERAGE}" \ + --arg minimum_php "${INPUTS_MINIMUM_PHP}" \ + --arg minimum_wp "${INPUTS_MINIMUM_WP}" \ + ' + .include |= ( + map( + # First, select only the versions that meet all minimum requirements + select( + (.php >= $minimum_php) and + (.wp == "latest" or .wp >= $minimum_wp) + ) | + + # Next, update the coverage flag on the remaining items + if $with_coverage_flag == false and .coverage == true then + .coverage = false + else + . + end + ) | + + # Finally, get the unique entries + unique_by([.php, .os]) + ) + ' <<< "$BASE_MATRIX")" >> "$GITHUB_OUTPUT" + else + echo "matrix=" >> "$GITHUB_OUTPUT" + fi + + - name: Set functional test matrix + id: functional + env: + BASE_MATRIX: ${{ steps.base.outputs.matrix }} + FILE_EXISTS: ${{ steps.check_functional_files.outputs.files_exists }} + DOCS_ONLY: ${{ steps.docs-only.outputs.value }} + INPUTS_MINIMUM_PHP: ${{ inputs.minimum-php }} + INPUTS_MINIMUM_WP: ${{ inputs.minimum-wp }} + WITH_COVERAGE: ${{ inputs.with-coverage }} + run: | + if [[ $FILE_EXISTS == 'true' && $DOCS_ONLY != 'true' ]]; then + echo "matrix=$(jq -c \ + --argjson with_coverage_flag "${WITH_COVERAGE}" \ + --arg minimum_php "${INPUTS_MINIMUM_PHP}" \ + --arg minimum_wp "${INPUTS_MINIMUM_WP}" \ + ' + # First, select only the versions that meet all minimum requirements + .include |= ( + map( + select( + .php >= $minimum_php + ) | + # Next, update the coverage flag on the remaining items + if $with_coverage_flag == false and .coverage == true then + .coverage = false + else + . + end + ) + ) | + + # Reassign WP4.9 to minimum_wp + .include |= ( + map( + select( + .wp == "4.9" + ).wp |= $minimum_wp + ) + ) + ' <<< "$BASE_MATRIX" )" >> "$GITHUB_OUTPUT" + else + echo "matrix=" >> "$GITHUB_OUTPUT" + fi unit: needs: prepare diff --git a/.github/workflows/reusable-unit.yml b/.github/workflows/reusable-unit.yml index 565cd18..6bcb8f7 100644 --- a/.github/workflows/reusable-unit.yml +++ b/.github/workflows/reusable-unit.yml @@ -10,31 +10,24 @@ on: required: false default: false os: - description: 'Runner to use. Defaults to ubuntu-24.04, or the RUNNERS_NAME repository variable when set.' + description: 'Runner to use. Defaults to ubuntu-22.04, or the RUNNERS_NAME repository variable when set.' type: string required: false default: '' - grouped: - description: 'Set when the calling job name already states the suite and PHP version, so this workflow leaves them out instead of repeating them inside the group.' - type: boolean - required: false - default: false permissions: contents: read jobs: unit: - # See the note on the equivalent name in reusable-functional.yml: `grouped` - # picks between a name that stands on its own and one that sits inside a group - # header which already states the suite and PHP version. A grouped unit leg is - # distinguished only by its runner and whether it collects coverage, so those - # suffixes carry the whole name. - name: ${{ !inputs.grouped && format('Unit | PHP {0}', inputs.php) || 'PHPUnit' }}${{ inputs.coverage && ' (with coverage)' || '' }}${{ startsWith( inputs.os, 'windows' ) && ' (Windows)' || '' }}${{ startsWith( inputs.os, 'macos' ) && ' (macOS)' || '' }} + # This name has to stand on its own. The run view labels a nested job with its + # own name only; the calling job's name is not surfaced at this depth, so + # anything omitted here is not shown anywhere. + name: Unit | PHP ${{ inputs.php }}${{ inputs.coverage && ' (with coverage)' || '' }}${{ startsWith( inputs.os, 'windows' ) && ' (Windows)' || '' }}${{ startsWith( inputs.os, 'macos' ) && ' (macOS)' || '' }} # Repositories can point the default Linux legs at a larger runner by setting # the `RUNNERS_NAME` repository variable, without having to fork this workflow. # Explicit macOS/Windows legs are unaffected. - runs-on: ${{ inputs.os || vars.RUNNERS_NAME || 'ubuntu-24.04' }} + runs-on: ${{ inputs.os || vars.RUNNERS_NAME || 'ubuntu-22.04' }} continue-on-error: ${{ inputs.php == 'nightly' }} timeout-minutes: ${{ inputs.coverage && 30 || 15 }} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f917c6e..35d7562 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,63 +13,6 @@ on: permissions: contents: read -# Lives here rather than in the called workflow, because the fan-out below is -# owned by this file. -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - -# This repository uses the fanned-out form as the reference implementation for -# the rest of the organisation, and refers to the workflows by local path so that -# a pull request here exercises its own changes rather than whatever is on main. -# Packages adopting this shape use `wp-cli/.github/.github/workflows/...@main`. -# See the README for what this buys and what it costs. jobs: - prepare: - name: Prepare test matrices - uses: ./.github/workflows/reusable-prepare-matrix.yml - - unit: - needs: prepare - if: ${{ needs.prepare.outputs.unit != '' }} - # A matrix only becomes a collapsible group in the run view when it sits on a - # job declared in the workflow the run belongs to, which is why this fan-out - # is here and not one level down. - # - # The run view groups legs whose calling job name is identical, so this name - # must vary by the dimension being grouped on and collide across every other. - # A name that does not vary with the matrix at all does not produce one group; - # GitHub appends the matrix combination to disambiguate it, giving one group - # per leg labelled "Unit (8.5, latest, mysql-8.0)". - name: Unit | PHP ${{ matrix.php }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.prepare.outputs.unit) }} - uses: ./.github/workflows/reusable-unit.yml - secrets: inherit - with: - php: ${{ matrix.php }} - coverage: ${{ matrix.coverage == true }} - os: ${{ matrix.os || '' }} - # The job name above already states the suite and PHP version. - grouped: true - - functional: - needs: prepare - if: ${{ needs.prepare.outputs.functional != '' }} - name: Behat | PHP ${{ matrix.php }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.prepare.outputs.functional) }} - uses: ./.github/workflows/reusable-functional.yml - secrets: inherit - with: - php: ${{ matrix.php }} - wp: ${{ matrix.wp }} - dbtype: ${{ matrix.dbtype || 'mysql' }} - mysql: ${{ matrix.mysql || '' }} - object_cache: ${{ matrix.object_cache }} - coverage: ${{ matrix.coverage == true }} - os: ${{ matrix.os || '' }} - # The job name above already states the suite and PHP version. - grouped: true + test: + uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main diff --git a/README.md b/README.md index e2142b4..2ebbd65 100644 --- a/README.md +++ b/README.md @@ -19,72 +19,6 @@ This repository contains reusable GitHub Actions workflows that are automaticall - **Regenerate README** (`regenerate-readme.yml`) - Automatically regenerates README.md files from source - **Check Branch Alias** (`check-branch-alias.yml`) - Monitors and updates Composer branch-alias configuration -#### Test workflows - -A package's `testing.yml` can call the test workflows in one of two shapes. - -The **wrapped** shape is the default and needs three lines: - -```yaml -jobs: - test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main - with: - minimum-php: '8.0' -``` - -The **fanned-out** shape calls `reusable-prepare-matrix.yml` for the matrix and -runs the legs from the package's own jobs. See this repository's own -`testing.yml` for a complete example. - -The only thing it buys is presentation: a matrix becomes a collapsible group in -the Actions run view only when it sits on a job declared in the workflow the run -belongs to. In the wrapped shape the matrix is one level down and is not -surfaced, so all fifty legs appear as one flat list. Fanning out puts the matrix -at the top level, so the run view collapses to one entry per PHP version. - -It costs about thirty lines in a file that is not synced, so every future change -to the fan-out has to be repeated in each package that adopts it. Prefer the -wrapped shape unless a package has a large enough matrix that the flat list is -genuinely hard to read. - -A fanned-out caller should pass `grouped: true` to `reusable-unit.yml` and -`reusable-functional.yml`, so their job names leave out what the group header -already states. Without it the legs read `Behat | PHP 8.5 | WP latest | SQLite` -inside a group already called `Behat | PHP 8.5`. - -The run view groups legs whose calling job name is identical, so that name must -vary by the dimension being grouped on and collide across every other one. Group -both suites by PHP version: - -``` -Behat | PHP 8.5 <- the calling job - WP latest | mysql-8.0 <- the called workflow, with grouped: true - WP trunk | SQLite - -Unit | PHP 8.5 - PHPUnit (with coverage) - PHPUnit (macOS) - PHPUnit (Windows) -``` - -A name that does not vary with the matrix does not collapse the legs into one -group. GitHub appends the matrix combination to disambiguate it, so `name: Unit` -produces one group per leg, labelled `Unit (8.5, latest, mysql-8.0)`. - -Some groups will hold a single leg — on a pull request there is one unit leg per -PHP version, because the macOS and Windows entries only run on the nightly -schedule. That is expected and still reads better than the alternative. - -Wrapped callers must leave `grouped` at its default of `false`, because there is -no group header for them and the name has to stand on its own. - -The one thing to watch is that with `grouped: true` a leg name is only unique -within its group — `WP latest | SQLite` occurs under every PHP version. That is -fine wherever the group header is part of the name, which is how the run view and -the checks list both render it. If a leg ever shows up somewhere without its -group, set `grouped: false` for that caller and the full name comes back. - #### Branch Alias Checker The branch alias checker workflow automatically ensures that the Composer `branch-alias` in each repository's `composer.json` is up-to-date. It: