From fbbca9c3e06da68cbc3a5c6b9caac109d1fe2ff8 Mon Sep 17 00:00:00 2001 From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:24:20 -0700 Subject: [PATCH] Refactored Linting & Unit Testing Workflows (#40572) --- .../setup-woocommerce-monorepo/action.yml | 9 +- .github/workflows/ci.yml | 184 ++-- .github/workflows/pr-code-sniff.yml | 51 - .github/workflows/pr-lint-js.yml | 54 - .github/workflows/pr-test-js.yml | 40 - .../workflows/pr-unit-test-other-plugins.yml | 24 - .github/workflows/pr-unit-tests.yml | 89 -- .github/workflows/prime-cache.yml | 24 - .github/workflows/scripts/build-ci-matrix.js | 972 ++++++++++++++++++ .github/workflows/scripts/parse-wp-version.js | 89 -- plugins/woocommerce/bin/lint-branch.sh | 10 +- .../changelog/refactor-unit-test-workflow | 4 + plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/package.json | 32 +- 14 files changed, 1117 insertions(+), 467 deletions(-) delete mode 100644 .github/workflows/pr-code-sniff.yml delete mode 100644 .github/workflows/pr-lint-js.yml delete mode 100644 .github/workflows/pr-test-js.yml delete mode 100644 .github/workflows/pr-unit-test-other-plugins.yml delete mode 100644 .github/workflows/pr-unit-tests.yml delete mode 100644 .github/workflows/prime-cache.yml create mode 100644 .github/workflows/scripts/build-ci-matrix.js delete mode 100644 .github/workflows/scripts/parse-wp-version.js create mode 100644 plugins/woocommerce/changelog/refactor-unit-test-workflow diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index c1b2a144998..245324bfe59 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -3,6 +3,9 @@ description: Handles the installation, building, and caching of the projects wit permissions: {} inputs: + install: + description: Indicates whether or not the action should install any projects. + default: 'true' install-filters: description: The PNPM filter used to decide what projects to install. Supports multiline strings for multiple filters. default: '' @@ -54,6 +57,9 @@ runs: - name: Install Node and PHP Dependencies shell: bash + if: ${{ inputs.install == 'true' }} + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' run: | pnpm -w install turbo pnpm install ${{ steps.parse-input.outputs.INSTALL_FILTERS }} @@ -70,6 +76,7 @@ runs: fi - name: Cache Build Output + if: ${{ inputs.build == 'true' }} uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: .turbo @@ -79,6 +86,6 @@ runs: ${{ runner.os }}-build-output - name: Build - if: ${{ inputs.build == 'true' }} + if: ${{ inputs.install == 'true' && inputs.build == 'true' }} shell: bash run: pnpm -w exec turbo run turbo:build --cache-dir=".turbo" ${{ steps.parse-input.outputs.BUILD_FILTERS }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3db2b93bc05..1bea9ff1cc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,90 +1,98 @@ -name: Run CI -on: - push: - branches: - - trunk - - 'release/**' - workflow_dispatch: - -defaults: - run: - shell: bash +name: 'CI' +on: + pull_request: + push: + branches: + - 'trunk' + - 'release/*' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true jobs: - test: - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} - timeout-minutes: 30 - runs-on: ubuntu-20.04 - permissions: - contents: read - continue-on-error: ${{ matrix.wp == 'nightly' }} - strategy: - fail-fast: false - matrix: - php: ['7.4', '8.0'] - wp: ['latest'] - unittests: ['shard1', 'shard2'] - include: - - wp: nightly - php: '7.4' - unittests: 'shard1' - - wp: nightly - php: '7.4' - unittests: 'shard2' - - wp: '6.1' - php: 7.4 - unittests: 'shard1' - - wp: '6.1' - php: 7.4 - unittests: 'shard2' - services: - database: - image: mysql:5.6 - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build-filters: woocommerce - - - name: Tool versions - run: | - php --version - composer --version - - - name: Build Admin feature config - working-directory: plugins/woocommerce - run: pnpm run build:feature-config - - - id: parseMatrix - name: Parse Matrix Variables - uses: actions/github-script@v6 - with: - script: | - const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' ); - parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => { - core.setOutput( 'wpVersion', version ); - } ); - - - name: Prepare Testing Environment - env: - WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }} - WP_ENV_PHP_VERSION: ${{ matrix.php }} - run: pnpm --filter=woocommerce env:test - - - name: Run Tests - env: - WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }} - WP_ENV_PHP_VERSION: ${{ matrix.php }} - run: pnpm --filter=woocommerce test:unit:env --testsuite ${{ matrix.unittests }} + project-matrix: + # Since this is a monorepo, not every pull request or change is going to impact every project. + # Instead of running CI tasks on all projects indiscriminately, we use a script to detect + # which projects have changed and what kind of change occurred. This lets us build a + # matrix that we can use to run CI tasks only on the projects that need them. + name: 'Build Project Matrix' + runs-on: 'ubuntu-20.04' + outputs: + matrix: ${{ steps.project-matrix.outputs.matrix }} + steps: + - uses: 'actions/checkout@v3' + name: 'Checkout' + with: + fetch-depth: 0 + - uses: './.github/actions/setup-woocommerce-monorepo' + name: 'Setup Monorepo' + with: + install: false + - uses: actions/github-script@v6 + id: 'project-matrix' + name: 'Build Matrix' + with: + script: | + let baseRef = ${{ toJson( github.base_ref ) }}; + if ( baseRef ) { + baseRef = 'origin/' + baseRef; + } + const buildCIMatrix = require( './.github/workflows/scripts/build-ci-matrix' ); + core.setOutput( 'matrix', JSON.stringify( await buildCIMatrix( baseRef ) ) ); + project-task-matrix: + # This is the actual CI job that will be ran against every project with applicable changes. + # Note that we only run the tasks that have commands set. Our script will set them if + # they are needed and so all the workflow needs to do is run them. + name: '${{ matrix.projectName }} - ${{ matrix.taskName }}' # Note: GitHub doesn't process expressions for skipped jobs so when there's no matrix the name will literally be this. + runs-on: 'ubuntu-20.04' + needs: 'project-matrix' + if: ${{ needs.project-matrix.outputs.matrix != '[]' }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON( needs.project-matrix.outputs.matrix ) }} + steps: + - uses: 'actions/checkout@v3' + name: 'Checkout' + with: + fetch-depth: 0 + - uses: './.github/actions/setup-woocommerce-monorepo' + id: 'setup-monorepo' + name: 'Setup Monorepo' + with: + # install-filters: '${{ matrix.projectName }}...' + build-filters: '${{ matrix.projectName }}' + - name: 'Lint' + if: ${{ !cancelled() && matrix.lintCommand && steps.setup-monorepo.conclusion == 'success' }} + run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.lintCommand }}' + - name: 'Prepare Test Environment' + id: 'prepare-test-environment' + if: ${{ !cancelled() && matrix.testEnvCommand && steps.setup-monorepo.conclusion == 'success' }} + env: ${{ matrix.testEnvVars }} + run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnvCommand }}' + - name: 'Test - JS' + if: ${{ !cancelled() && matrix.jsTestCommand && steps.setup-monorepo.conclusion == 'success' && ( ! matrix.testEnvCommand || steps.prepare-test-environment.conclusion == 'success' ) }} + run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.jsTestCommand }}' + - name: 'Test - PHP' + if: ${{ !cancelled() && matrix.phpTestCommand && steps.setup-monorepo.conclusion == 'success' && ( ! matrix.testEnvCommand || steps.prepare-test-environment.conclusion == 'success' ) }} + run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.phpTestCommand }}' + project-task-matrix-evaluation: + # In order to add a required status check we need a consistent job that we can grab onto. + # Since we are dynamically generating a project matrix, however, we can't rely on + # on any specific job being present. We can get around this limitation by using + # a job that runs after all the others and either passes or fails based on the + # results of the other jobs in the workflow. + name: 'Evaluate Project Matrix' + runs-on: 'ubuntu-20.04' + needs: 'project-task-matrix' + if: ${{ always() }} + steps: + - name: 'Check Matrix Success' + run: | + result="${{ needs.project-task-matrix.result }}" + if [[ $result == "success" || $result == "skipped" ]]; then + echo "The matrix has completed successfully." + exit 0 + else + echo "One or more jobs in the matrix has failed." + exit 1 + fi diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml deleted file mode 100644 index 30c690db906..00000000000 --- a/.github/workflows/pr-code-sniff.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Run code sniff on PR -on: - pull_request: - paths-ignore: - - '**/changelog/**' -defaults: - run: - shell: bash -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - test: - name: Code sniff (PHP 7.4, WP Latest) - timeout-minutes: 15 - runs-on: ubuntu-20.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get Changed Files - id: changed-files - uses: tj-actions/changed-files@v39 - with: - path: plugins/woocommerce - files: "**/*.php" - - - name: Setup WooCommerce Monorepo - if: steps.changed-files.outputs.any_changed == 'true' - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build: false - - - name: Tool versions - if: steps.changed-files.outputs.any_changed == 'true' - working-directory: plugins/woocommerce - run: | - php --version - composer --version - phpcs-changed --version - - - name: Run PHPCS - if: steps.changed-files.outputs.any_changed == 'true' - working-directory: plugins/woocommerce - run: phpcs-changed -s --git --git-base ${{ github.event.pull_request.base.sha }} ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/pr-lint-js.yml b/.github/workflows/pr-lint-js.yml deleted file mode 100644 index 0482a9d8cbf..00000000000 --- a/.github/workflows/pr-lint-js.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Lint packages - -on: - pull_request: - paths-ignore: - - '**/changelog/**' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - lint-test-js: - name: Lint - runs-on: ubuntu-20.04 - permissions: - contents: read - # This is required to allow the action to annotate the PR with the linting results. - checks: write - pull-requests: read - steps: - - uses: actions/checkout@v3 - - - name: Setup PNPM - uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd - with: - version: '8.6.7' - - - name: Setup Node - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c - with: - node-version-file: .nvmrc - cache: pnpm - registry-url: 'https://registry.npmjs.org' - - - name: Install prerequisites - run: | - # ignore scripts is faster, and postinstall should not be needed for lint. - pnpm install --ignore-scripts - - - name: Lint JS and CSS - run: pnpm run -r --filter='release-posts' --filter='woocommerce/client/admin...' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' --color lint - - - name: Collect and Combine Eslint Reports - if: ${{ github.event.pull_request.head.repo.fork != true && always() }} - run: node ./.github/workflows/scripts/collect-eslint-reports.js - - - name: Annotate Code Linting Results - uses: ataylorme/eslint-annotate-action@a1bf7cb320a18aa53cb848a267ce9b7417221526 - if: ${{ github.event.pull_request.head.repo.fork != true && always() }} - with: - repo-token: '${{ secrets.GITHUB_TOKEN }}' - report-json: 'combined_eslint_report.json' diff --git a/.github/workflows/pr-test-js.yml b/.github/workflows/pr-test-js.yml deleted file mode 100644 index ca7c642a7d5..00000000000 --- a/.github/workflows/pr-test-js.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Run tests for JS packages and woocommerce-admin/client - -on: - pull_request: - paths-ignore: - - '**/changelog/**' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - test-js: - name: Run JS Tests - runs-on: ubuntu-20.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v3 - - - name: Setup PNPM - uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd - with: - version: '8.6.7' - - - name: Setup Node - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c - with: - node-version-file: .nvmrc - cache: pnpm - registry-url: 'https://registry.npmjs.org' - - - name: Install prerequisites - run: | - # ignore scripts is faster, and postinstall should not be needed for tests. - pnpm install --ignore-scripts - - - name: Test - run: pnpm run test --filter='woocommerce/client/admin...' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/e2e*' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/api' --color diff --git a/.github/workflows/pr-unit-test-other-plugins.yml b/.github/workflows/pr-unit-test-other-plugins.yml deleted file mode 100644 index b9666c7c9d5..00000000000 --- a/.github/workflows/pr-unit-test-other-plugins.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Run unit for other PHP plugins -on: - pull_request: - paths-ignore: - - '**/changelog/**' - -permissions: {} - -jobs: - test: - runs-on: ubuntu-20.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - php-version: '8.0' - - - name: Run WooCommerce Docs Tests - run: pnpm test:unit - working-directory: ./plugins/woocommerce-docs diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml deleted file mode 100644 index 31b9d88e02c..00000000000 --- a/.github/workflows/pr-unit-tests.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Run unit tests on PR -on: - pull_request: - paths-ignore: - - '**/changelog/**' -defaults: - run: - shell: bash -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - test: - if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }} - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} ${{ matrix.hpos && 'HPOS' || '' }} - timeout-minutes: 30 - runs-on: ubuntu-20.04 - permissions: - contents: read - continue-on-error: ${{ matrix.wp == 'nightly' }} - env: - HPOS: ${{ matrix.hpos }} - strategy: - fail-fast: false - matrix: - php: ['7.4', '8.0'] - wp: ['latest'] - unittests: ['shard1', 'shard2'] - include: - - wp: nightly - php: '7.4' - unittests: 'shard1' - - wp: nightly - php: '7.4' - unittests: 'shard2' - - wp: '6.1' - php: 7.4 - unittests: 'shard1' - - wp: '6.1' - php: 7.4 - unittests: 'shard2' - - wp: 'latest' - php: '7.4' - hpos: true - unittests: 'shard1' - - wp: 'latest' - php: '7.4' - hpos: true - unittests: 'shard2' - services: - database: - image: mysql:5.6 - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - php-version: ${{ matrix.php }} - - - id: parseMatrix - name: Parse Matrix Variables - uses: actions/github-script@v6 - with: - script: | - const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' ); - parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => { - core.setOutput( 'wpVersion', version ); - } ); - - - name: Prepare Testing Environment - env: - WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }} - WP_ENV_PHP_VERSION: ${{ matrix.php }} - run: pnpm --filter=woocommerce env:test - - - name: Run Tests - env: - WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }} - WP_ENV_PHP_VERSION: ${{ matrix.php }} - run: pnpm --filter=woocommerce test:unit:env --testsuite ${{ matrix.unittests }} diff --git a/.github/workflows/prime-cache.yml b/.github/workflows/prime-cache.yml deleted file mode 100644 index a5d604a1625..00000000000 --- a/.github/workflows/prime-cache.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Prime caches against trunk -on: - push: - branches: - - trunk - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - prime: - name: Prime cache - runs-on: ubuntu-20.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo diff --git a/.github/workflows/scripts/build-ci-matrix.js b/.github/workflows/scripts/build-ci-matrix.js new file mode 100644 index 00000000000..c5ee41a794d --- /dev/null +++ b/.github/workflows/scripts/build-ci-matrix.js @@ -0,0 +1,972 @@ +/** + * External dependencies. + */ +const child_process = require( 'child_process' ); +const fs = require( 'fs' ); +const https = require( 'http' ); + +/** + * Uses the WordPress API to get the downlod URL to the latest version of an X.X version line. This + * also accepts "latest-X" to get an offset from the latest version of WordPress. + * + * @param {string} wpVersion The version of WordPress to look for. + * @return {Promise.} The precise WP version download URL. + */ +async function getPreciseWPVersionURL( wpVersion ) { + return new Promise( ( resolve, reject ) => { + // We're going to use the WordPress.org API to get information about available versions of WordPress. + const request = https.get( + 'http://api.wordpress.org/core/stable-check/1.0/', + ( response ) => { + // Listen for the response data. + let responseData = ''; + response.on( 'data', ( chunk ) => { + responseData += chunk; + } ); + + // Once we have the entire response we can process it. + response.on( 'end', () => + resolve( JSON.parse( responseData ) ) + ); + } + ); + + request.on( 'error', ( error ) => { + reject( error ); + } ); + } ).then( ( allVersions ) => { + // Note: allVersions is an object where the keys are the version and the value is information about the version's status. + + // If we're requesting a "latest" offset then we need to figure out what version line we're offsetting from. + const latestSubMatch = wpVersion.match( /^latest(?:-([0-9]+))?$/i ); + if ( latestSubMatch ) { + for ( const version in allVersions ) { + if ( allVersions[ version ] !== 'latest' ) { + continue; + } + + // We don't care about the patch version because we will + // the latest version from the version line below. + const versionParts = version.match( /^([0-9]+)\.([0-9]+)/ ); + + // We're going to subtract the offset to figure out the right version. + let offset = parseInt( latestSubMatch[ 1 ] ?? 0, 10 ); + let majorVersion = parseInt( versionParts[ 1 ], 10 ); + let minorVersion = parseInt( versionParts[ 2 ], 10 ); + while ( offset > 0 ) { + minorVersion--; + if ( minorVersion < 0 ) { + majorVersion--; + minorVersion = 9; + } + offset--; + } + + // Set the version that we found in the offset. + wpVersion = majorVersion + '.' + minorVersion; + } + } + + // Scan through all of the versions to find the latest version in the version line. + let latestVersion = null; + let latestPatch = -1; + for ( const v in allVersions ) { + // Parse the version so we can make sure we're looking for the latest. + const matches = v.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ ); + + // We only care about the correct minor version. + const minor = `${ matches[ 1 ] }.${ matches[ 2 ] }`; + if ( minor !== wpVersion ) { + continue; + } + + // Track the latest version in the line. + const patch = + matches[ 3 ] === undefined ? 0 : parseInt( matches[ 3 ], 10 ); + + if ( patch > latestPatch ) { + latestPatch = patch; + latestVersion = v; + } + } + + if ( ! latestVersion ) { + throw new Error( + `Unable to find latest version for version line ${ wpVersion }.` + ); + } + + return `https://wordpress.org/wordpress-${ latestVersion }.zip`; + } ); +} + +/** + * Parses a display-friendly WordPress version and returns a link to download the given version. + * + * @param {string} wpVersion A display-friendly WordPress version. Supports ("master", "trunk", "nightly", "latest", "latest-X", "X.X" for version lines, and "X.X.X" for specific versions) + * @return {Promise.} A link to download the given version of WordPress. + */ +async function parseWPVersion( wpVersion ) { + // Allow for download URLs in place of a version. + if ( wpVersion.match( /[a-z]+:\/\//i ) ) { + return wpVersion; + } + + // Start with versions we can infer immediately. + switch ( wpVersion ) { + case 'master': + case 'trunk': { + return 'WordPress/WordPress#master'; + } + + case 'nightly': { + return 'https://wordpress.org/nightly-builds/wordpress-latest.zip'; + } + + case 'latest': { + return 'https://wordpress.org/latest.zip'; + } + } + + // We can also infer X.X.X versions immediately. + const parsedVersion = wpVersion.match( /^([0-9]+)\.([0-9]+)\.([0-9]+)$/ ); + if ( parsedVersion ) { + // Note that X.X.0 versions use a X.X download URL. + let urlVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`; + if ( parsedVersion[ 3 ] !== '0' ) { + urlVersion += `.${ parsedVersion[ 3 ] }`; + } + + return `https://wordpress.org/wordpress-${ urlVersion }.zip`; + } + + // Since we haven't found a URL yet we're going to use the WordPress.org API to try and infer one. + return getPreciseWPVersionURL( wpVersion ); +} + +/** + * Given a path within a project, + * + * @param {string} absolutePath An absolute path to a project or a project file. + * @return {string} The path to the project. + */ +function getProjectPathFromAbsolutePath( absolutePath ) { + const matches = absolutePath.match( + // Note the special handling for `plugins/woocommerce/client/*` packages. + /((?:plugins\/woocommerce\/client\/[a-z0-9\-_.]+|plugins\/|packages\/[a-z0-9\-_.]+\/|tools\/)[a-z0-9\-_.]+)\/?/i + ); + if ( ! matches ) { + return null; + } + return matches[ 1 ]; +} + +/** + * A record for a project and all of the changes that have occurred to it. + * + * @typedef {Object} ProjectChanges + * @property {string} path The path to the project. + * @property {boolean} phpSourceChanges Whether or not the project has changes to PHP source files. + * @property {boolean} jsSourceChanges Whether or not the project has changes to JS source files. + * @property {boolean} assetSourceChanges Whether or not the project has changed to asset source files. + * @property {boolean} documentationChanges Whether or not the project has documentation changes. + * @property {boolean} phpTestChanges Whether or not the project has changes to PHP test files. + * @property {boolean} jsTestChanges Whether or not the project has changes to JS test files. + * @property {boolean} e2eTestChanges Whether or not the project has changes to e2e test files. + */ + +/** + * Scans through the files that have been changed since baseRef and returns information about the projects that have + * changes and the kind of changes that have taken place. + * + * @param {string} baseRef The base branch to check for changes against. + * @return {Array.} An array of projects and the kinds of changes that have occurred. + */ +function detectProjectChanges( baseRef ) { + // Using a diff will not only allow us to find the projects that have changed but we can also identify the nature of the change. + const output = child_process.execSync( + `git diff --relative --name-only ${ baseRef }`, + { encoding: 'utf8' } + ); + const changedFilePaths = output.split( '\n' ); + + // Scan all of the changed files into the projects they belong to. + const projectsWithChanges = {}; + for ( const filePath of changedFilePaths ) { + if ( ! filePath ) { + continue; + } + + const projectPath = getProjectPathFromAbsolutePath( filePath ); + if ( ! projectPath ) { + console.log( + `${ filePath }: ignoring change because it is not part of a project.` + ); + continue; + } + if ( ! projectsWithChanges[ projectPath ] ) { + projectsWithChanges[ projectPath ] = []; + } + projectsWithChanges[ projectPath ].push( filePath ); + console.log( + `${ filePath }: marked as a change in project "${ projectPath }".` + ); + } + + // Scan through the projects that have changes and identify the type of changes that have occurred. + const projectChanges = []; + for ( const projectPath in projectsWithChanges ) { + // We are only interested in projects that are part of our workspace. + if ( ! fs.existsSync( `${ projectPath }/package.json` ) ) { + console.error( `${ projectPath }: no "package.json" file found.` ); + continue; + } + + // Keep track of the kind of changes that have occurred. + let phpTestChanges = false; + let jsTestChanges = false; + let e2eTestChanges = false; + let phpSourceChanges = false; + let jsSourceChanges = false; + let assetSourceChanges = false; + let documentationChanges = false; + + // Now we can look through all of the files that have changed and figure out the type of changes that have occurred. + const fileChanges = projectsWithChanges[ projectPath ]; + for ( const filePath of fileChanges ) { + // Some types of changes are not interesting and should be ignored completely. + if ( filePath.match( /\/changelog\//i ) ) { + console.log( + `${ projectPath }: ignoring changelog file "${ filePath }".` + ); + continue; + } + + // As a preface, the detection of changes here is likely not absolutely perfect. We're going to be making some assumptions + // about file extensions and paths in order to decide whether or not something is a type of change. This should still + // be okay though since we have other cases where we check everything without looking at any changes to filter. + + // We can identify PHP test files using PSR-4 or WordPress file naming conventions. We also have + // a fallback to any PHP files in a "tests" directory or its children. + // Note: We need to check for this before we check for source files, otherwise we will + // consider test file changes to be PHP source file changes. + if ( + filePath.match( /(?:[a-z]+Test|-test|\/tests?\/[^\.]+)\.php$/i ) + ) { + phpTestChanges = true; + console.log( + `${ projectPath }: detected PHP test file change in "${ filePath }".` + ); + continue; + } + + // We can identify JS test files using Jest file file naming conventions. We also have + // a fallback to any JS files in a "tests" directory or its children, but we need to + // avoid picking up E2E test files in the process. + // Note: We need to check for this before we check for source files, otherwise we will + // consider test file changes to be JS source file changes. + if ( + filePath.match( + /(?:(?} projectChanges The project changes to cascade. + * @return {Array.} The project changes with any cascading changes. + */ +function cascadeProjectChanges( projectChanges ) { + const cascadedChanges = {}; + + // Scan through all of the changes and add any other projects that are affected by the changes. + for ( const changes of projectChanges ) { + // Populate the change object for the project if it doesn't already exist. + // It might exist if the project has been affected by another project. + if ( ! cascadedChanges[ changes.path ] ) { + cascadedChanges[ changes.path ] = changes; + } + + // Make sure that we are recording any "true" changes that have occurred either in the project itself or as a result of another project. + for ( const property in changes ) { + // We're going to assume the only properties on this object are "path" and the change flags. + if ( property === 'path' ) { + continue; + } + cascadedChanges[ changes.path ][ property ] = + changes[ property ] || + cascadedChanges[ changes.path ][ property ]; + } + + // Use PNPM to get a list of dependent packages that may have been affected. + // Note: This is actually a pretty slow way of doing this. If we find it is + // taking too long we can instead use `--depth="Infinity" --json` and then + // traverse the dependency tree ourselves. + const output = child_process.execSync( + `pnpm list --filter='...{./${ changes.path }}' --only-projects --depth='-1' --parseable`, + { encoding: 'utf8' } + ); + // The `--parseable` flag returns a list of package directories separated by newlines. + const affectedProjects = output.split( '\n' ); + + // At the VERY least PNPM will return the path to the project if it exists. The only way + // this will happen is if the project isn't part of the workspace and we can ignore it. + // We expect this to happen and thus haven't use the caret in the filter above. + if ( ! affectedProjects ) { + continue; + } + + // Run through and decide whether or not the project has been affected by the changes. + for ( const affected of affectedProjects ) { + const affectedProjectPath = + getProjectPathFromAbsolutePath( affected ); + if ( ! affectedProjectPath ) { + continue; + } + + // Skip the project we're checking against since it'll be in the results. + if ( affectedProjectPath === changes.path ) { + continue; + } + + // Only changes to source files will impact other projects. + if ( + ! changes.phpSourceChanges && + ! changes.jsSourceChanges && + ! changes.assetSourceChanges + ) { + continue; + } + + console.log( + `${ changes.path }: cascading source file changes to ${ affectedProjectPath }.` + ); + + // Populate the change object for the affected project if it doesn't already exist. + if ( ! cascadedChanges[ affectedProjectPath ] ) { + cascadedChanges[ affectedProjectPath ] = { + path: affectedProjectPath, + phpSourceChanges: false, + jsSourceChanges: false, + assetSourceChanges: false, + documentationChanges: false, + phpTestChanges: false, + jsTestChanges: false, + e2eTestChanges: false, + }; + } + + // Consider the source files to have changed in the affected project because they are dependent on the source files in the changed project. + if ( changes.phpSourceChanges ) { + cascadedChanges[ affectedProjectPath ].phpSourceChanges = true; + } + if ( changes.jsSourceChanges ) { + cascadedChanges[ affectedProjectPath ].jsSourceChanges = true; + } + if ( changes.assetSourceChanges ) { + cascadedChanges[ + affectedProjectPath + ].assetSourceChanges = true; + } + } + } + + return Object.values( cascadedChanges ); +} + +/** + * The valid commands that we can execute. + * + * @typedef {string} CommandType + * @enum {CommandType} + */ +const COMMAND_TYPE = { + Lint: 'lint', + TestPHP: 'test:php', + TestJS: 'test:js', + E2E: 'e2e', +}; + +/** + * Checks a command to see whether or not it is valid. + * + * @param {CommandType} command The command to check. + * @return {boolean} Whether or not the command is valid.T + */ +function isValidCommand( command ) { + for ( const commandType in COMMAND_TYPE ) { + if ( COMMAND_TYPE[ commandType ] === command ) { + return true; + } + } + + return false; +} + +/** + * Indicates whether or not the command is a test command. + * + * @param {CommandType} command The command to check. + * @return {boolean} Whether or not the command is a test command. + */ +function isTestCommand( command ) { + return ( + command === COMMAND_TYPE.TestPHP || + command === COMMAND_TYPE.TestJS || + command === COMMAND_TYPE.E2E + ); +} + +/** + * Details about a task that should be run for a project. + * + * @typedef {Object} ProjectTask + * @property {string} name The name of the task. + * @property {Array.} commandsToRun The commands that the project should run. + * @property {Object.} customCommands Any commands that should be run in place of the default commands. + * @property {string|null} testEnvCommand The command that should be run to start the test environment if one is needed. + * @property {Object.} testEnvConfig Any configuration for the test environment if one is needed. + */ + +/** + * Parses the task configuration from the package.json file and returns a task object. + * + * @param {Object} packageFile The package file for the project. + * @param {Object} config The taw task configuration. + * @param {Array.} commandsForChanges The commands that we should run for the project. + * @param {ProjectTask|null} parentTask The task that this task is a child of. + * @return {ProjectTask|null} The parsed task. + */ +function parseTaskConfig( + packageFile, + config, + commandsForChanges, + parentTask +) { + // Child tasks are required to have a name because otherwise + // every task for a project would be named "default". + let taskName = 'default'; + if ( parentTask ) { + taskName = config.name; + if ( ! taskName ) { + throw new Error( `${ packageFile.name }: missing name for task.` ); + } + } + + // When the config object declares a command filter we should remove any + // of the commands it contains from the list of commands to run. + if ( config?.commandFilter ) { + // Check for invalid commands being used since they won't do anything. + for ( const command of config.commandFilter ) { + if ( ! isValidCommand( command ) ) { + throw new Error( + `${ packageFile.name }: invalid command filter type of "${ command }" for task "${ taskName }".` + ); + } + } + + // Apply the command filter. + commandsForChanges = commandsForChanges.filter( ( command ) => + config.commandFilter.includes( command ) + ); + } + + // Custom commands developers to support a command without having to use the + // standardized script name for it. For ease of use we will add parent task + // custom commands to children and allow the children to override any + // specific tasks they want. + const customCommands = Object.assign( + {}, + parentTask?.customCommands ?? {} + ); + if ( config?.customCommands ) { + for ( const customCommandType in config.customCommands ) { + // Check for invalid commands being mapped since they won't do anything. + if ( ! isValidCommand( customCommandType ) ) { + throw new Error( + `${ packageFile.name }: invalid custom command type "${ customCommandType } for task "${ taskName }".` + ); + } + + // Custom commands may have tokens that we need to remove in order to check them for existence. + const split = + config.customCommands[ customCommandType ].split( ' ' ); + const customCommand = split[ 0 ]; + + if ( ! packageFile.scripts?.[ customCommand ] ) { + throw new Error( + `${ packageFile.name }: unknown custom "${ customCommandType }" command "${ customCommand }" for task "${ taskName }".` + ); + } + + // We only need to bother with commands we can actually run. + if ( commandsForChanges.includes( customCommandType ) ) { + customCommands[ customCommandType ] = + config.customCommands[ customCommandType ]; + } + } + } + + // Our goal is to run only the commands that have changes, however, not all + // projects will have scripts for all of the commands we want to run. + const commandsToRun = []; + for ( const command of commandsForChanges ) { + // We have already filtered and confirmed custom commands. + if ( customCommands[ command ] ) { + commandsToRun.push( command ); + continue; + } + + // Commands that don't have a script to run should be ignored. + if ( ! packageFile.scripts?.[ command ] ) { + continue; + } + + commandsToRun.push( command ); + } + + // We don't want to create a task if there aren't any commands to run. + if ( ! commandsToRun.length ) { + return null; + } + + // The test environment command only needs to be set when a test environment is needed. + let testEnvCommand = null; + if ( commandsToRun.some( ( command ) => isTestCommand( command ) ) ) { + if ( config?.testEnvCommand ) { + // Make sure that a developer hasn't put in a test command that doesn't exist. + if ( ! packageFile.scripts?.[ config.testEnvCommand ] ) { + throw new Error( + `${ packageFile.name }: unknown test environment command "${ config.testEnvCommand }" for task "${ taskName }".` + ); + } + + testEnvCommand = + config?.testEnvCommand ?? parentTask?.testEnvCommand; + } else if ( packageFile.scripts?.[ 'test:env:start' ] ) { + testEnvCommand = 'test:env:start'; + } + } + + // The test environment configuration should also cascade from parent task to child task. + const testEnvConfig = Object.assign( + {}, + parentTask?.testEnvConfig ?? {}, + config?.testEnvConfig ?? {} + ); + + return { + name: taskName, + commandsToRun, + customCommands, + testEnvCommand, + testEnvConfig, + }; +} + +/** + * Details about a project and the tasks that should be run for it. + * + * @typedef {Object} ProjectTasks + * @property {string} name The name of the project. + * @property {Array.} tasks The tasks that should be run for the project. + */ + +/** + * Evaluates the given changes against the possible commands and returns those that should run as + * a result of the change criteria being met. + * + * @param {ProjectChanges|null} changes Any changes that have occurred to the project. + * @return {Array.} The commands that can be run for the project. + */ +function getCommandsForChanges( changes ) { + // Here are all of the commands that we support and the change criteria that they require to execute. + // We treat the command's criteria as passing if any of the properties are true. + const commandCriteria = { + [ COMMAND_TYPE.Lint ]: [ + 'phpSourceChanges', + 'jsSourceChanges', + 'assetSourceChanges', + 'phpTestChanges', + 'jsTestChanges', + ], + [ COMMAND_TYPE.TestPHP ]: [ 'phpSourceChanges', 'phpTestChanges' ], + [ COMMAND_TYPE.TestJS ]: [ 'jsSourceChanges', 'jsTestChanges' ], + //[ COMMAND_TYPE.E2E ]: [ 'phpSourceChanges', 'jsSourceChanges', 'assetSourceChanges', 'e2eTestFileChanges' ], + }; + + // We only want the list of possible commands to contain those that + // the project actually has and meet the criteria for execution. + const commandsForChanges = []; + for ( const command in commandCriteria ) { + // The criteria only needs to be checked if there is a change object to evaluate. + if ( changes ) { + let commandCriteriaMet = false; + for ( const criteria of commandCriteria[ command ] ) { + // Confidence check to make sure the criteria wasn't misspelled. + if ( ! changes.hasOwnProperty( criteria ) ) { + throw new Error( + `Invalid criteria "${ criteria }" for command "${ command }".` + ); + } + + if ( changes[ criteria ] ) { + commandCriteriaMet = true; + break; + } + } + + // As long as we meet one of the criteria requirements we can add the command. + if ( ! commandCriteriaMet ) { + continue; + } + } + + commandsForChanges.push( command ); + console.log( `${ changes.path }: command "${ command }" added based on given changes.` ); + } + + return commandsForChanges; +} + +/** + * Builds a task object for the project with support for limiting the tasks to only those that have changed. + * + * @param {string} projectPath The path to the project. + * @param {ProjectChanges|null} changes Any changes that have occurred to the project. + * @return {ProjectTasks|null} The tasks that should be run for the project. + */ +function buildTasksForProject( projectPath, changes ) { + // There's nothing to do if the project has no tasks. + const commandsForChanges = getCommandsForChanges( changes ); + if ( ! commandsForChanges.length ) { + return null; + } + + // Load the package file so we can check for task existence before adding them. + const rawPackageFile = fs.readFileSync( + `${ projectPath }/package.json`, + 'utf8' + ); + const packageFile = JSON.parse( rawPackageFile ); + + // We're going to parse each of the projects and add them to the list of tasks if necessary. + const projectTasks = []; + + // Parse the task configuration from the package.json file. + const parentTask = parseTaskConfig( + packageFile, + packageFile.config?.ci, + commandsForChanges, + null + ); + if ( parentTask ) { + projectTasks.push( parentTask ); + } + + if ( packageFile.config?.ci?.additionalTasks ) { + for ( const additionalTask of packageFile.config.ci.additionalTasks ) { + const task = parseTaskConfig( + packageFile, + additionalTask, + commandsForChanges, + parentTask + ); + if ( task ) { + projectTasks.push( task ); + } + } + } + + if ( ! projectTasks.length ) { + return null; + } + + return { + name: packageFile.name, + tasks: projectTasks, + }; +} + +/** + * This function takes a list of project changes and generates a list of tasks that should be run for each project. + * + * @param {Array.} projectChanges The project changes to generate tasks for. + * @return {Array.} All of the projects and the tasks that they should undertake. + */ +function generateProjectTasksForChanges( projectChanges ) { + const projectTasks = []; + + // Scan through all of the changes and generate task objects for them. + for ( const changes of projectChanges ) { + const tasks = buildTasksForProject( changes.path, changes ); + if ( tasks ) { + projectTasks.push( tasks ); + } + } + + return projectTasks; +} + +/** + * Generates a list of tasks that should be run for each project in the workspace. + * + * @return {Array.} All of the projects and the tasks that they should undertake. + */ +function generateProjectTasksForWorkspace() { + // We can use PNPM to quickly get a list of every project in the workspace. + const output = child_process.execSync( + "pnpm list --filter='*' --only-projects --depth='-1' --parseable", + { encoding: 'utf8' } + ); + // The `--parseable` flag returns a list of package directories separated by newlines. + const workspaceProjects = output.split( '\n' ); + + const projectTasks = []; + for ( const project of workspaceProjects ) { + const projectPath = getProjectPathFromAbsolutePath( project ); + if ( ! projectPath ) { + continue; + } + + const tasks = buildTasksForProject( projectPath, null ); + if ( tasks ) { + projectTasks.push( tasks ); + } + } + + return projectTasks; +} + +/** + * A CI matrix for the GitHub workflow. + * + * @typedef {Object} CIMatrix + * @property {string} projectName The name of the project. + * @property {string} taskName The name of the task. + * @property {Object.} testEnvVars The environment variables for the test environment. + * @property {string|null} lintCommand The command to run if linting is necessary. + * @property {string|null} phpTestCommand The command to run if PHP tests are necessary. + * @property {string|null} jsTestCommand The command to run if JS tests are necessary. + * @property {string|null} e2eCommand The command to run if E2E is necessary. + */ + +/** + * Parses the test environment's configuration and returns any environment variables that + * should be set. + * + * @param {Object} testEnvConfig The test environment configuration. + * @return {Promise.} The environment variables for the test environment. + */ +async function parseTestEnvConfig( testEnvConfig ) { + const envVars = {}; + + // Convert `wp-env` configuration options to environment variables. + if ( testEnvConfig.wpVersion ) { + try { + envVars.WP_ENV_CORE = await parseWPVersion( + testEnvConfig.wpVersion + ); + } catch ( error ) { + throw new Error( + `Failed to parse WP version: ${ error.message }.` + ); + } + } + if ( testEnvConfig.phpVersion ) { + envVars.WP_ENV_PHP_VERSION = testEnvConfig.phpVersion; + } + + return envVars; +} + +/** + * Generates a command for the task that can be executed in the CI matrix. This will check the task + * for the command, apply any command override, and replace any valid tokens with their values. + * + * @param {ProjectTask} task The task to get the command for. + * @param {CommandType} command The command to run. + * @param {Object.} tokenValues Any tokens that should be replaced and their associated values. + * @return {string|null} The command that should be run for the task or null if the command should not be run. + */ +function getCommandForMatrix( task, command, tokenValues ) { + if ( ! task.commandsToRun.includes( command ) ) { + return null; + } + + // Support overriding the default command with a custom one. + command = task.customCommands[ command ] ?? command; + + // Replace any of the tokens that are used in commands with their values if one exists. + let matrixCommand = command; + const matches = command.matchAll( /\${([a-z0-9_\-]+)}/gi ); + if ( matches ) { + for ( const match of matches ) { + if ( ! tokenValues.hasOwnProperty( match[ 1 ] ) ) { + throw new Error( + `Command "${ command }" contains unknown token "${ match[ 1 ] }".` + ); + } + + matrixCommand = matrixCommand.replace( + match[ 0 ], + tokenValues[ match[ 1 ] ] + ); + } + } + + return matrixCommand; +} + +/** + * Generates a matrix for the CI GitHub Workflow. + * + * @param {string} baseRef The base branch to check for changes against. If empty we check for everything. + * @return {Promise.>} The CI matrix to be used in the CI workflows. + */ +async function buildCIMatrix( baseRef ) { + const matrix = []; + + // Build the project tasks based on the branch we are comparing against. + let projectTasks = []; + if ( baseRef ) { + const projectChanges = detectProjectChanges( baseRef ); + const cascadedProjectChanges = cascadeProjectChanges( projectChanges ); + projectTasks = generateProjectTasksForChanges( cascadedProjectChanges ); + } else { + projectTasks = generateProjectTasksForWorkspace(); + } + + // Prepare the tokens that are able to be replaced in commands. + const commandTokens = { + baseRef: baseRef ?? '', + }; + + // Parse the tasks and generate matrix entries for each of them. + for ( const project of projectTasks ) { + for ( const task of project.tasks ) { + matrix.push( { + projectName: project.name, + taskName: task.name, + testEnvCommand: task.testEnvCommand, + testEnvVars: await parseTestEnvConfig( task.testEnvConfig ), + lintCommand: getCommandForMatrix( + task, + COMMAND_TYPE.Lint, + commandTokens + ), + phpTestCommand: getCommandForMatrix( + task, + COMMAND_TYPE.TestPHP, + commandTokens + ), + jsTestCommand: getCommandForMatrix( + task, + COMMAND_TYPE.TestJS, + commandTokens + ), + e2eCommand: getCommandForMatrix( + task, + COMMAND_TYPE.E2E, + commandTokens + ), + } ); + } + } + + return matrix; +} + +module.exports = buildCIMatrix; diff --git a/.github/workflows/scripts/parse-wp-version.js b/.github/workflows/scripts/parse-wp-version.js deleted file mode 100644 index f3f3c420368..00000000000 --- a/.github/workflows/scripts/parse-wp-version.js +++ /dev/null @@ -1,89 +0,0 @@ -const https = require( 'http' ); - -/** - For convenience, this method will convert between a display-friendly version format and one used - internally by wp-env. We lean towards using WordPress.org ZIPs which requires us to reference - the full URL to the archive. For instance, instead of needing the action to fully define the - URL to the nightly build we can pass "nightly" to this function and retrieve it. - - @param {string} wpVersion The display-friendly version. Supports ("master", "trunk", "nightly", - "latest", "X.X" for version lines, and "X.X.X" for specific versions) - @return {Promise.} The wp-env "core" property". -**/ -module.exports = async function parseWPVersion( wpVersion ) { - // Start with versions we can infer immediately. - switch ( wpVersion ) { - case 'master': - case 'trunk': { - return 'WordPress/WordPress#master'; - } - - case 'nightly': { - return 'https://wordpress.org/nightly-builds/wordpress-latest.zip'; - } - - case 'latest': { - return 'https://wordpress.org/latest.zip'; - } - } - - return new Promise( ( resolve, reject ) => { - // We're going to download the correct zip archive based on the version they're requesting. - const parsedVersion = wpVersion.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ ); - if ( ! parsedVersion ) { - throw new Error( `Invalid 'wp-version': ${ wpVersion } must be 'trunk', 'nightly', 'latest', 'X.X', or 'X.X.X'.` ); - } - - // When they've provided a specific version we can just provide that. - if ( parsedVersion[ 3 ] !== undefined ) { - let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`; - // .0 versions do not have a patch. - if ( parsedVersion[ 3 ] !== '0' ) { - zipVersion += `.${ parsedVersion[ 3 ] }`; - } - - resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` ); - } - - const request = https.get( - 'http://api.wordpress.org/core/stable-check/1.0/', - ( response ) => { - // Listen for the response data. - let data = ''; - response.on('data', (chunk) => { - data += chunk; - }); - - // Once we have the entire response we can process it. - response.on('end', () => { - // Parse the response and find the latest version of every minor release. - const latestVersions = {}; - const rawVersions = JSON.parse( data ); - for ( const v in rawVersions ) { - // Parse the version so we can find the latest. - const matches = v.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ ); - const minor = `${ matches[1] }.${ matches[2] }`; - const patch = matches[ 3 ] === undefined ? 0 : parseInt( matches[ 3 ] ); - - // We will only be keeping the latest release of each minor. - if ( latestVersions[ minor ] === undefined || patch > latestVersions[ minor ] ) { - latestVersions[ minor ] = patch; - } - } - - let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`; - // .0 versions do not have a patch. - if ( latestVersions[ zipVersion ] !== 0 ) { - zipVersion += `.${ latestVersions[ zipVersion ]}`; - } - - resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` ); - }); - }, - ); - - request.on( 'error', ( error ) => { - reject( error ); - } ); - } ); -} diff --git a/plugins/woocommerce/bin/lint-branch.sh b/plugins/woocommerce/bin/lint-branch.sh index ed17883fe5c..008f68c0d3c 100644 --- a/plugins/woocommerce/bin/lint-branch.sh +++ b/plugins/woocommerce/bin/lint-branch.sh @@ -1,5 +1,4 @@ -#!/bin/bash - +#!/usr/bin/env bash # Lint branch # # Runs phpcs-changed, comparing the current branch to its "base" or "parent" branch. @@ -14,4 +13,9 @@ baseBranch=${1:-"trunk"} changedFiles=$(git diff $(git merge-base HEAD $baseBranch) --relative --name-only -- '*.php') # Only complete this if changed files are detected. -[[ -z $changedFiles ]] || composer exec phpcs-changed -- -s --git --git-base $baseBranch $changedFiles +if [[ -z $changedFiles ]]; then + echo "No changed files detected." + exit 0 +fi + +composer exec phpcs-changed -- -s --git --git-base $baseBranch $changedFiles diff --git a/plugins/woocommerce/changelog/refactor-unit-test-workflow b/plugins/woocommerce/changelog/refactor-unit-test-workflow new file mode 100644 index 00000000000..c37373fdecf --- /dev/null +++ b/plugins/woocommerce/changelog/refactor-unit-test-workflow @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: Just making some changes to support the new CI workflow. + diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 3ca110d090c..5041a34b23a 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -97,7 +97,7 @@ "chg=$(git diff HEAD --relative --name-only -- '*.php'); [[ -z $chg ]] || phpcs-changed -s --git $chg" ], "lint-branch": [ - "sh ./bin/lint-branch.sh" + "bash ./bin/lint-branch.sh" ], "phpcbf": [ "phpcbf -p" diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index c757584f92b..c35dc2bb7c5 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -10,7 +10,34 @@ "license": "GPL-3.0+", "config": { "wp_org_slug": "woocommerce", - "build_step": "pnpm run build:zip" + "build_step": "pnpm run build:zip", + "ci": { + "name": "WP: latest", + "customCommands": { + "lint": "lint:php:changes:branch ${baseRef}", + "test:php": "test:php:env" + }, + "testEnvCommand": "env:test", + "testEnvConfig": { + "wpVersion": "latest" + }, + "additionalTasks": [ + { + "name": "WP: latest-1", + "commandFilter": [ "test:php" ], + "testEnvConfig": { + "wpVersion": "latest-1" + } + }, + { + "name": "WP: latest-2", + "commandFilter": [ "test:php" ], + "testEnvConfig": { + "wpVersion": "latest-2" + } + } + ] + } }, "scripts": { "e2e": "pnpm exec wc-e2e test:e2e", @@ -19,8 +46,7 @@ "postinstall": "composer install", "changelog": "composer exec -- changelogger", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm test:js", - "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "pnpm lint:php", "build:feature-config": "php bin/generate-feature-config.php", "build:zip": "./bin/build-zip.sh",