diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php index 67d76a918a..ab8bb330cc 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php @@ -1001,6 +1001,22 @@ public static function get_release_confirmation_link( $tag, $post = null, $what ); } + /** + * Generates a link to enable Release Confirmations. + * + * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. + * @return string URL to enable confirmations. + */ + public static function get_phased_rollout_link( $post = null ) { + $post = get_post( $post ); + + return add_query_arg( + array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ), + home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/phased-rollout' ) + ); + } + + /** * Returns the reasons for closing or disabling a plugin. * @@ -1385,6 +1401,23 @@ static function get_rollout_strategies() { 'name' => __( 'Manual updates only (24 hours)', 'wporg-plugins' ), 'description' => __( 'Plugin updates will be released to all sites, but automatic updates will be disabled for 24 hours. After that, sites will receive the update as normal.', 'wporg-plugins' ), ], + /* + [ + 'name' => __( 'Slow rollout', 'wporg-plugins' ), + 'slug' => 'slow', + 'description' => __( 'Plugin updates will be released to 5% of sites for the first 6 hours, increasing to 100% over the next 2 days.', 'wporg-plugins' ), + ], + [ + 'name' => __( 'Extra slow rollout', 'wporg-plugins' ), + 'slug' => 'extra-slow', + 'description' => __( 'Plugin updates will be released to 5% of sites for the first 6 hours, increasing to 100% over the next 3 days.', 'wporg-plugins' ), + ], + [ + 'name' => __( 'Cautious rollout', 'wporg-plugins' ), + 'slug' => 'cautious', + 'description' => __( 'Plugin updates will be released to 1% of sites for the first 6 hours, increasing to 10% by day 2, and to 100% of sites within 5 days.', 'wporg-plugins' ), + ] + */ ]; } } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php index 19c59de20b..eeeac9f67f 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php @@ -110,6 +110,10 @@ public static function update_single_plugin( $plugin_slug, $self_loop = false ) $meta['rollout'] = array( 'strategy' => $release['rollout_strategy'], ); + } elseif ( $post->rollout_strategy ) { + $meta['rollout'] = array( + 'strategy' => $post->rollout_strategy, + ); } $data = array( diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/standalone/plugin-update-helpers.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/standalone/plugin-update-helpers.php index bfc1e10532..656a9023ab 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/standalone/plugin-update-helpers.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/standalone/plugin-update-helpers.php @@ -62,7 +62,156 @@ function phased_rollout( $plugin_info, $plugin_details, $installed_version ) { $hours_since_release <= 24 ) { $plugin_info->disable_autoupdate = true; + + // Early return to avoid further processing. + return $plugin_info; + } + + $do_not_offer_update = false; + + // Handle the percent-based strategies. + $plugin_percent_rollout = phased_rollout_get_plugin_percent( $strategy, $hours_since_release, $plugin_details ); + if ( $plugin_percent_rollout !== false ) { + + $site_percent = get_site_percentage( $plugin_details->plugin_slug, $plugin_details->version ); + + if ( $site_percent > $plugin_percent_rollout ) { + $do_not_offer_update = true; + } + } + + // If the site should not update, we'll return the last-version if possible. + if ( $do_not_offer_update ) { + $plugin_info->version = ( $plugin_details->meta->last_version ?? '' ) ?: $installed_version; + $plugin_info->stable_tag = ( $plugin_details->meta->last_stable_tag ?? '' ) ?: $installed_version; + + // Match update-check API. + unset( + $plugin_info->tested, + $plugin_info->requires_php, + $plugin_info->requires_plugins, + $plugin_info->compatibility, + $plugin_info->upgrade_notice + ); } return $plugin_info; } + +/** + * Return the current sites update-percentage. + * + * @global string $wp_url The WordPress site URL. Extracted from the HTTP User Agent header. + * + * @param string $slug The plugin slug. + * @param string $version The plugin version. + * + * @return float 0...100.00 + */ +function get_site_percentage( string $slug = '', string $version = '' ) { + global $wp_url; + + /* + * If the site URL hasn't been extracted already, pull it from the global. + * NOTE: This may be set by the tests or other codepaths that run before this function. + */ + if ( empty( $wp_url ) && preg_match( '#^WordPress/.+; (http.+)$#i', $_SERVER['HTTP_USER_AGENT'] ?? '', $m ) ) { + $wp_url = $m[1]; + } + + $site_domain = strtolower( parse_url( $wp_url, PHP_URL_HOST ) ?: '' ); + + // If we've reached this point and have no URL, delay the update until 100% is reached. + if ( ! $site_domain ) { + return 100; + } + + // $site_step represents an integer from 0 to 4095. + $site_step = base_convert( substr( md5( "{$site_domain}|{$slug}|{$version}" ), 0, 3 ), 16, 10 ); + $site_percent = $site_step / 4095 * 100; + + return $site_percent; +} + +/** + * Get the percentage of sites that should receive the update for the plugin. + * + * @link https://www.desmos.com/calculator/59sl7efajq + * + * @param string $strategy The rollout strategy. + * @param float $hours_since_release The number of hours since the plugin was released. + * @param object $update_details The plugin update details. + * + * @return float|false The percentage of sites that should receive the update, or false invalid details. + */ +function phased_rollout_get_plugin_percent( string $strategy, float $hours_since_release, object $update_details ) { + $percent_based_strategies = [ + 'custom', + 'slow', + 'extra-slow', + 'cautious', + ]; + + $phase_details = $update_details->meta->rollout ?? false; + + if ( + ! $phase_details || + ! in_array( $strategy, $percent_based_strategies, true ) + ) { + return false; + } + + switch( $strategy ) { + default: + return false; + + // Custom defined by the plugin author, they must update this value in settings. + case 'custom': + return $phase_details['percentage'] ?? 100; + + /* + * Straight curve, start at 5%, increases to 100% over the next 48hrs (2d). + * + * At 6 hours, the percentage is 5 + (6/48) * 95 = 16.875% + * At 12 hours, the percentage is 5 + (12/48) * 95 = 28.75% + * At 24 hours, the percentage is 5 + (24/48) * 95 = 52.5% + * At 36 hours, the percentage is 5 + (36/48) * 95 = 72.25% + * At 48 hours, the percentage is 5 + (48/48) * 95 = 100% + */ + case 'slow': + return 5 + ( $hours_since_release / 48 ) * 95; + + /* + * Polynomial curve, starts at 5%, increases to 100% over the next 72hrs (3d). + * + * At 6 hours, the percentage is 9 * ( 1.0345 ** 6 ) - 3.9 = 7.13% + * At 12 hours, the percentage is 9 * ( 1.0345 ** 12 ) - 3.9 = 9.62% + * At 24 hours, the percentage is 9 * ( 1.0345 ** 24 ) - 3.9 = 16.41% + * At 36 hours, the percentage is 9 * ( 1.0345 ** 36 ) - 3.9 = 26.61% + * At 48 hours, the percentage is 9 * ( 1.0345 ** 48 ) - 3.9 = 41.95% + * At 60 hours, the percentage is 9 * ( 1.0345 ** 60 ) - 3.9 = 64.97% + * At 72 hours, the percentage is 9 * ( 1.0345 ** 72 ) - 3.9 = 100% + */ + case 'extra-slow': + return 9 * ( 1.0345 ** $hours_since_release ) - 3.9; + + /* + * Polynomial curve, starts at 1%, with an increase to 100% over the next 120hrs (5d). + * + * At 6 hours, the percentage is 11 * ( 1.0195 ** 6 ) - 10 = 2.35% + * At 12 hours, the percentage is 11 * ( 1.0195 ** 12 ) - 10 = 3.87% + * At 24 hours, the percentage is 11 * ( 1.0195 ** 24 ) - 10 = 7.49% + * At 36 hours, the percentage is 11 * ( 1.0195 ** 36 ) - 10 = 12% + * At 48 hours, the percentage is 11 * ( 1.0195 ** 48 ) - 10 = 17.8% + * At 72 hours, the percentage is 11 * ( 1.0195 ** 72 ) - 10 = 34.18% + * At 96 hours, the percentage is 11 * ( 1.0195 ** 96 ) - 10 = 60.24% + * At 120 hours, the percentage is 11 * ( 1.0195 ** 120 ) - 10 = 101.65 ~= 100% + * + */ + case 'cautious': + return 11 * ( 1.0195 ** $hours_since_release ) - 10; + } + + // If we reach this point, something is wrong. + return false; +} diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/inc/template-tags.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/inc/template-tags.php index f539c6eec9..139c33d1ac 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/inc/template-tags.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/inc/template-tags.php @@ -478,6 +478,9 @@ function the_plugin_danger_zone() { // Output the Release Confirmation form. the_plugin_release_confirmation_form(); + // Output the rollout settings. + the_rollout_settings(); + if ( 'publish' != $post->post_status ) { // A reminder of the closed status. the_active_plugin_notice(); @@ -772,3 +775,45 @@ function the_author_notice( $post = null ) { ); } } + +/** + * Displays the "phased rollout" settings for a plugin. + */ +function the_rollout_settings() { + $post = get_post(); + if ( ! current_user_can( 'plugin_manage_releases', $post ) ) { + return; + } + $rollout = $post->phased_rollout ?: ''; + + echo '

' . __( 'Rollout Strategy', 'wporg-plugins' ) . '

'; + + echo '

' . + __( 'Phased rollout of a plugin initially delivers updates to a small selection of sites, increasing over time.', 'wporg-plugins' ) . + ' ' . + __( 'This allows for the plugin author to limit the impact of a change in a plugin which may negatively impact user experience, to receive that feedback, and resolve the issue before the plugin update is delivered to all websites.', 'wporg-plugins' ) . + '

'; + + echo '
'; + echo ''; + echo '
' . esc_html( Template::get_rollout_strategies()[ $rollout ]['description'] ?? '' ) . '
'; + + echo '

'; + echo ''; + echo '

'; + echo '
'; +}