From 277d9a81d79fe654596f11f0f3854f6663075b9a Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 2 Jun 2026 15:40:30 +0000 Subject: [PATCH 1/5] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d5e319e5..617a49f0 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false From b448f395c6f36a4ff74131b9c3f24e1559db3711 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Mon, 8 Jun 2026 16:57:35 +0000 Subject: [PATCH 2/5] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 617a49f0..ffb6f8fd 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,7 +31,7 @@ jobs: - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: 'latest' ini-values: zend.assertions=1, error_reporting=-1, display_errors=On From becf3158e928fa9ae371a14997665490730e6e04 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:04:31 +0200 Subject: [PATCH 3/5] Add `plugin/theme download` subcommands runnable before WordPress load (#527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Hervé THOMAS Co-authored-by: Alain Schlesser Co-authored-by: Pascal Birchler --- composer.json | 2 + extension-command.php | 4 + features/extension-download.feature | 169 ++++++++++++++++++++++++++++ phpcs.xml.dist | 2 +- phpstan.neon.dist | 1 + src/Plugin_Download_Command.php | 159 ++++++++++++++++++++++++++ src/Theme_Download_Command.php | 159 ++++++++++++++++++++++++++ tests/phpstan/scan-files.php | 14 +++ 8 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 features/extension-download.feature create mode 100644 src/Plugin_Download_Command.php create mode 100644 src/Theme_Download_Command.php create mode 100644 tests/phpstan/scan-files.php diff --git a/composer.json b/composer.json index d12c0bbc..ebc117cc 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "plugin search", "plugin status", "plugin check-update", + "plugin download", "plugin toggle", "plugin uninstall", "plugin update", @@ -82,6 +83,7 @@ "theme search", "theme status", "theme check-update", + "theme download", "theme update", "theme mod list", "theme auto-updates", diff --git a/extension-command.php b/extension-command.php index 3cc65c80..f42d46ee 100644 --- a/extension-command.php +++ b/extension-command.php @@ -8,6 +8,8 @@ if ( file_exists( $wpcli_extension_autoloader ) ) { require_once $wpcli_extension_autoloader; } +require_once __DIR__ . '/src/Plugin_Download_Command.php'; +require_once __DIR__ . '/src/Theme_Download_Command.php'; $wpcli_extension_requires_wp_5_5 = [ 'before_invoke' => static function () { @@ -18,8 +20,10 @@ ]; WP_CLI::add_command( 'plugin', 'Plugin_Command' ); +WP_CLI::add_command( 'plugin download', 'Plugin_Download_Command' ); WP_CLI::add_command( 'plugin auto-updates', 'Plugin_AutoUpdates_Command', $wpcli_extension_requires_wp_5_5 ); WP_CLI::add_command( 'theme', 'Theme_Command' ); +WP_CLI::add_command( 'theme download', 'Theme_Download_Command' ); WP_CLI::add_command( 'theme auto-updates', 'Theme_AutoUpdates_Command', $wpcli_extension_requires_wp_5_5 ); WP_CLI::add_command( 'theme mod', 'Theme_Mod_Command' ); diff --git a/features/extension-download.feature b/features/extension-download.feature new file mode 100644 index 00000000..f05f5eef --- /dev/null +++ b/features/extension-download.feature @@ -0,0 +1,169 @@ +Feature: Download WordPress.org extensions without loading WordPress + + Scenario: Downloading a plugin package works before WordPress is loaded + Given an empty directory + + When I run `wp plugin download debug-bar` + Then STDOUT should contain: + """ + Downloading debug-bar + """ + And STDOUT should contain: + """ + Success: Downloaded plugin package to + """ + And save STDOUT 'Success: Downloaded plugin package to (.+)' as {DOWNLOADED_PLUGIN} + And the {DOWNLOADED_PLUGIN} file should exist + And STDERR should be empty + + Scenario: Downloading a plugin package to a custom path + Given an empty directory + + When I run `wp plugin download debug-bar --target-path=/tmp/wp-cli-download-test-plugin` + Then STDOUT should contain: + """ + Success: Downloaded plugin package to + """ + And save STDOUT 'Success: Downloaded plugin package to (.+)' as {DOWNLOADED_PLUGIN} + And the {DOWNLOADED_PLUGIN} file should exist + And STDERR should be empty + + Scenario: Downloading a specific version of a plugin + Given an empty directory + + When I run `wp plugin download debug-bar --version=1.1` + Then STDOUT should contain: + """ + Downloading debug-bar (1.1) + """ + And STDOUT should contain: + """ + Success: Downloaded plugin package to + """ + And STDERR should be empty + + Scenario: Downloading a non-existent version of a plugin fails with clear error + Given an empty directory + + When I try `wp plugin download debug-bar --version=9.9.9` + Then STDERR should contain: + """ + Error: Can't find the requested plugin's version 9.9.9 + """ + And the return code should be 1 + + Scenario: Downloading a plugin with --force overwrites existing file + Given an empty directory + + When I run `wp plugin download debug-bar` + And I run `wp plugin download debug-bar --force` + Then STDOUT should contain: + """ + Success: Downloaded plugin package to + """ + And STDERR should be empty + + Scenario: Downloading a plugin without --force fails if destination exists + Given an empty directory + + When I run `wp plugin download debug-bar` + And I try `wp plugin download debug-bar` + Then STDERR should contain: + """ + Error: Destination file already exists: + """ + And the return code should be 1 + + Scenario: Downloading an unknown plugin fails with a clear error + Given an empty directory + + When I try `wp plugin download this-plugin-does-not-exist-xyz-abc-123` + Then STDERR should contain: + """ + Error: The 'this-plugin-does-not-exist-xyz-abc-123' plugin could not be found. + """ + And the return code should be 1 + + Scenario: Downloading a theme package works before WordPress is loaded + Given an empty directory + + When I run `wp theme download twentytwelve` + Then STDOUT should contain: + """ + Downloading twentytwelve + """ + And STDOUT should contain: + """ + Success: Downloaded theme package to + """ + And save STDOUT 'Success: Downloaded theme package to (.+)' as {DOWNLOADED_THEME} + And the {DOWNLOADED_THEME} file should exist + And STDERR should be empty + + Scenario: Downloading a theme package to a custom path + Given an empty directory + + When I run `wp theme download twentytwelve --target-path=/tmp/wp-cli-download-test-theme` + Then STDOUT should contain: + """ + Success: Downloaded theme package to + """ + And save STDOUT 'Success: Downloaded theme package to (.+)' as {DOWNLOADED_THEME} + And the {DOWNLOADED_THEME} file should exist + And STDERR should be empty + + Scenario: Downloading a specific version of a theme + Given an empty directory + + When I run `wp theme download twentytwelve --version=1.3` + Then STDOUT should contain: + """ + Downloading twentytwelve (1.3) + """ + And STDOUT should contain: + """ + Success: Downloaded theme package to + """ + And STDERR should be empty + + Scenario: Downloading a non-existent version of a theme fails with clear error + Given an empty directory + + When I try `wp theme download twentytwelve --version=9.9.9` + Then STDERR should contain: + """ + Error: Can't find the requested theme's version 9.9.9 + """ + And the return code should be 1 + + Scenario: Downloading a theme with --force overwrites existing file + Given an empty directory + + When I run `wp theme download twentytwelve` + And I run `wp theme download twentytwelve --force` + Then STDOUT should contain: + """ + Success: Downloaded theme package to + """ + And STDERR should be empty + + Scenario: Downloading a theme without --force fails if destination exists + Given an empty directory + + When I run `wp theme download twentytwelve` + And I try `wp theme download twentytwelve` + Then STDERR should contain: + """ + Error: Destination file already exists: + """ + And the return code should be 1 + + Scenario: Downloading an unknown theme fails with a clear error + Given an empty directory + + When I try `wp theme download this-theme-does-not-exist-xyz-abc-123` + Then STDERR should contain: + """ + Error: The 'this-theme-does-not-exist-xyz-abc-123' theme could not be found. + """ + And the return code should be 1 diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5b3175e3..0a51c466 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -53,7 +53,7 @@ - */src/(Plugin_(AutoUpdates_)?|Theme_(Mod_|AutoUpdates_)?)Command\.php$ + */src/(Plugin_(AutoUpdates_|Download_)?|Theme_(Mod_|AutoUpdates_|Download_)?)Command\.php$ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index abaa502d..3b8aeab5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,6 +7,7 @@ parameters: - vendor/wp-cli/wp-cli/php scanFiles: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php treatPhpDocTypesAsCertain: false ignoreErrors: - identifier: missingType.iterableValue diff --git a/src/Plugin_Download_Command.php b/src/Plugin_Download_Command.php new file mode 100644 index 00000000..b3ccb8e5 --- /dev/null +++ b/src/Plugin_Download_Command.php @@ -0,0 +1,159 @@ + + * : Slug of the plugin to download. + * + * [--target-path=] + * : Directory to store the downloaded zip file. Defaults to the current directory. + * + * [--version=] + * : Version to download. Accepts a version number or `dev`. + * + * [--force] + * : Overwrite destination file if it already exists. + * + * [--insecure] + * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. + * + * ## EXAMPLES + * + * $ wp plugin download bbpress + * Downloading bbpress (2.5.9)... + * Success: Downloaded plugin package to /path/to/bbpress.2.5.9.zip + * + * @when before_wp_load + * + * @param array{0: string} $args Positional arguments. + * @param array{target-path?: string, version?: string, force?: bool, insecure?: bool} $assoc_args Associative arguments. + */ + public function __invoke( $args, $assoc_args ) { + $slug = (string) $args[0]; + if ( '' === trim( $slug ) ) { + WP_CLI::error( 'Please provide a plugin slug.' ); + } + + $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); + $force = Utils\get_flag_value( $assoc_args, 'force', false ); + $requested = Utils\get_flag_value( $assoc_args, 'version', null ); + $download_dir = Utils\get_flag_value( $assoc_args, 'target-path', getcwd() ?: '.' ); + + if ( ! is_dir( $download_dir ) ) { + if ( ! @mkdir( $download_dir, 0755, true ) ) { + WP_CLI::error( "Failed to create directory '{$download_dir}'." ); + } + } + + if ( ! is_writable( $download_dir ) ) { + WP_CLI::error( "'{$download_dir}' is not writable by current user." ); + } + + try { + $plugin_data = ( new WpOrgApi( [ 'insecure' => $insecure ] ) )->get_plugin_info( $slug ); + } catch ( Exception $exception ) { + WP_CLI::error( "The '{$slug}' plugin could not be found. " . $exception->getMessage() ); + } + + if ( ! is_array( $plugin_data ) || empty( $plugin_data['download_link'] ) || empty( $plugin_data['version'] ) ) { + WP_CLI::error( "The '{$slug}' plugin could not be found." ); + } + + $download_url = $plugin_data['download_link']; + $version = $plugin_data['version']; + + if ( is_string( $requested ) && '' !== $requested && $requested !== $plugin_data['version'] ) { + $current_zip = basename( (string) Utils\parse_url( $download_url, PHP_URL_PATH ) ); + if ( 'dev' === $requested ) { + $download_url = str_replace( $current_zip, $slug . '.zip', $download_url ); + $version = 'Development Version'; + } else { + $download_url = str_replace( $current_zip, $slug . '.' . $requested . '.zip', $download_url ); + $version = $requested; + + try { + $head_response = Utils\http_request( 'HEAD', $download_url, null, [], [ 'insecure' => (bool) $insecure ] ); + } catch ( Exception $exception ) { + WP_CLI::error( $exception->getMessage() ); + } + + if ( 200 !== (int) $head_response->status_code ) { + WP_CLI::error( + sprintf( + "Can't find the requested plugin's version %s in the WordPress.org plugin repository (HTTP code %d).", + $requested, + $head_response->status_code + ) + ); + } + } + } + + $zip_name = basename( (string) Utils\parse_url( $download_url, PHP_URL_PATH ) ); + if ( '' === $zip_name ) { + $zip_name = "{$slug}.zip"; + } + + $download_file = rtrim( $download_dir, '/\\' ) . DIRECTORY_SEPARATOR . $zip_name; + + if ( ! $force && file_exists( $download_file ) ) { + WP_CLI::error( "Destination file already exists: {$download_file}" ); + } + + $destination_file = $download_file; + $tmp_file = $download_file; + + if ( $force && file_exists( $destination_file ) ) { + $tmp_file = $destination_file . '.tmp.' . uniqid( '', true ); + } + + WP_CLI::log( "Downloading {$slug} ({$version})..." ); + + try { + $response = Utils\http_request( + 'GET', + $download_url, + null, + [], + [ + 'filename' => $tmp_file, + 'insecure' => (bool) $insecure, + ] + ); + } catch ( Exception $exception ) { + if ( file_exists( $tmp_file ) ) { + unlink( $tmp_file ); + } + WP_CLI::error( $exception->getMessage() ); + } + + if ( 200 !== (int) $response->status_code ) { + if ( file_exists( $tmp_file ) ) { + unlink( $tmp_file ); + } + WP_CLI::error( sprintf( 'Failed to download plugin package (HTTP code %d).', $response->status_code ) ); + } + + if ( $tmp_file !== $destination_file ) { + if ( file_exists( $destination_file ) && ! @unlink( $destination_file ) ) { + WP_CLI::error( "Failed to remove existing destination file: {$destination_file}" ); + } + if ( ! @rename( $tmp_file, $destination_file ) ) { + WP_CLI::error( "Failed to move downloaded file into place: {$destination_file}" ); + } + } + + WP_CLI::success( "Downloaded plugin package to {$destination_file}" ); + } +} diff --git a/src/Theme_Download_Command.php b/src/Theme_Download_Command.php new file mode 100644 index 00000000..4d4d5870 --- /dev/null +++ b/src/Theme_Download_Command.php @@ -0,0 +1,159 @@ + + * : Slug of the theme to download. + * + * [--target-path=] + * : Directory to store the downloaded zip file. Defaults to the current directory. + * + * [--version=] + * : Version to download. Accepts a version number or `dev`. + * + * [--force] + * : Overwrite destination file if it already exists. + * + * [--insecure] + * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. + * + * ## EXAMPLES + * + * $ wp theme download twentytwelve + * Downloading twentytwelve (1.3)... + * Success: Downloaded theme package to /path/to/twentytwelve.1.3.zip + * + * @when before_wp_load + * + * @param array{0: string} $args Positional arguments. + * @param array{target-path?: string, version?: string, force?: bool, insecure?: bool} $assoc_args Associative arguments. + */ + public function __invoke( $args, $assoc_args ) { + $slug = (string) $args[0]; + if ( '' === trim( $slug ) ) { + WP_CLI::error( 'Please provide a theme slug.' ); + } + + $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); + $force = Utils\get_flag_value( $assoc_args, 'force', false ); + $requested = Utils\get_flag_value( $assoc_args, 'version', null ); + $download_dir = Utils\get_flag_value( $assoc_args, 'target-path', getcwd() ?: '.' ); + + if ( ! is_dir( $download_dir ) ) { + if ( ! @mkdir( $download_dir, 0755, true ) ) { + WP_CLI::error( "Failed to create directory '{$download_dir}'." ); + } + } + + if ( ! is_writable( $download_dir ) ) { + WP_CLI::error( "'{$download_dir}' is not writable by current user." ); + } + + try { + $theme_data = ( new WpOrgApi( [ 'insecure' => $insecure ] ) )->get_theme_info( $slug ); + } catch ( Exception $exception ) { + WP_CLI::error( "The '{$slug}' theme could not be found. " . $exception->getMessage() ); + } + + if ( ! is_array( $theme_data ) || empty( $theme_data['download_link'] ) || empty( $theme_data['version'] ) ) { + WP_CLI::error( "The '{$slug}' theme could not be found." ); + } + + $download_url = $theme_data['download_link']; + $version = $theme_data['version']; + + if ( is_string( $requested ) && '' !== $requested && $requested !== $theme_data['version'] ) { + $current_zip = basename( (string) Utils\parse_url( $download_url, PHP_URL_PATH ) ); + if ( 'dev' === $requested ) { + $download_url = str_replace( $current_zip, $slug . '.zip', $download_url ); + $version = 'Development Version'; + } else { + $download_url = str_replace( $current_zip, $slug . '.' . $requested . '.zip', $download_url ); + $version = $requested; + + try { + $head_response = Utils\http_request( 'HEAD', $download_url, null, [], [ 'insecure' => (bool) $insecure ] ); + } catch ( Exception $exception ) { + WP_CLI::error( $exception->getMessage() ); + } + + if ( 200 !== (int) $head_response->status_code ) { + WP_CLI::error( + sprintf( + "Can't find the requested theme's version %s in the WordPress.org theme repository (HTTP code %d).", + $requested, + $head_response->status_code + ) + ); + } + } + } + + $zip_name = basename( (string) Utils\parse_url( $download_url, PHP_URL_PATH ) ); + if ( '' === $zip_name ) { + $zip_name = "{$slug}.zip"; + } + + $download_file = rtrim( $download_dir, '/\\' ) . DIRECTORY_SEPARATOR . $zip_name; + + if ( ! $force && file_exists( $download_file ) ) { + WP_CLI::error( "Destination file already exists: {$download_file}" ); + } + + $destination_file = $download_file; + $tmp_file = $download_file; + + if ( $force && file_exists( $destination_file ) ) { + $tmp_file = $destination_file . '.tmp.' . uniqid( '', true ); + } + + WP_CLI::log( "Downloading {$slug} ({$version})..." ); + + try { + $response = Utils\http_request( + 'GET', + $download_url, + null, + [], + [ + 'filename' => $tmp_file, + 'insecure' => (bool) $insecure, + ] + ); + } catch ( Exception $exception ) { + if ( file_exists( $tmp_file ) ) { + unlink( $tmp_file ); + } + WP_CLI::error( $exception->getMessage() ); + } + + if ( 200 !== (int) $response->status_code ) { + if ( file_exists( $tmp_file ) ) { + unlink( $tmp_file ); + } + WP_CLI::error( sprintf( 'Failed to download theme package (HTTP code %d).', $response->status_code ) ); + } + + if ( $tmp_file !== $destination_file ) { + if ( file_exists( $destination_file ) && ! @unlink( $destination_file ) ) { + WP_CLI::error( "Failed to remove existing destination file: {$destination_file}" ); + } + if ( ! @rename( $tmp_file, $destination_file ) ) { + WP_CLI::error( "Failed to move downloaded file into place: {$destination_file}" ); + } + } + + WP_CLI::success( "Downloaded theme package to {$destination_file}" ); + } +} diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 00000000..f6f5525f --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,14 @@ + Date: Wed, 10 Jun 2026 19:06:35 +0200 Subject: [PATCH 4/5] Regenerate README file (#530) --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 15e8e349..55dbbb33 100644 --- a/README.md +++ b/README.md @@ -780,6 +780,39 @@ Lists the available plugin updates. Similar to `wp core check-update`. +### wp plugin download + +Downloads a plugin zip package without loading WordPress. + +~~~ +wp plugin download [--target-path=] [--version=] [--force] [--insecure] +~~~ + +**OPTIONS** + + + Slug of the plugin to download. + + [--target-path=] + Directory to store the downloaded zip file. Defaults to the current directory. + + [--version=] + Version to download. Accepts a version number or `dev`. + + [--force] + Overwrite destination file if it already exists. + + [--insecure] + Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. + +**EXAMPLES** + + $ wp plugin download bbpress + Downloading bbpress (2.5.9)... + Success: Downloaded plugin package to /path/to/bbpress.2.5.9.zip + + + ### wp plugin toggle Toggles a plugin's activation state. @@ -1811,6 +1844,39 @@ Lists the available theme updates. Similar to `wp core check-update`. +### wp theme download + +Downloads a theme zip package without loading WordPress. + +~~~ +wp theme download [--target-path=] [--version=] [--force] [--insecure] +~~~ + +**OPTIONS** + + + Slug of the theme to download. + + [--target-path=] + Directory to store the downloaded zip file. Defaults to the current directory. + + [--version=] + Version to download. Accepts a version number or `dev`. + + [--force] + Overwrite destination file if it already exists. + + [--insecure] + Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. + +**EXAMPLES** + + $ wp theme download twentytwelve + Downloading twentytwelve (1.3)... + Success: Downloaded theme package to /path/to/twentytwelve.1.3.zip + + + ### wp theme update Updates one or more themes. From edbfb7b0801b1767031948062c0ca33f11ba9e0f Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz <7408560+ekamran@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:37:52 +0530 Subject: [PATCH 5/5] Keep failed update output parseable for JSON and CSV (#529) Co-authored-by: Pascal Birchler --- features/plugin-update.feature | 31 +++++++++++++++++++++++++++++++ src/WP_CLI/CommandWithUpgrade.php | 3 +++ 2 files changed, 34 insertions(+) diff --git a/features/plugin-update.feature b/features/plugin-update.feature index 771454d6..86c5f68c 100644 --- a/features/plugin-update.feature +++ b/features/plugin-update.feature @@ -380,6 +380,37 @@ Feature: Update WordPress plugins Success: Updated 2 of 2 plugins. """ + @require-wp-5.2 @skip-windows + Scenario: Failed plugin update keeps JSON output parseable + Given a WP install + And I run `wp plugin install wordpress-importer --version=0.5 --force` + And I run `chmod -w wp-content/plugins/wordpress-importer` + + When I try `wp plugin update wordpress-importer --format=json` + Then STDOUT should be JSON containing: + """ + [{"name":"wordpress-importer","old_version":"0.5","status":"Error"}] + """ + And STDERR should be empty + And the return code should be 1 + + When I run `chmod +w wp-content/plugins/wordpress-importer` + + @require-wp-5.2 @skip-windows + Scenario: Failed plugin update keeps CSV output parseable + Given a WP install + And I run `wp plugin install wordpress-importer --version=0.5 --force` + And I run `chmod -w wp-content/plugins/wordpress-importer` + + When I try `wp plugin update wordpress-importer --format=csv` + Then STDOUT should be CSV containing: + | name | old_version | status | + | wordpress-importer | 0.5 | Error | + And STDERR should be empty + And the return code should be 1 + + When I run `chmod +w wp-content/plugins/wordpress-importer` + # Skipped on Windows because of mkdir usage that would need to be refactored for compatibility. @require-wp-5.2 @skip-windows Scenario: Skip plugin update when plugin directory is a VCS checkout diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index eafd6ebd..4ad5032b 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -880,6 +880,9 @@ static function ( $result ) { $errors = $skipped; $skipped = null; } + if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], [ 'json', 'csv' ], true ) && $errors ) { + WP_CLI::halt( 1 ); + } Utils\report_batch_operation_results( $this->item_type, 'update', $total_updated, $num_updated, $errors, $skipped ); if ( null !== $exclude ) { WP_CLI::log( "Skipped updates for: $exclude" );