Skip to content

Commit

Permalink
Add support for GitHub release installation (#421)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Bogdan Ungureanu <[email protected]>
Co-authored-by: Daniel Bachhuber <[email protected]>
  • Loading branch information
3 people authored Apr 27, 2024
1 parent fa8bfd9 commit 617e9c5
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 1 deletion.
18 changes: 18 additions & 0 deletions features/plugin-install-github-latest.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Feature: Install WordPress plugins

Scenario: Verify that providing a plugin releases/latest GitHub URL will get the latest ZIP
Given a WP install
When I run `wp plugin install https://github.com/danielbachhuber/one-time-login/releases/latest`
Then STDOUT should contain:
"""
Latest release resolved to Version
"""
And STDOUT should contain:
"""
package from https://api.github.com/repos/danielbachhuber/one-time-login/zipball/
"""
And STDOUT should contain:
"""
Plugin installed successfully.
Success: Installed 1 of 1 plugins.
"""
119 changes: 118 additions & 1 deletion src/WP_CLI/CommandWithUpgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {

protected $chained_command = false;

/**
* The GitHub Releases public api endpoint.
*
* @var string
*/
private $github_releases_api_endpoint = 'https://api.github.com/repos/%s/releases';

/**
* The GitHub latest release url format.
*
* @var string
*/
private $github_latest_release_url = '/^https:\/\/github\.com\/(.*)\/releases\/latest\/?$/';

// Invalid version message.
const INVALID_VERSION_MESSAGE = 'version higher than expected';

Expand Down Expand Up @@ -145,7 +159,6 @@ private function show_legend( $items ) {
}

public function install( $args, $assoc_args ) {

$successes = 0;
$errors = 0;
foreach ( $args as $slug ) {
Expand All @@ -159,6 +172,25 @@ public function install( $args, $assoc_args ) {

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

if ( $is_remote ) {
$github_repo = $this->get_github_repo_from_releases_url( $slug );

if ( $github_repo ) {
$version = $this->get_the_latest_github_version( $github_repo );

if ( is_wp_error( $version ) ) {
WP_CLI::error( $version->get_error_message() );
}

/**
* Sets the $slug that will trigger the installation based on a zip file.
*/
$slug = $version['url'];

WP_CLI::log( 'Latest release resolved to ' . $version['name'] );
}
}

// Check if a URL to a remote or local zip has been specified.
if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) {
// Install from local or remote zip file.
Expand Down Expand Up @@ -785,4 +817,89 @@ private function parse_url_host_component( $url, $component ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
return function_exists( 'wp_parse_url' ) ? wp_parse_url( $url, $component ) : parse_url( $url, $component );
}

/**
* Get the latest package version based on a given repo slug.
*
* @param string $repo_slug
*
* @return array{ name: string, url: string }|\WP_Error
*/
public function get_the_latest_github_version( $repo_slug ) {
$api_url = sprintf( $this->github_releases_api_endpoint, $repo_slug );
$token = getenv( 'GITHUB_TOKEN' );

$request_arguments = $token ? [ 'headers' => 'Authorization: Bearer ' . getenv( 'GITHUB_TOKEN' ) ] : [];

$response = \wp_remote_get( $api_url, $request_arguments );

if ( \is_wp_error( $response ) ) {
return $response;
}

$body = \wp_remote_retrieve_body( $response );
$decoded_body = json_decode( $body );

// WP_Http::FORBIDDEN doesn't exist in WordPress 3.7
if ( 403 === wp_remote_retrieve_response_code( $response ) ) {
return new \WP_Error(
403,
$this->build_rate_limiting_error_message( $decoded_body )
);
}

if ( null === $decoded_body ) {
return new \WP_Error( 500, 'Empty response received from GitHub.com API' );
}

if ( ! isset( $decoded_body[0] ) ) {
return new \WP_Error( '400', 'The given Github repository does not have any releases' );
}

$latest_release = $decoded_body[0];

return [
'name' => $latest_release->name,
'url' => $this->get_asset_url_from_release( $latest_release ),
];
}

/**
* Get the asset URL from the release array. When the asset is not present, we fallback to the zipball_url (source code) property.
*/
private function get_asset_url_from_release( $release ) {
if ( isset( $release->assets[0]->browser_download_url ) ) {
return $release->assets[0]->browser_download_url;
}

if ( isset( $release->zipball_url ) ) {
return $release->zipball_url;
}

return null;
}

/**
* Get the GitHub repo from the URL.
*
* @param string $url
*
* @return string|null
*/
public function get_github_repo_from_releases_url( $url ) {
preg_match( $this->github_latest_release_url, $url, $matches );

return isset( $matches[1] ) ? $matches[1] : null;
}

/**
* Build the error message we display in WP-CLI for the API Rate limiting error response.
*
* @param $decoded_body
*
* @return string
*/
private function build_rate_limiting_error_message( $decoded_body ) {
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.';
}
}

0 comments on commit 617e9c5

Please sign in to comment.