diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000..e2f934a --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,22 @@ +# Configuration for actionlint, run by the `actionlint` job in +# `.github/workflows/reusable-code-quality.yml`. +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + # `copilot-requests` is a real permission scope - it is what lets a + # workflow authenticate the Copilot CLI with the built-in `GITHUB_TOKEN` + # instead of a personal access token - but actionlint's hard-coded scope + # list has not caught up with it yet, so it reports every use as unknown. + # Drop this entry once actionlint ships the scope, and it will go back to + # catching genuine typos in permission names. + # See https://docs.github.com/en/copilot/how-tos/copilot-cli/use-copilot-cli-in-actions + - 'unknown permission scope "copilot-requests"' + # Steps can run in parallel through the `background`, `wait`, `wait-all`, + # `cancel` and `parallel` keywords, but actionlint does not know them yet: + # it rejects `background` as an unexpected key, and a `wait-all` or + # `parallel` step as one that has neither `run` nor `uses`. Drop these + # entries once actionlint supports the keywords. Until then, a step that is + # genuinely missing both `run` and `uses` is no longer reported. + # See https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsparallel + - 'unexpected key "background" for step' + - 'step must run script with "run" section or run action with "uses" section' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d6c7b8b..49fbe19 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,22 @@ updates: open-pull-requests-limit: 10 labels: - scope:distribution + cooldown: + default-days: 7 + # Packages from our own org are trusted, so they are updated immediately. + exclude: + - "wp-cli/*" + # Only wp-cli-tests carries a package.json, holding the pinned version of the + # Gherkin linter. This is a no-op in every other repository. + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + labels: + - scope:distribution + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: "/" schedule: @@ -14,4 +30,6 @@ updates: open-pull-requests-limit: 10 labels: - scope:distribution + cooldown: + default-days: 7 diff --git a/.github/workflows/check-branch-alias.yml b/.github/workflows/check-branch-alias.yml new file mode 100644 index 0000000..78da637 --- /dev/null +++ b/.github/workflows/check-branch-alias.yml @@ -0,0 +1,14 @@ +name: Check Branch Alias + +on: + release: + types: [released] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + check-branch-alias: + uses: wp-cli/.github/.github/workflows/reusable-check-branch-alias.yml@main diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 07e4fd1..27f9b00 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,7 +6,13 @@ on: branches: - main - master + schedule: + - cron: '17 2 * * *' # Run every day on a seemly random time. jobs: code-quality: uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main + with: + parallel-lint-excludes: | + process + wordpress-core diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..5d79e8a --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,89 @@ +name: "Copilot Setup Steps" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +permissions: + contents: read + +jobs: + copilot-setup-steps: + name: Setup environment + runs-on: ubuntu-latest + permissions: + contents: read + + env: + MYSQL_HOST: 127.0.0.1 + MYSQL_TCP_PORT: 3306 + WP_CLI_TEST_DBROOTUSER: root + WP_CLI_TEST_DBROOTPASS: root + WP_CLI_TEST_DBNAME: wp_cli_test + WP_CLI_TEST_DBUSER: wp_cli_test + WP_CLI_TEST_DBPASS: password1 + WP_CLI_TEST_DBHOST: 127.0.0.1:3306 + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check existence of composer.json file + id: check_composer_file + run: echo "files_exists=$(test -f composer.json && echo true || echo false)" >> "$GITHUB_OUTPUT" + + # Without a database the Behat runner silently falls back to SQLite, so the + # agent's test runs are not the ones CI will do. Mirrors the setup in + # reusable-functional.yml. + - name: Check existence of behat.yml file + id: check_behat_file + run: echo "files_exists=$(test -f behat.yml && echo true || echo false)" >> "$GITHUB_OUTPUT" + + # Downloading and starting the database server is the slowest part of the + # setup and it is independent of PHP and Composer, so it runs in the + # background and is waited for right before the test database is prepared. + - name: Setup MySQL Server + if: steps.check_behat_file.outputs.files_exists == 'true' + background: true + uses: shogo82148/actions-setup-mysql@1d4ebc60ba1227dd13fc7e6ba845bd3997928d75 # v1 + with: + mysql-version: '8.0' # Standard MySQL version for these tests + auto-start: true + root-password: ${{ env.WP_CLI_TEST_DBROOTPASS }} + user: ${{ env.WP_CLI_TEST_DBUSER }} + password: ${{ env.WP_CLI_TEST_DBPASS }} + + - name: Set up PHP environment + if: steps.check_composer_file.outputs.files_exists == 'true' + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: 'latest' + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: 'none' + tools: composer,cs2pr + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Composer dependencies & cache dependencies + if: steps.check_composer_file.outputs.files_exists == 'true' + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + env: + COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} + + - name: Wait for the MySQL server + wait-all: + + - name: Remove system MySQL binary + if: steps.check_behat_file.outputs.files_exists == 'true' + run: sudo rm -f /usr/bin/mysql /usr/bin/mysqldump + + - name: Prepare test database + if: steps.check_behat_file.outputs.files_exists == 'true' + run: composer prepare-tests diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..0795944 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,41 @@ +--- +name: Issue and PR Triage + +'on': + issues: + types: [opened] + pull_request_target: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue/PR number to triage (leave empty to process all)' + required: false + type: string + +permissions: + issues: write + pull-requests: write + contents: read + # A caller can only cap a reusable workflow's permissions, so the scope the + # Copilot CLI needs has to be granted here too. `models: read` was for the + # GitHub Models provider, which `actions/ai-inference` v3 removed. + copilot-requests: write + # A caller can only cap a reusable workflow's permissions, never raise them, + # so `actions: write` has to be granted here for the dispatch job downstream + # to work at all. The reusable workflow narrows it to that single job, so the + # `pull_request_target` job does not receive it. Removing it here breaks + # dispatching (see #271/#272); narrow it there instead. + actions: write + +jobs: + issue-triage: + uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main + with: + issue_number: >- + ${{ + (github.event_name == 'workflow_dispatch' && inputs.issue_number) || + (github.event_name == 'pull_request_target' && github.event.pull_request.number) || + (github.event_name == 'issues' && github.event.issue.number) || + '' + }} diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml new file mode 100644 index 0000000..45711bd --- /dev/null +++ b/.github/workflows/manage-labels.yml @@ -0,0 +1,19 @@ +--- +name: Manage Labels + +'on': + workflow_dispatch: + push: + branches: + - main + - master + paths: + - 'composer.json' + +permissions: + issues: write + contents: read + +jobs: + manage-labels: + uses: wp-cli/.github/.github/workflows/reusable-manage-labels.yml@main diff --git a/.github/workflows/regenerate-readme.yml b/.github/workflows/regenerate-readme.yml new file mode 100644 index 0000000..6198d63 --- /dev/null +++ b/.github/workflows/regenerate-readme.yml @@ -0,0 +1,19 @@ +name: Regenerate README file + +on: + workflow_dispatch: + push: + branches: + - main + - master + paths-ignore: + - "features/**" + - "README.md" + +permissions: + contents: write + pull-requests: write + +jobs: + regenerate-readme: + uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..da8d53c --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,17 @@ +name: Testing + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + test: + uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main + with: + # composer.json still allows PHP 7.2, but the development environment + # cannot be installed below 7.4: johnpbloch/wordpress-core dev-master + # requires it, so `composer install` fails on the 7.2 and 7.3 legs. + minimum-php: '7.4' diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml new file mode 100644 index 0000000..bc01490 --- /dev/null +++ b/.github/workflows/welcome-new-contributors.yml @@ -0,0 +1,15 @@ +name: Welcome New Contributors + +on: + pull_request_target: + types: [opened] + branches: + - main + - master + +permissions: + pull-requests: write + +jobs: + welcome: + uses: wp-cli/.github/.github/workflows/reusable-welcome-new-contributors.yml@main diff --git a/.gitignore b/.gitignore index 823aa74..b511fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # Exclude all subfolders... /*/ -# except for the _maintenance folder. +# except for the following. +!/.github/ !/.maintenance/ !/.readme-partials/ +!/tests/ composer.lock *.log diff --git a/.maintenance/clone-all-repositories.php b/.maintenance/clone-all-repositories.php deleted file mode 100644 index 29c3656..0000000 --- a/.maintenance/clone-all-repositories.php +++ /dev/null @@ -1,56 +0,0 @@ - 'dot-github', -); - -$request = 'https://api.github.com/orgs/wp-cli/repos?per_page=100'; -$headers = ''; -$token = getenv( 'GITHUB_TOKEN' ); -if ( ! empty( $token ) ) { - $headers = "--header \"Authorization: Bearer $token\""; - $response = shell_exec( "curl -s {$headers} {$request}" ); -} else { - $response = shell_exec( "curl -s {$request}" ); -} -$repositories = json_decode( $response ); -if ( ! is_array( $repositories ) && property_exists( $repositories, 'message' ) ) { - echo 'GitHub responded with: ' . $repositories->message . "\n"; - echo "If you are running into a rate limiting issue during large events please set GITHUB_TOKEN environment variable.\n"; - echo "See https://github.com/settings/tokens\n"; - exit( 1 ); -} - -$pwd = getcwd(); -$update_folders = []; - -foreach ( $repositories as $repository ) { - if ( in_array( $repository->name, $skip_list, true ) ) { - continue; - } - - $destination = isset( $clone_destination_map[ $repository->name ] ) ? $clone_destination_map[ $repository->name ] : $repository->name; - - if ( ! is_dir( $destination ) ) { - printf( "Fetching \033[32mwp-cli/{$repository->name}\033[0m...\n" ); - $clone_url = getenv( 'GITHUB_ACTION' ) ? $repository->clone_url : $repository->ssh_url; - system( "git clone {$clone_url} {$destination}" ); - } - - $update_folders[] = $destination; -} - -$updates = implode( '\n', $update_folders ); -system( "echo '$updates' | xargs -n1 -P8 -I% php .maintenance/refresh-repository.php %" ); diff --git a/.maintenance/clone-all-repositories.sh b/.maintenance/clone-all-repositories.sh new file mode 100755 index 0000000..33bf6b7 --- /dev/null +++ b/.maintenance/clone-all-repositories.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if ! command -v jq &>/dev/null; then + echo "Required command 'jq' is not installed or not available in PATH." >&2 + exit 1 +fi + +SKIP_LIST=( + "autoload-splitter" + "composer-changelogs" + "dash-docset-generator" + "ideas" + "package-index" + "regenerate-readme" + "sample-plugin" + "wp-cli-dev" + "wp-cli-roadmap" +) + +# Detect number of CPU cores, defaulting to 4. +if command -v nproc &>/dev/null; then + DETECTED_CORES=$(nproc) +elif command -v sysctl &>/dev/null; then + DETECTED_CORES=$(sysctl -n hw.logicalcpu 2>/dev/null || echo 4) +else + DETECTED_CORES=4 +fi + +MAX_CORES=8 +CORES="${CLONE_JOBS:-${WPCLI_DEV_JOBS:-${DETECTED_CORES}}}" + +if ! [[ "${CORES}" =~ ^[1-9][0-9]*$ ]]; then + CORES=4 +elif [[ -z "${CLONE_JOBS:-}" && -z "${WPCLI_DEV_JOBS:-}" && "${CORES}" -gt "${MAX_CORES}" ]]; then + CORES=${MAX_CORES} +fi + +# Fetch repository list from the GitHub API. +CURL_OPTS=(-fsS) +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + CURL_OPTS+=(--header "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +if ! RESPONSE=$(curl "${CURL_OPTS[@]}" 'https://api.github.com/orgs/wp-cli/repos?per_page=100'); then + echo "Failed to fetch repository list from the GitHub API." >&2 + exit 1 +fi + +# Validate the response shape and detect API errors such as rate limiting. +if ! jq -e 'type == "array"' >/dev/null <<< "${RESPONSE}"; then + if jq -e '.message' >/dev/null <<< "${RESPONSE}"; then + MESSAGE=$(jq -r '.message' <<< "${RESPONSE}") + echo "GitHub responded with: ${MESSAGE}" >&2 + echo "If you are running into a rate limiting issue during large events please set GITHUB_TOKEN environment variable." >&2 + echo "See https://github.com/settings/tokens" >&2 + else + echo "GitHub API returned an unexpected response; expected a JSON array of repositories." >&2 + fi + exit 1 +fi + +is_skipped() { + local name="$1" + for skip in "${SKIP_LIST[@]}"; do + [[ "${skip}" == "${name}" ]] && return 0 + done + return 1 +} + +get_destination() { + local name="$1" + if [[ "${name}" == ".github" ]]; then + echo "dot-github" + else + echo "${name}" + fi +} + +CLONE_LIST=() +UPDATE_FOLDERS=() + +while IFS=$'\t' read -r name clone_url ssh_url; do + if is_skipped "${name}"; then + continue + fi + + destination=$(get_destination "${name}") + + if [[ ! -d "${destination}" ]]; then + if [[ -n "${GITHUB_ACTION:-}" ]]; then + CLONE_LIST+=("${destination}"$'\t'"${clone_url}") + else + CLONE_LIST+=("${destination}"$'\t'"${ssh_url}") + fi + fi + + UPDATE_FOLDERS+=("${destination}") +done < <(echo "${RESPONSE}" | jq -r '.[] | [.name, .clone_url, .ssh_url] | @tsv') + +if [[ ${#CLONE_LIST[@]} -gt 0 ]]; then + printf '%s\n' "${CLONE_LIST[@]}" | xargs -n2 -P"${CORES}" bash "${SCRIPT_DIR}/clone-repository.sh" +fi + +if [[ ${#UPDATE_FOLDERS[@]} -gt 0 ]]; then + printf '%s\n' "${UPDATE_FOLDERS[@]}" | xargs -P"${CORES}" -I% php "${SCRIPT_DIR}/refresh-repository.php" % +fi diff --git a/.maintenance/clone-repository.sh b/.maintenance/clone-repository.sh new file mode 100755 index 0000000..2b9567f --- /dev/null +++ b/.maintenance/clone-repository.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: clone-repository.sh " >&2 + exit 1 +fi + +destination="$1" +clone_url="$2" + +printf "Fetching \033[32m%s\033[0m...\n" "${destination}" +git clone "${clone_url}" "${destination}" diff --git a/.maintenance/phpstorm.exclude-recursive-folders.php b/.maintenance/phpstorm.exclude-recursive-folders.php index 26e8ac3..1ef0441 100644 --- a/.maintenance/phpstorm.exclude-recursive-folders.php +++ b/.maintenance/phpstorm.exclude-recursive-folders.php @@ -55,11 +55,11 @@ protected static function has_project_iml() { if ( is_file( self::get_project_iml_path() ) ) { return; // already exists. } - $modules_xml_content = << - + diff --git a/.maintenance/src/Bundle.php b/.maintenance/src/Bundle.php new file mode 100644 index 0000000..8d7862b --- /dev/null +++ b/.maintenance/src/Bundle.php @@ -0,0 +1,302 @@ + 'all' ] ) as $milestone ) { + if ( ltrim( $milestone->title, 'v' ) === $release ) { + return $milestone; + } + } + + WP_CLI::warning( "Couldn't find milestone '{$release}' in repository '{$repo}'." ); + + return null; + } + + return array_reduce( + GitHub::get_project_milestones( $repo ), + static function ( $lowest, $milestone ) { + if ( null === $lowest ) { + return $milestone; + } + + return version_compare( $milestone->title, $lowest->title, '<' ) ? $milestone : $lowest; + } + ); + } + + /** + * Gets the ref and composer.lock that describe the packages of a bundle + * release. + * + * @param string|null $release Bundle release version, e.g. '3.0.0'. Its + * tag is used when it exists already. + * @param string|null $bundle_ref Explicit ref; wins over the release tag. + * + * @return array{0: string, 1: array} Ref and decoded lockfile. + */ + public static function get_release_lock( $release = null, $bundle_ref = null ) { + if ( $bundle_ref ) { + return [ $bundle_ref, self::get_lock( $bundle_ref ) ]; + } + + if ( $release ) { + $tag = 'v' . ltrim( $release, 'v' ); + $lock = self::get_lock( $tag, false ); + + if ( $lock ) { + return [ $tag, $lock ]; + } + + WP_CLI::debug( "Tag {$tag} does not exist in " . self::REPO . ' yet, using the default branch.', 'bundle' ); + } + + $branch = GitHub::get_default_branch( self::REPO ); + + return [ $branch, self::get_lock( $branch ) ]; + } + + /** + * Gets the ref and composer.lock of the bundle release before the given + * one, i.e. the highest closed milestone below it. + * + * @param string|null $release Bundle release version, e.g. '3.0.0'. Without + * it, the highest closed milestone is used. + * + * @return array{0: string|null, 1: array|null} Ref and decoded lockfile, + * both null without a + * previous release. + */ + public static function get_previous_release_lock( $release = null ) { + $previous = self::find_previous_release( + GitHub::get_project_milestones( self::REPO, [ 'state' => 'closed' ] ), + $release + ); + + if ( null === $previous ) { + return [ null, null ]; + } + + $tag = "v{$previous}"; + + return [ $tag, self::get_lock( $tag ) ]; + } + + /** + * Picks the highest release version below the given one out of a list of + * milestones. Milestones whose title is not a version are ignored. + * + * @param array $milestones Milestone objects with a `title`. + * @param string|null $release Bundle release version, e.g. '3.0.0'. + * Without it, the highest version wins. + * + * @return string|null Version without leading 'v', or null when there is + * no release below the given one. + */ + public static function find_previous_release( array $milestones, $release = null ) { + $release = $release ? ltrim( $release, 'v' ) : null; + $previous = null; + + foreach ( $milestones as $milestone ) { + $title = ltrim( $milestone->title, 'v' ); + + if ( ! self::is_version( $title ) ) { + continue; + } + + if ( $release && ! version_compare( $title, $release, '<' ) ) { + continue; + } + + if ( null === $previous || version_compare( $title, $previous, '>' ) ) { + $previous = $title; + } + } + + return $previous; + } + + /** + * Fetches and decodes the composer.lock of wp-cli/wp-cli-bundle at a ref. + * + * @param string $ref Branch, tag or commit. + * @param bool $missing_is_fatal Whether a ref without a lockfile (HTTP + * 404) is fatal. Any other failure always + * is, so that a rate limit or an outage + * never passes for a missing tag. + * + * @return array|false Decoded lockfile, or false when the ref has none + * and that is not fatal. + */ + public static function get_lock( $ref, $missing_is_fatal = true ) { + $url = sprintf( 'https://raw.githubusercontent.com/%s/%s/composer.lock', self::REPO, $ref ); + $response = Utils\http_request( 'GET', $url ); + + if ( 404 === (int) $response->status_code && ! $missing_is_fatal ) { + return false; + } + + if ( 200 !== (int) $response->status_code ) { + WP_CLI::error( sprintf( 'Could not fetch %s (HTTP code %d)', $url, $response->status_code ) ); + } + + return json_decode( $response->body, true ); + } + + /** + * Lists the bundled packages of a lockfile with their versions. + * + * @param array $lock Decoded composer.lock. + * + * @return array Package name => version without leading 'v', + * sorted by name. + */ + public static function get_packages( array $lock ) { + $packages = []; + + foreach ( [ 'packages', 'packages-dev' ] as $section ) { + if ( empty( $lock[ $section ] ) ) { + continue; + } + + foreach ( $lock[ $section ] as $package ) { + if ( ! self::is_bundled_package( $package['name'] ) ) { + continue; + } + + $packages[ $package['name'] ] = ltrim( $package['version'], 'v' ); + } + } + + ksort( $packages ); + + return $packages; + } + + /** + * Gets the closed milestones of a package that shipped in a bundle + * release: newer than the version the previous release locked, and not + * newer than the version this release locks. A package without a + * previous version is newly bundled, so all of its releases count. + * + * @param string $package Package name. + * @param string|null $previous_version Version locked by the previous release. + * @param string $current_version Version locked by this release. + * + * @return array + */ + public static function get_shipped_milestones( $package, $previous_version, $current_version ) { + return self::filter_shipped_milestones( + GitHub::get_project_milestones( $package, [ 'state' => 'closed' ] ), + $previous_version, + $current_version + ); + } + + /** + * Keeps the milestones whose version lies in (previous, current]. A + * version that cannot be compared, such as `dev-main`, does not bound; + * milestones whose title is not a version are dropped. + * + * @param array $milestones Milestone objects with a `title`. + * @param string|null $previous_version Version locked by the previous release. + * @param string|null $current_version Version locked by this release. + * + * @return array + */ + public static function filter_shipped_milestones( array $milestones, $previous_version, $current_version ) { + return array_values( + array_filter( + $milestones, + static function ( $milestone ) use ( $previous_version, $current_version ) { + $title = ltrim( $milestone->title, 'v' ); + + if ( ! self::is_version( $title ) ) { + return false; + } + + if ( self::is_version( $previous_version ) + && ! version_compare( $title, $previous_version, '>' ) ) { + return false; + } + + if ( self::is_version( $current_version ) + && version_compare( $title, $current_version, '>' ) ) { + return false; + } + + return true; + } + ) + ); + } + + /** + * Checks whether a package's contributors and release notes belong to a + * bundle release. + * + * @param string $name Package name. + * + * @return bool + */ + public static function is_bundled_package( $name ) { + return (bool) preg_match( '#^wp-cli/.+-command$#', $name ) + || in_array( $name, self::OTHER_PACKAGES, true ); + } + + /** + * Checks whether a string is a version that can be compared, such as + * `2.12.0` or `3.0.0-beta1`, as opposed to `dev-main` or `3.0.0 (docs)`. + * + * @param string|null $version Version string, or null when unknown. + * + * @return bool + */ + public static function is_version( $version ) { + return null !== $version && (bool) preg_match( '/^\d+(\.\d+)*(-[0-9A-Za-z.]+)?$/', $version ); + } +} diff --git a/.maintenance/src/Contrib_List_Command.php b/.maintenance/src/Contrib_List_Command.php index d62fb28..bf69cec 100644 --- a/.maintenance/src/Contrib_List_Command.php +++ b/.maintenance/src/Contrib_List_Command.php @@ -21,6 +21,19 @@ final class Contrib_List_Command { * : Name of one or more milestones to fetch the release notes for. If none * are passed, the current open one is assumed. * + * [--release=] + * : Version of the bundle release, e.g. 3.0.0. Uses the milestone with + * that title in wp-cli/wp-cli-bundle, wp-cli/wp-cli and wp-cli/handbook + * instead of the currently open one, and reads the bundled packages from + * the composer.lock at the release tag of wp-cli/wp-cli-bundle. Only + * applies when no repo is passed. + * + * [--bundle-ref=] + * : Branch, tag or commit of wp-cli/wp-cli-bundle whose composer.lock lists + * the packages and versions shipped in this release. Defaults to the + * release tag when --release is passed and the tag exists, and to the + * default branch otherwise. Only applies when no repo is passed. + * * [--format=] * : Render output in a specific format. * --- @@ -46,14 +59,12 @@ public function __invoke( $args, $assoc_args ) { $milestone_names = $args; + $release = Utils\get_flag_value( $assoc_args, 'release' ); + $bundle_ref = Utils\get_flag_value( $assoc_args, 'bundle-ref' ); + if ( empty( $repos ) ) { $use_bundle = true; - $repos = [ - 'wp-cli/wp-cli-bundle', - 'wp-cli/wp-cli', - 'wp-cli/handbook', - 'wp-cli/wp-cli.github.com', - ]; + $repos = Bundle::RELEASE_REPOS; } $contributors = array(); @@ -96,21 +107,15 @@ public function __invoke( $args, $assoc_args ) { ); } } else { - $milestones = GitHub::get_project_milestones( $repo ); - // Cheap way to get the latest milestone - $milestone = array_shift( $milestones ); - if ( ! $milestone ) { - continue; - } + $milestone = Bundle::get_release_milestone( $repo, $use_bundle ? $release : null ); + $milestones = $milestone ? [ $milestone ] : []; } - $entries = array(); + foreach ( $milestones as $milestone ) { - WP_CLI::debug( "Using milestone '{$milestone->title}' for repo '{$repo}'", 'release-notes' ); - WP_CLI::log( 'Current open ' . $repo . ' milestone: ' . $milestone->title ); + WP_CLI::debug( "Using milestone '{$milestone->title}' for repo '{$repo}'", 'contrib-list' ); $pull_requests = GitHub::get_project_milestone_pull_requests( $repo, $milestone->number ); $repo_contributors = GitHub::parse_contributors_from_pull_requests( $pull_requests ); - WP_CLI::log( ' - Contributors: ' . count( $repo_contributors ) ); - WP_CLI::log( ' - Pull requests: ' . count( $pull_requests ) ); + WP_CLI::debug( count( $repo_contributors ) . ' contributors, ' . count( $pull_requests ) . " pull requests in '{$repo}' milestone '{$milestone->title}'", 'contrib-list' ); $pull_request_count += count( $pull_requests ); $contributors = array_merge( $contributors, $repo_contributors ); } @@ -118,80 +123,36 @@ public function __invoke( $args, $assoc_args ) { if ( $use_bundle ) { // Identify all command dependencies and their contributors + list( $ref, $lock ) = Bundle::get_release_lock( $release, $bundle_ref ); + list( $previous_ref, $previous_lock ) = Bundle::get_previous_release_lock( $release ); - $bundle = 'wp-cli/wp-cli-bundle'; + WP_CLI::debug( "Bundled packages read from wp-cli/wp-cli-bundle@{$ref}, previous release: " . ( $previous_ref ?: 'none' ), 'contrib-list' ); + + $previous_versions = $previous_lock ? Bundle::get_packages( $previous_lock ) : []; + + foreach ( Bundle::get_packages( $lock ) as $package_name => $version ) { + $previous_version = isset( $previous_versions[ $package_name ] ) ? $previous_versions[ $package_name ] : null; - $milestones = GitHub::get_project_milestones( $bundle, array( 'state' => 'closed' ) ); - $milestone = array_reduce( - $milestones, - function ( $tag, $milestone ) { - if ( ! $tag ) { - return $milestone->title; - } - return version_compare( $milestone->title, $tag, '>' ) ? $milestone->title : $tag; - } - ); - $tag = ! empty( $milestone ) ? "v{$milestone}" : GitHub::get_default_branch( $bundle ); - - $composer_lock_url = sprintf( 'https://raw.githubusercontent.com/%s/%s/composer.lock', $bundle, $tag ); - WP_CLI::log( 'Fetching ' . $composer_lock_url ); - $response = Utils\http_request( 'GET', $composer_lock_url ); - if ( 200 !== $response->status_code ) { - WP_CLI::error( sprintf( 'Could not fetch composer.json (HTTP code %d)', $response->status_code ) ); - } - $composer_json = json_decode( $response->body, true ); - - // TODO: Only need for initial v2. - $composer_json['packages'][] = array( - 'name' => 'wp-cli/i18n-command', - 'version' => 'v2', - ); - usort( - $composer_json['packages'], - function ( $a, $b ) { - return $a['name'] < $b['name'] ? -1 : 1; - } - ); - - foreach ( $composer_json['packages'] as $package ) { - $package_name = $package['name']; - $version_constraint = str_replace( 'v', '', $package['version'] ); - if ( ! preg_match( '#^wp-cli/.+-command$#', $package_name ) - && ! in_array( - $package_name, - array( - 'wp-cli/wp-cli-tests', - 'wp-cli/regenerate-readme', - 'wp-cli/autoload-splitter', - 'wp-cli/wp-config-transformer', - 'wp-cli/php-cli-tools', - 'wp-cli/spyc', - ), - true - ) ) { - continue; - } // Closed milestones denote a tagged release - $milestones = GitHub::get_project_milestones( $package_name, array( 'state' => 'closed' ) ); - $milestone_ids = array(); - $milestone_titles = array(); - foreach ( $milestones as $milestone ) { - if ( ! version_compare( $milestone->title, $version_constraint, '>' ) ) { - continue; - } - $milestone_ids[] = $milestone->number; - $milestone_titles[] = $milestone->title; - } + $milestones = Bundle::get_shipped_milestones( $package_name, $previous_version, $version ); + // No shipped releases for this milestone. - if ( empty( $milestone_ids ) ) { + if ( empty( $milestones ) ) { continue; } - WP_CLI::log( 'Closed ' . $package_name . ' milestone(s): ' . implode( ', ', $milestone_titles ) ); - foreach ( $milestone_ids as $milestone_id ) { - $pull_requests = GitHub::get_project_milestone_pull_requests( $package_name, $milestone_id ); + + $milestone_titles = array_map( + static function ( $milestone ) { + return $milestone->title; + }, + $milestones + ); + WP_CLI::debug( "Closed {$package_name} milestone(s): " . implode( ', ', $milestone_titles ), 'contrib-list' ); + + foreach ( $milestones as $milestone ) { + $pull_requests = GitHub::get_project_milestone_pull_requests( $package_name, $milestone->number ); $repo_contributors = GitHub::parse_contributors_from_pull_requests( $pull_requests ); - WP_CLI::log( ' - Contributors: ' . count( $repo_contributors ) ); - WP_CLI::log( ' - Pull requests: ' . count( $pull_requests ) ); + WP_CLI::debug( count( $repo_contributors ) . ' contributors, ' . count( $pull_requests ) . " pull requests in '{$package_name}' milestone '{$milestone->title}'", 'contrib-list' ); $pull_request_count += count( $pull_requests ); $contributors = array_merge( $contributors, $repo_contributors ); } diff --git a/.maintenance/src/GitHub.php b/.maintenance/src/GitHub.php index 1791a6e..aaa879f 100644 --- a/.maintenance/src/GitHub.php +++ b/.maintenance/src/GitHub.php @@ -26,9 +26,28 @@ public static function get_project_milestones( $args['per_page'] = 100; - list( $body, $headers ) = self::request( $request_url, $args ); + $milestones = []; + do { + list( $body, $headers ) = self::request( $request_url, $args ); + foreach ( $body as $milestone ) { + $milestones[] = $milestone; + } + $args = array(); + $request_url = false; + // Set $request_url to 'rel="next" if present' + if ( ! empty( $headers['Link'] ) ) { + $bits = explode( ',', $headers['Link'] ); + foreach ( $bits as $bit ) { + if ( false !== stripos( $bit, 'rel="next"' ) ) { + $hrefandrel = explode( '; ', $bit ); + $request_url = trim( trim( $hrefandrel[0] ), '<>' ); + break; + } + } + } + } while ( $request_url ); - return $body; + return $milestones; } /** @@ -122,7 +141,13 @@ public static function get_release_by_tag( $args['per_page'] = 100; - list( $body, $headers ) = self::request( $request_url, $args ); + $result = self::request( $request_url, $args ); + + if ( ! $result ) { + return false; + } + + list( $body, $headers ) = $result; return $body; } diff --git a/.maintenance/src/Milestones_After_Command.php b/.maintenance/src/Milestones_After_Command.php index 9d03f4f..df422c6 100644 --- a/.maintenance/src/Milestones_After_Command.php +++ b/.maintenance/src/Milestones_After_Command.php @@ -13,7 +13,7 @@ final class Milestones_After_Command { * : Name of the repository to fetch the milestones for. * * - * : Milestone to serve as treshold. + * : Milestone to serve as threshold. * * @when before_wp_load */ diff --git a/.maintenance/src/Milestones_Since_Command.php b/.maintenance/src/Milestones_Since_Command.php index d7ef4d0..beb3de6 100644 --- a/.maintenance/src/Milestones_Since_Command.php +++ b/.maintenance/src/Milestones_Since_Command.php @@ -7,7 +7,7 @@ final class Milestones_Since_Command { /** * Retrieves the milestones that were closed for a given repository after a - * specific date treshold. + * specific date threshold. * * ## OPTIONS * diff --git a/.maintenance/src/Release_Command.php b/.maintenance/src/Release_Command.php index d6981cd..8199fb8 100644 --- a/.maintenance/src/Release_Command.php +++ b/.maintenance/src/Release_Command.php @@ -289,42 +289,8 @@ static function ( $repo ) use ( $exclude ) { } private function get_bundle_repos() { - $repos = []; - $default_branch = GitHub::get_default_branch( 'wp-cli/wp-cli-bundle' ); - $composer_lock_url = "https://raw.githubusercontent.com/wp-cli/wp-cli-bundle/{$default_branch}/composer.lock"; - $response = Utils\http_request( 'GET', $composer_lock_url ); - if ( 200 !== $response->status_code ) { - WP_CLI::error( sprintf( 'Could not fetch composer.json (HTTP code %d)', $response->status_code ) ); - } - $composer_json = json_decode( $response->body, true ); - - usort( - $composer_json['packages'], - static function ( $a, $b ) { - return $a['name'] < $b['name'] ? - 1 : 1; - } - ); - - foreach ( $composer_json['packages'] as $package ) { - $package_name = $package['name']; - if ( ! preg_match( '#^wp-cli/.+-command$#', $package_name ) - && ! in_array( - $package_name, - array( - 'wp-cli/wp-cli-tests', - 'wp-cli/regenerate-readme', - 'wp-cli/autoload-splitter', - 'wp-cli/wp-config-transformer', - 'wp-cli/php-cli-tools', - 'wp-cli/spyc', - ), - true - ) ) { - continue; - } - $repos[] = $package_name; - } + $default_branch = GitHub::get_default_branch( Bundle::REPO ); - return $repos; + return array_keys( Bundle::get_packages( Bundle::get_lock( $default_branch ) ) ); } } diff --git a/.maintenance/src/Release_Notes_Command.php b/.maintenance/src/Release_Notes_Command.php index 8565659..f4b5cb7 100644 --- a/.maintenance/src/Release_Notes_Command.php +++ b/.maintenance/src/Release_Notes_Command.php @@ -19,6 +19,19 @@ final class Release_Notes_Command { * : Name of one or more milestones to fetch the release notes for. If none * are passed, the current open one is assumed. * + * [--release=] + * : Version of the bundle release, e.g. 3.0.0. Uses the milestone with + * that title in wp-cli/wp-cli-bundle, wp-cli/wp-cli and wp-cli/handbook + * instead of the currently open one, and reads the bundled packages from + * the composer.lock at the release tag of wp-cli/wp-cli-bundle. Only + * applies when no repo is passed. + * + * [--bundle-ref=] + * : Branch, tag or commit of wp-cli/wp-cli-bundle whose composer.lock lists + * the packages and versions shipped in this release. Defaults to the + * release tag when --release is passed and the tag exists, and to the + * default branch otherwise. Only applies when no repo is passed. + * * [--source=] * : Choose source from where to copy content. * --- @@ -48,8 +61,10 @@ public function __invoke( $args, $assoc_args ) { $milestone_names = $args; - $source = Utils\get_flag_value( $assoc_args, 'source', 'release' ); - $format = Utils\get_flag_value( $assoc_args, 'format', 'markdown' ); + $source = Utils\get_flag_value( $assoc_args, 'source', 'release' ); + $format = Utils\get_flag_value( $assoc_args, 'format', 'markdown' ); + $release = Utils\get_flag_value( $assoc_args, 'release' ); + $bundle_ref = Utils\get_flag_value( $assoc_args, 'bundle-ref' ); if ( $repo ) { $this->get_repo_release_notes( @@ -62,29 +77,13 @@ public function __invoke( $args, $assoc_args ) { return; } - $this->get_bundle_release_notes( $source, $format ); + $this->get_bundle_release_notes( $source, $format, $release, $bundle_ref ); } - private function get_bundle_release_notes( $source, $format ) { - // Get the release notes for the lowest open project milestones. - foreach ( - array( - 'wp-cli/wp-cli-bundle', - 'wp-cli/wp-cli', - 'wp-cli/handbook', - 'wp-cli/wp-cli.github.com', - ) as $repo - ) { - $milestones = GitHub::get_project_milestones( $repo ); - $milestone = array_reduce( - $milestones, - static function ( $latest, $milestone ) { - if ( null === $latest ) { - return $milestone; - } - return version_compare( $milestone->title, $latest->title, '<' ) ? $milestone : $latest; - } - ); + private function get_bundle_release_notes( $source, $format, $release, $bundle_ref ) { + // Get the release notes for the milestones of the release repositories. + foreach ( Bundle::RELEASE_REPOS as $repo ) { + $milestone = Bundle::get_release_milestone( $repo, $release ); if ( ! $milestone ) { WP_CLI::debug( "No milestone found for repo '{$repo}'", 'release-notes' ); @@ -104,84 +103,27 @@ static function ( $latest, $milestone ) { } // Identify all command dependencies and their release notes + list( $ref, $lock ) = Bundle::get_release_lock( $release, $bundle_ref ); + list( $previous_ref, $previous_lock ) = Bundle::get_previous_release_lock( $release ); - $bundle = 'wp-cli/wp-cli-bundle'; - - $milestones = GitHub::get_project_milestones( - $bundle, - array( 'state' => 'closed' ) - ); + WP_CLI::debug( "Bundled packages read from wp-cli/wp-cli-bundle@{$ref}, previous release: " . ( $previous_ref ?: 'none' ), 'release-notes' ); - $milestone = array_reduce( - $milestones, - function ( $tag, $milestone ) { - if ( ! $tag ) { - return $milestone->title; - } - return version_compare( $milestone->title, $tag, '>' ) ? $milestone->title : $tag; - } - ); + $previous_versions = $previous_lock ? Bundle::get_packages( $previous_lock ) : []; - $tag = ! empty( $milestone ) ? "v{$milestone}" : GitHub::get_default_branch( $bundle ); + foreach ( Bundle::get_packages( $lock ) as $package_name => $version ) { + $previous_version = isset( $previous_versions[ $package_name ] ) ? $previous_versions[ $package_name ] : null; - $composer_lock_url = sprintf( - 'https://raw.githubusercontent.com/%s/%s/composer.lock', - $bundle, - $tag - ); - $response = Utils\http_request( 'GET', $composer_lock_url ); - if ( 200 !== $response->status_code ) { - WP_CLI::error( - sprintf( - 'Could not fetch composer.json (HTTP code %d)', - $response->status_code - ) - ); - } - $composer_json = json_decode( $response->body, true ); - - usort( - $composer_json['packages'], - function ( $a, $b ) { - return $a['name'] < $b['name'] ? - 1 : 1; - } - ); + // Closed milestones denote a tagged release + $milestones = Bundle::get_shipped_milestones( $package_name, $previous_version, $version ); - foreach ( $composer_json['packages'] as $package ) { - $package_name = $package['name']; - $version_constraint = str_replace( 'v', '', $package['version'] ); - if ( ! preg_match( '#^wp-cli/.+-command$#', $package_name ) - && ! in_array( - $package_name, - array( - 'wp-cli/wp-cli-tests', - 'wp-cli/regenerate-readme', - 'wp-cli/autoload-splitter', - 'wp-cli/wp-config-transformer', - 'wp-cli/php-cli-tools', - 'wp-cli/spyc', - ), - true - ) ) { + if ( empty( $milestones ) ) { + WP_CLI::debug( "No releases of '{$package_name}' shipped since " . ( $previous_ref ?: 'the beginning' ), 'release-notes' ); continue; } WP_CLI::log( $this->repo_heading( $package_name, $format ) ); - // Closed milestones denote a tagged release - $milestones = GitHub::get_project_milestones( - $package_name, - array( 'state' => 'closed' ) - ); foreach ( $milestones as $milestone ) { - if ( ! version_compare( - $milestone->title, - $version_constraint, - '>' - ) ) { - continue; - } - $this->get_repo_release_notes( $package_name, $milestone->title, diff --git a/.readme-partials/INSTALLATION.md b/.readme-partials/INSTALLATION.md index d4d062e..b36d31c 100644 --- a/.readme-partials/INSTALLATION.md +++ b/.readme-partials/INSTALLATION.md @@ -8,7 +8,7 @@ In order to make code changes to WP-CLI, you'll need to set up this `wp-cli-dev` Before you can proceed further, you'll need to make sure you have [Composer](https://getcomposer.org/), PHP, and a functioning MySQL or MariaDB server on your local machine. -Once the prequisites are met, clone the GitHub repository and run the installation process: +Once the prerequisites are met, clone the GitHub repository and run the installation process: ```bash git clone https://github.com/wp-cli/wp-cli-dev wp-cli-dev diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..46364c5 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,23 @@ +[default] +extend-ignore-re = [ + "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", + "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", + "(#|//)\\s*spellchecker:ignore-next-line\\n.*" +] +[files] +extend-exclude = [ + "*-command/*", + ".git/*", + "builds/*", + "dashboard/*", + "handbook/*", + "mustache.php/*", + "process/*", + "restful/*", + "spyc/*", + "vendor/*", + "wordpress-core/*", + "wp-cli/*", + "wp-cli.github.com/*", + "wp-super-cache-cli/*" +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..908c63d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,140 @@ +# Instructions + +This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file. + +## Best Practices for Code Contributions + +When contributing to this package, please adhere to the following guidelines: + +* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns. +* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package. +* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory. +* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation. + +### Building and running + +Run the narrowest check that covers your change: + +| You changed | Run | +| --- | --- | +| Any PHP file | `composer phpcs -- ` and `composer phpstan` | +| Logic with unit test coverage | `composer phpunit -- --filter ` | +| One feature file | `composer behat -- features/.feature` | +| One scenario | `composer behat -- features/.feature:` | + +After a failure, `composer behat-rerun` re-runs only the scenarios that failed, and `composer behat -- --stop-on-failure` bails out at the first one instead of working through the rest. + +`composer test` runs every suite, the whole Behat suite included. In most packages that means installing WordPress from scratch once per scenario, which takes tens of minutes and needs `jq`, a MySQL or MariaDB client with a prepared test database, and network access to WordPress.org. Run it before opening a pull request when you have touched something cross-cutting. Do not run it to check a two-line change. + +Set these first, so the tools report problems compactly instead of drawing progress bars and ANSI color into output you are going to read back: + +```bash +export NO_COLOR=1 +export WP_CLI_TEST_QUIET=1 +``` + +Note that a green `composer test` is not the same as a green CI. `actionlint` and `typos` also run on every pull request and are not part of it. + +### Useful Composer Commands + +The project uses Composer to manage dependencies and run scripts. The following commands are available: + +* `composer install`: Install dependencies. +* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests. +* `composer lint`: Check for syntax errors. +* `composer lint-gherkin`: Check the Behat feature files for style violations. +* `composer phpcs`: Check for code style violations. +* `composer phpcbf`: Automatically fix code style violations. +* `composer phpstan`: Run static analysis. +* `composer phpunit`: Run unit tests. +* `composer behat`: Run behavior-driven tests. + +### Coding Style + +The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them. + +## Documentation + +The `README.md` file might be generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). In that case, changes need to be made against the corresponding part of the codebase. + +### Inline Documentation + +Only write high-value comments if at all. Avoid talking to the user through comments. + +## Testing + +The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis. + +* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`. +* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`. +* **Static analysis** is performed with PHPStan. + +All tests are run on GitHub Actions for every pull request. + +When writing tests, aim to follow existing patterns. Key conventions include: + +* When adding tests, first examine existing tests to understand and conform to established conventions. +* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class. +* For Behat tests, only WP-CLI commands installed in `composer.json` can be run. + +### Behat Steps + +WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests. + +> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` . + +#### Given + +* `Given an empty directory` - Creates an empty directory. +* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory. +* `Given an empty cache` - Clears the WP-CLI cache directory. +* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents. +* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex. +* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL. +* `Given WP files` - Download WordPress files without installing. +* `Given wp-config.php` - Create a wp-config.php file using `wp config create`. +* `Given a database` - Creates an empty database. +* `Given a WP install(ation)` - Installs WordPress. +* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory. +* `Given a WP install(ation) with Composer` - Installs WordPress with Composer. +* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory. +* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite. +* `Given these installed and active plugins:` - Installs and activates one or more plugins. +* `Given a custom wp-content directory` - Configure a custom `wp-content` directory. +* `Given download:` - Download multiple files into the given destinations. +* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable. +* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version. +* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub. +* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable. +* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string. +* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency. +* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory. +* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory. + +#### When + +* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background. +* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command. +* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory. +* `When /^I (run|try) the previous command again$/` - Run or try the previous command again. + +#### Then + +* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command. +* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR. +* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value. +* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value. +* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows. +* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows. +* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT. +* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT. +* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values. +* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content. +* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty. +* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty. +* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version. +* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents. +* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex. +* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex. +* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not). +* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`. diff --git a/README.md b/README.md index 5e9df3f..fc7bbf4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Sets up a WP-CLI development environment that allows for easy development across This allows easy development across all packages and contains additional maintenance commands that simplify repository chores and the release process. - +[![Testing](https://github.com/wp-cli/wp-cli-dev/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/wp-cli-dev/actions/workflows/testing.yml) Quick links: [Installation](#installation) | [Development](#development) | [Using](#using) | [Contributing](#contributing) | [Support](#support) @@ -21,7 +21,7 @@ In order to make code changes to WP-CLI, you'll need to set up this `wp-cli-dev` Before you can proceed further, you'll need to make sure you have [Composer](https://getcomposer.org/), PHP, and a functioning MySQL or MariaDB server on your local machine. -Once the prequisites are met, clone the GitHub repository and run the installation process: +Once the prerequisites are met, clone the GitHub repository and run the installation process: ```bash git clone https://github.com/wp-cli/wp-cli-dev wp-cli-dev @@ -64,13 +64,35 @@ wp maintenance Lists all contributors to this release. ~~~ -wp maintenance contrib-list [--format=] +wp maintenance contrib-list [] [...] [--release=] [--bundle-ref=] [--format=] ~~~ Run within the main WP-CLI project repository. **OPTIONS** + [] + Name of the repository to fetch the release notes for. If no user/org + was provided, 'wp-cli' org is assumed. If no repo is passed, release + notes for the entire org state since the last bundle release are fetched. + + [...] + Name of one or more milestones to fetch the release notes for. If none + are passed, the current open one is assumed. + + [--release=] + Version of the bundle release, e.g. 3.0.0. Uses the milestone with + that title in wp-cli/wp-cli-bundle, wp-cli/wp-cli and wp-cli/handbook + instead of the currently open one, and reads the bundled packages from + the composer.lock at the release tag of wp-cli/wp-cli-bundle. Only + applies when no repo is passed. + + [--bundle-ref=] + Branch, tag or commit of wp-cli/wp-cli-bundle whose composer.lock lists + the packages and versions shipped in this release. Defaults to the + release tag when --release is passed and the tag exists, and to the + default branch otherwise. Only applies when no repo is passed. + [--format=] Render output in a specific format. --- @@ -96,7 +118,7 @@ wp maintenance milestones-after Name of the repository to fetch the milestones for. - Milestone to serve as treshold. + Milestone to serve as threshold. @@ -108,7 +130,7 @@ Retrieves the milestones that were closed for a given repository after a wp maintenance milestones-since ~~~ -specific date treshold. +specific date threshold. **OPTIONS** @@ -144,7 +166,7 @@ wp maintenance release-date Gets the release notes for one or more milestones of a repository. ~~~ -wp maintenance release-notes [] [...] [--source=] [--format=] +wp maintenance release-notes [] [...] [--release=] [--bundle-ref=] [--source=] [--format=] ~~~ **OPTIONS** @@ -156,7 +178,20 @@ wp maintenance release-notes [] [...] [--source=] [--fo [...] Name of one or more milestones to fetch the release notes for. If none - are passed, the currently open one is assumed. + are passed, the current open one is assumed. + + [--release=] + Version of the bundle release, e.g. 3.0.0. Uses the milestone with + that title in wp-cli/wp-cli-bundle, wp-cli/wp-cli and wp-cli/handbook + instead of the currently open one, and reads the bundled packages from + the composer.lock at the release tag of wp-cli/wp-cli-bundle. Only + applies when no repo is passed. + + [--bundle-ref=] + Branch, tag or commit of wp-cli/wp-cli-bundle whose composer.lock lists + the packages and versions shipped in this release. Defaults to the + release tag when --release is passed and the tag exists, and to the + default branch otherwise. Only applies when no repo is passed. [--source=] Choose source from where to copy content. @@ -207,7 +242,7 @@ We appreciate you taking the initiative to contribute to this project. Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. -For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policies and guidelines. +For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. ### Reporting a bug @@ -223,8 +258,12 @@ Want to contribute a new feature? Please first [open a new issue](https://github Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. +### License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + ## Support -Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support +GitHub issues aren't for general support questions. For support resources and next steps, see the WP-CLI Support page: https://make.wordpress.org/cli/handbook/support/ diff --git a/composer.json b/composer.json index 5e6784c..ebdd611 100644 --- a/composer.json +++ b/composer.json @@ -180,6 +180,18 @@ "type": "path", "url": "i18n-command" }, + { + "type": "path", + "url": "ability-command" + }, + { + "type": "path", + "url": "ai-command" + }, + { + "type": "path", + "url": "block-command" + }, { "type": "path", "url": "wp-cli-tests" @@ -194,11 +206,16 @@ } ], "require": { - "php": ">=5.6.20", - "ext-json": "*", + "php": ">=7.2.24", "ext-dom": "*", + "ext-json": "*", + "johnpbloch/wordpress-core": "dev-master", + "johnpbloch/wordpress-core-installer": "^1.0 || ^2.0", + "wp-cli/ability-command": "^1.0@dev", + "wp-cli/ai-command": "^1.0@dev", "wp-cli/admin-command": "dev-main", "wp-cli/automated-tests": "dev-main", + "wp-cli/block-command": "^1.0@dev", "wp-cli/cache-command": "dev-main", "wp-cli/checksum-command": "dev-main", "wp-cli/config-command": "dev-main", @@ -221,7 +238,7 @@ "wp-cli/media-command": "dev-main", "wp-cli/mustangostang-spyc": "dev-master as 0.6.x-dev", "wp-cli/package-command": "dev-main", - "wp-cli/php-cli-tools": "dev-master as 0.11.x-dev", + "wp-cli/php-cli-tools": "dev-main as 0.13.x-dev", "wp-cli/profile-command": "dev-main", "wp-cli/restful": "dev-main", "wp-cli/rewrite-command": "dev-main", @@ -237,9 +254,7 @@ "wp-cli/wp-cli-bundle": "dev-main", "wp-cli/wp-cli-tests": "dev-main", "wp-cli/wp-config-transformer": "dev-main as 1.2.x-dev", - "wp-cli/wp-super-cache-cli": "dev-main", - "johnpbloch/wordpress-core-installer": "^1.0 || ^2.0", - "johnpbloch/wordpress-core": "dev-master" + "wp-cli/wp-super-cache-cli": "dev-main" }, "require-dev": { "roave/security-advisories": "dev-latest" @@ -249,15 +264,19 @@ }, "config": { "process-timeout": 7200, + "preferred-install": { + "wp-cli/*": "source" + }, "sort-packages": true, "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, "johnpbloch/wordpress-core-installer": true, - "dealerdirect/phpcodesniffer-composer-installer": true + "phpstan/extension-installer": true } }, "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "2.0.x-dev" }, "commands": [ "maintenance", @@ -300,8 +319,8 @@ "minimum-stability": "dev", "prefer-stable": true, "scripts": { - "pre-install-cmd": "php .maintenance/clone-all-repositories.php", - "pre-update-cmd": "php .maintenance/clone-all-repositories.php", + "pre-install-cmd": "bash .maintenance/clone-all-repositories.sh", + "pre-update-cmd": "bash .maintenance/clone-all-repositories.sh", "post-install-cmd": [ "php .maintenance/symlink-vendor-folders.php", "php .maintenance/phpstorm.exclude-recursive-folders.php" @@ -314,11 +333,13 @@ "behat-rerun": "rerun-behat-tests", "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", + "phpstan": "run-phpstan-tests", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", "test": [ "@lint", "@phpcs", + "@phpstan", "@phpunit", "@behat" ] diff --git a/foreach-bundle b/foreach-bundle index 4c6d86a..8396bf4 100755 --- a/foreach-bundle +++ b/foreach-bundle @@ -6,7 +6,7 @@ get_bundle_dependencies() { echo "wp-cli/wp-cli-bundle" - curl --silent "https://raw.githubusercontent.com/wp-cli/wp-cli-bundle/master/composer.json" \ + curl --silent "https://raw.githubusercontent.com/wp-cli/wp-cli-bundle/main/composer.json" \ | jq --raw-output '."require" | keys[] | select(startswith("wp-cli/"))' } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 142a306..58dbf40 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -5,8 +5,8 @@ @@ -38,7 +38,7 @@ - +