Skip to content

Commit 617e9c5

Browse files
tfirdausBogdanUngureanudanielbachhuber
authored
Add support for GitHub release installation (#421)
--------- Co-authored-by: Bogdan Ungureanu <bogdanungureanu21@gmail.com> Co-authored-by: Daniel Bachhuber <daniel.bachhuber@automattic.com>
1 parent fa8bfd9 commit 617e9c5

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Feature: Install WordPress plugins
2+
3+
Scenario: Verify that providing a plugin releases/latest GitHub URL will get the latest ZIP
4+
Given a WP install
5+
When I run `wp plugin install https://github.com/danielbachhuber/one-time-login/releases/latest`
6+
Then STDOUT should contain:
7+
"""
8+
Latest release resolved to Version
9+
"""
10+
And STDOUT should contain:
11+
"""
12+
package from https://api.github.com/repos/danielbachhuber/one-time-login/zipball/
13+
"""
14+
And STDOUT should contain:
15+
"""
16+
Plugin installed successfully.
17+
Success: Installed 1 of 1 plugins.
18+
"""

src/WP_CLI/CommandWithUpgrade.php

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {
2222

2323
protected $chained_command = false;
2424

25+
/**
26+
* The GitHub Releases public api endpoint.
27+
*
28+
* @var string
29+
*/
30+
private $github_releases_api_endpoint = 'https://api.github.com/repos/%s/releases';
31+
32+
/**
33+
* The GitHub latest release url format.
34+
*
35+
* @var string
36+
*/
37+
private $github_latest_release_url = '/^https:\/\/github\.com\/(.*)\/releases\/latest\/?$/';
38+
2539
// Invalid version message.
2640
const INVALID_VERSION_MESSAGE = 'version higher than expected';
2741

@@ -145,7 +159,6 @@ private function show_legend( $items ) {
145159
}
146160

147161
public function install( $args, $assoc_args ) {
148-
149162
$successes = 0;
150163
$errors = 0;
151164
foreach ( $args as $slug ) {
@@ -159,6 +172,25 @@ public function install( $args, $assoc_args ) {
159172

160173
$is_remote = false !== strpos( $slug, '://' );
161174

175+
if ( $is_remote ) {
176+
$github_repo = $this->get_github_repo_from_releases_url( $slug );
177+
178+
if ( $github_repo ) {
179+
$version = $this->get_the_latest_github_version( $github_repo );
180+
181+
if ( is_wp_error( $version ) ) {
182+
WP_CLI::error( $version->get_error_message() );
183+
}
184+
185+
/**
186+
* Sets the $slug that will trigger the installation based on a zip file.
187+
*/
188+
$slug = $version['url'];
189+
190+
WP_CLI::log( 'Latest release resolved to ' . $version['name'] );
191+
}
192+
}
193+
162194
// Check if a URL to a remote or local zip has been specified.
163195
if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) {
164196
// Install from local or remote zip file.
@@ -785,4 +817,89 @@ private function parse_url_host_component( $url, $component ) {
785817
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
786818
return function_exists( 'wp_parse_url' ) ? wp_parse_url( $url, $component ) : parse_url( $url, $component );
787819
}
820+
821+
/**
822+
* Get the latest package version based on a given repo slug.
823+
*
824+
* @param string $repo_slug
825+
*
826+
* @return array{ name: string, url: string }|\WP_Error
827+
*/
828+
public function get_the_latest_github_version( $repo_slug ) {
829+
$api_url = sprintf( $this->github_releases_api_endpoint, $repo_slug );
830+
$token = getenv( 'GITHUB_TOKEN' );
831+
832+
$request_arguments = $token ? [ 'headers' => 'Authorization: Bearer ' . getenv( 'GITHUB_TOKEN' ) ] : [];
833+
834+
$response = \wp_remote_get( $api_url, $request_arguments );
835+
836+
if ( \is_wp_error( $response ) ) {
837+
return $response;
838+
}
839+
840+
$body = \wp_remote_retrieve_body( $response );
841+
$decoded_body = json_decode( $body );
842+
843+
// WP_Http::FORBIDDEN doesn't exist in WordPress 3.7
844+
if ( 403 === wp_remote_retrieve_response_code( $response ) ) {
845+
return new \WP_Error(
846+
403,
847+
$this->build_rate_limiting_error_message( $decoded_body )
848+
);
849+
}
850+
851+
if ( null === $decoded_body ) {
852+
return new \WP_Error( 500, 'Empty response received from GitHub.com API' );
853+
}
854+
855+
if ( ! isset( $decoded_body[0] ) ) {
856+
return new \WP_Error( '400', 'The given Github repository does not have any releases' );
857+
}
858+
859+
$latest_release = $decoded_body[0];
860+
861+
return [
862+
'name' => $latest_release->name,
863+
'url' => $this->get_asset_url_from_release( $latest_release ),
864+
];
865+
}
866+
867+
/**
868+
* Get the asset URL from the release array. When the asset is not present, we fallback to the zipball_url (source code) property.
869+
*/
870+
private function get_asset_url_from_release( $release ) {
871+
if ( isset( $release->assets[0]->browser_download_url ) ) {
872+
return $release->assets[0]->browser_download_url;
873+
}
874+
875+
if ( isset( $release->zipball_url ) ) {
876+
return $release->zipball_url;
877+
}
878+
879+
return null;
880+
}
881+
882+
/**
883+
* Get the GitHub repo from the URL.
884+
*
885+
* @param string $url
886+
*
887+
* @return string|null
888+
*/
889+
public function get_github_repo_from_releases_url( $url ) {
890+
preg_match( $this->github_latest_release_url, $url, $matches );
891+
892+
return isset( $matches[1] ) ? $matches[1] : null;
893+
}
894+
895+
/**
896+
* Build the error message we display in WP-CLI for the API Rate limiting error response.
897+
*
898+
* @param $decoded_body
899+
*
900+
* @return string
901+
*/
902+
private function build_rate_limiting_error_message( $decoded_body ) {
903+
return $decoded_body->message . PHP_EOL . $decoded_body->documentation_url . PHP_EOL . 'In order to pass the token to WP-CLI, you need to use the GITHUB_TOKEN environment variable.';
904+
}
788905
}

0 commit comments

Comments
 (0)