Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions trac.wordpress.org/conf/workflow-themes.ini
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,18 @@ approve_and_live.set_resolution = live
approve_and_live.permissions = TRUSTED_REVIEWER
approve_and_live.default = -40

# this is for automated ticket closing by themetracbot
new_no_review = new -> closed
# Automated approval of a theme update by themetracbot. Lands the ticket in the
# `approved` status (rather than closing it live) so the release-cooldown cron can
# promote it to live once the cooldown has elapsed.
new_no_review_delay = new -> approved
new_no_review_delay.name = approve and delay
new_no_review_delay.operations = set_owner_to_self
new_no_review_delay.permissions = TICKET_CREATE
new_no_review_delay.default = -41

# Automated marking-live by themetracbot, either straight from review or once the
# release cooldown on an `approved` ticket has elapsed (driven by the release-to-live cron).
new_no_review = new,approved -> closed
new_no_review.name = approve and mark
new_no_review.operations = set_resolution, set_owner_to_self
new_no_review.set_resolution = live
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ function wporg_themes_meta_box_callback( $post ) {
<p><?php echo $text; ?> -
<select name="wporg_themes_status[<?php echo base64_encode( $version ); // base64 because version numbers don't work so well as parts of keys ?>]">
<option value="new" <?php selected( $status, 'new' ); ?>><?php esc_html_e( 'New', 'wporg-themes' ); ?></option>
<?php if ( 'approved' === $status ) : ?>
<?php // `approved` is a transient Trac-driven pre-release state; only shown so the current value displays correctly. ?>
<option value="approved" selected><?php esc_html_e( 'Approved (pending release)', 'wporg-themes' ); ?></option>
<?php endif; ?>
<option value="live" <?php selected( $status, 'live' ); ?>><?php esc_html_e( 'Live', 'wporg-themes' ); ?></option>
<option value="old" <?php selected( $status, 'old' ); ?>><?php esc_html_e( 'Old', 'wporg-themes' ); ?></option>
</select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1158,8 +1158,10 @@ public function prepare_trac_ticket() {
$this->trac_ticket->priority = 'new theme';
if ( ! empty( $this->theme_post->_status ) ) {

// Is this an update to an existing, approved theme?
if ( 'live' === $this->theme_post->_status[ $this->theme_post->max_version ] ) {
// Is this an update to an existing, approved theme? An 'approved' status
// (live version still in release cooldown) counts as approved for ticket
// priority — the previous live version is still being served.
if ( in_array( $this->theme_post->_status[ $this->theme_post->max_version ], [ 'live', 'approved' ], true ) ) {
$this->trac_ticket->priority = 'theme update';

// Apparently not, it must be a new upload for previously unapproved theme.
Expand Down Expand Up @@ -1296,18 +1298,29 @@ public function create_or_update_trac_ticket() {
*/
$trac_ticket_reporter = wp_get_current_user()->user_login ?? $this->author->user_login;

// If there's a previous version and the most current version's status is `new`, we update.
if (
! empty( $this->theme_post->max_version ) &&
'new' == $this->theme_post->_status[ $this->theme_post->max_version ]
) {
$prev_status = $this->theme_post->_status[ $this->theme_post->max_version ?? '' ] ?? '';

/*
* If the previous version is still on an open ticket — either awaiting review
* (`new`) or approved and waiting out the release delay (`approved`) — update that
* same ticket rather than opening a new one. Re-uploading resets the ticket's
* changetime, so an approved update's release delay restarts from this upload and
* the superseded version is simply demoted to `old`.
*/
if ( in_array( $prev_status, [ 'new', 'approved' ], true ) ) {
$ticket_id = (int) $this->theme_post->_ticket_id[ $this->theme_post->max_version ];
$ticket = $this->trac->ticket_get( $ticket_id );

// Make sure the ticket has not yet been resolved.
if ( $ticket && empty( $ticket[3]['resolution'] ) ) {
$result = $this->trac->ticket_update( $ticket_id, $this->trac_ticket->description, array( 'summary' => $this->trac_ticket->summary, 'keywords' => implode( ' ', $this->trac_ticket->keywords ) ), true /* Trigger email notifications */ );
$ticket_id = $result ? $ticket_id : false;

// Keep an approved update in the `approved` window; the release-to-live
// cron promotes it once the delay (measured from this upload) elapses.
if ( 'approved' === $prev_status ) {
$this->version_status = 'approved';
}
} else {
$ticket_id = $this->trac->ticket_create( $this->trac_ticket->summary, $this->trac_ticket->description, array(
'type' => 'theme',
Expand All @@ -1330,13 +1343,25 @@ public function create_or_update_trac_ticket() {
'owner' => '',
) );

// Themes team auto-approves theme-updates, so mark the theme as live immediately.
// Note that this only applies to new ticket creation, so it won't happen on themes with existing outstanding tickets.
// Themes team auto-approves theme-updates. Note that this only applies to new
// ticket creation, so it won't happen on themes with existing outstanding tickets.
if ( $this->trac_ticket->priority == 'theme update' ) {
$this->trac->ticket_update( $ticket_id, 'Theme Update for existing Live theme - automatically approved', array( 'action' => 'new_no_review' ), false );
$release_delay = wporg_themes_get_release_cooldown_delay( $this->theme_slug );
if ( $release_delay ) {
// Land the update in the `approved` status; the release-to-live cron
// promotes it to live once the cooldown elapses. The previous live
// version continues to be served in the meantime.
$delay_hours = (int) round( $release_delay / HOUR_IN_SECONDS );
$this->trac->ticket_update( $ticket_id, sprintf( 'Theme Update for existing Live theme - automatically approved, will be marked live in %dhrs.', $delay_hours ), array( 'action' => 'new_no_review_delay' ), false );
Comment on lines +1354 to +1355

$this->version_status = 'approved';
} else {
// Cooldown disabled: mark the theme live immediately.
$this->trac->ticket_update( $ticket_id, 'Theme Update for existing Live theme - automatically approved', array( 'action' => 'new_no_review' ), false );

$this->trac_ticket->resolution = 'live';
$this->version_status = 'live';
$this->trac_ticket->resolution = 'live';
$this->version_status = 'live';
}
}

}
Expand Down Expand Up @@ -1561,10 +1586,13 @@ public function send_email_notification() {
* Skip sending an email when..
* - The theme is to be made live immediately.
* `wporg_themes_approve_version()` will send a "Congratulations! It's live!" shortly.
* - The theme was auto-approved into the release cooldown. It's not awaiting
* review, so the "new version uploaded" feedback email doesn't apply; the
* "now live" email follows once the cooldown elapses.
* - No Trac ticket was created, so there's nothing to reference about where feedback is.
*/
if (
'live' === $this->version_status ||
in_array( $this->version_status, [ 'live', 'approved' ], true ) ||
! $this->trac_ticket->id
) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ class Trac_Sync {
* @var array
*/
protected static $stati = [
'new' => [
'new' => [
'status' => 'reopened',
],
'live' => [
'approved' => [
// `approved` is an open Trac status (not a resolution): a reviewer has
// approved the ticket, or themetracbot auto-approved an update, and it's
// waiting out the release cooldown before being marked live.
'status' => 'approved',
],
'live' => [
'status' => 'closed',
'resolution' => 'live',
],
'old' => [
'old' => [
'status' => 'closed',
'resolution' => 'not-approved',
],
Expand All @@ -51,6 +57,10 @@ public static function cron_trigger() {
$last_request = get_option( 'wporg-themes-last-trac-sync', strtotime( '-2 days' ) );
update_option( 'wporg-themes-last-trac-sync', time() );

// Migrate approved theme updates whose release delay has elapsed to live on Trac,
// so the sync below imports them as it would any other newly-live ticket.
self::release_to_live( $trac );

foreach ( self::$stati as $new_status => $args ) {
// Get array of tickets.
$tickets = (array) $trac->ticket_query( add_query_arg( wp_parse_args( $args, [
Expand All @@ -76,32 +86,117 @@ public static function cron_trigger() {
continue;
}

$current_status = wporg_themes_get_version_status( $theme_id, $version );

/*
* Bail if the the theme has the wrong status.
*
* For approved and rejected themes, we bail if the current status is not
* 'new' That can happen when there are additional ticket updates (like
* comments) after the ticket was closed.
*
* For reopened tickets we bail if the version is already marked as 'new'.
* This should only be the case if the ticket was closed and reopened before
* this script was able to sync the closed status.
* Skip if the version is already in the target status. This is the common
* case for additional ticket activity (e.g. comments) after a ticket has
* reached a resolved state.
*/
$current_status = wporg_themes_get_version_status( $theme_id, $version );
if ( ( 'new' !== $new_status && 'new' !== $current_status ) || ( 'new' === $new_status && 'new' === $current_status ) ) {
if ( $current_status === $new_status ) {
continue;
}

// We don't need to set an already approved live version to live again.
if ( 'live' === $current_status && 'live' === $new_status ) {
continue;
/*
* Only act on transitions that make sense in the directory's lifecycle
* (new -> approved -> live, with branches to old or back to new). This
* guards against ticket activity that arrives out of order or after the
* version has already moved on.
*/
switch ( $new_status ) {
case 'new':
// Reopened: always sync back to 'new' from any resolved state.
break;

case 'approved':
// Newly approved (reviewer or auto-approved update): only valid
// coming from 'new'.
if ( 'new' !== $current_status ) {
continue 2;
}
break;

case 'live':
// Going live: straight from review (approve_and_live / new_no_review,
// current 'new') or out of the release cooldown (current 'approved',
// via the release-to-live cron or a reviewer force-release on Trac).
if ( ! in_array( $current_status, [ 'new', 'approved' ], true ) ) {
continue 2;
}
break;

case 'old':
// Rejected during review: only valid coming from 'new'.
if ( 'new' !== $current_status ) {
continue 2;
}
break;
}

wporg_themes_update_version_status( $theme_id, $version, $new_status );
}
}
}

/**
* Migrates auto-approved theme updates out of the release delay, on Trac.
*
* Finds `theme update` tickets that have been in the `approved` status for at least
* the theme's release cooldown delay (wporg_themes_get_release_cooldown_delay()) and
* closes them as resolution=live. That's the only change made here — cron_trigger()'s
* normal sync, which runs straight after, imports the now-live ticket into WordPress
* like any other.
*
* Scoped to the `theme update` priority on purpose: first-time theme submissions also
* pass through the `approved` status, but a trusted reviewer marks those live by hand,
* so they should not be promoted on a timer.
*
* @param \Trac $trac An authenticated Trac client.
*/
public static function release_to_live( $trac ) {
/*
* Auto-approved theme updates currently in the `approved` status. We check each
* ticket's changetime in PHP rather than filtering server-side, so a quirk in
* Trac's date-range query syntax can't silently strand themes in the delay.
*/
$tickets = (array) $trac->ticket_query( add_query_arg( [
'status' => 'approved',
'priority' => 'theme update',
'order' => 'changetime',
] ) );

foreach ( $tickets as $ticket_id ) {
$ticket = $trac->ticket_get( $ticket_id );

// Skip if the ticket was force-released or reopened since the query.
if ( ! $ticket || 'approved' !== ( $ticket['status'] ?? '' ) ) {
continue;
}

// Resolve the theme slug so the release delay can be filtered per-theme.
$theme_slug = get_post_field( 'post_name', self::get_theme_id( $ticket_id ) );
$cutoff = time() - wporg_themes_get_release_cooldown_delay( $theme_slug );
Comment on lines +176 to +178

// Only once the release delay, measured from the ticket's changetime, has elapsed.
$changed = $ticket[2] instanceof \IXR_Date ? $ticket[2]->getTimestamp() : strtotime( (string) $ticket[2] );
if ( ! $changed || $changed > $cutoff ) {
continue;
}

// Close as live. Pass the concurrency token we just read to avoid a second
// ticket.get; cron_trigger()'s sync then imports it as a newly-live version.
$trac->ticket_update(
$ticket_id,
'Marking live.',
[
'action' => 'new_no_review',
'_ts' => $ticket['_ts'],
],
false
);
}
}

/**
* Returns the ID of a theme associated with the passed ticket number.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,52 @@

define( 'WPORG_THEMES_E2E_REPO', 'WordPress/theme-review-e2e' );

/**
* Delay between a theme version being approved (by a reviewer on Trac, or via the
* auto-approval path for theme updates) and it becoming the live version served to
* sites by the themes API. Approved versions are held in Trac's `approved` status and
* migrated to live by the theme_directory_trac_sync cron once this delay elapses (see
* Trac_Sync::release_to_live()); the previous live version (if any) continues to be
* served in the meantime. Mitigates supply-chain risks by giving scanners and humans a
* window to flag bad releases. Reviewers can bypass the delay with Trac's `approve and
* mark` / `mark this theme` actions, which close the ticket as live immediately.
*
* Defers to the shared WPORG_PLUGIN_THEME_RELEASE_DELAY constant when it's defined
* so the plugin and theme directories can be tuned (or disabled) in lockstep from a
* single override point.
*
* Defaults to 0 (cooldown disabled, versions go live immediately) for now; this will be
* raised once the surrounding workflow is ready. Can be pre-defined in global config to
* override the default.
*/
if ( ! defined( 'WPORG_THEMES_RELEASE_COOL_DOWN_DELAY' ) ) {
define( 'WPORG_THEMES_RELEASE_COOL_DOWN_DELAY', defined( 'WPORG_PLUGIN_THEME_RELEASE_DELAY' ) ? WPORG_PLUGIN_THEME_RELEASE_DELAY : 0 );
}

/**
* Returns the release cooldown delay, in seconds, for a theme.
*
* The WPORG_THEMES_RELEASE_COOL_DOWN_DELAY constant provides the default, which is then
* passed through the `wporg_themes_release_cooldown_delay` filter so the delay can be
* shortened, extended, or removed (return 0 to disable the cooldown) on a per-theme basis.
* The theme slug is passed to the filter when it is known.
*
* @param string $theme_slug The slug of the theme being acted upon, if known.
* @return int Delay in seconds. 0 disables the cooldown (the version goes live immediately).
*/
function wporg_themes_get_release_cooldown_delay( $theme_slug = '' ) {
/**
* Filters the release cooldown delay for a theme.
*
* Return 0 to disable the cooldown (the approved version goes live immediately), or a
* larger/smaller number of seconds to lengthen or shorten the delay for this theme.
*
* @param int $delay The default delay in seconds (WPORG_THEMES_RELEASE_COOL_DOWN_DELAY).
* @param string $theme_slug The slug of the theme being acted upon, or '' when not known.
*/
return (int) apply_filters( 'wporg_themes_release_cooldown_delay', WPORG_THEMES_RELEASE_COOL_DOWN_DELAY, $theme_slug );
}
Comment on lines +79 to +90

/**
* Things to change on activation.
*/
Expand Down Expand Up @@ -400,9 +446,9 @@ function wporg_themes_author_lookup() {
/**
* Handles updating the status of theme versions.
*
* @param int $post_id Post ID.
* @param string $current_version The theme version to update.
* @param string $new_status The status to update the current version to.
* @param int $post_id Post ID.
* @param string $current_version The theme version to update.
* @param string $new_status The status to update the current version to.
* @return int|bool Meta ID if the key didn't exist, true on successful update,
* false on failure.
*/
Expand All @@ -423,6 +469,7 @@ function wporg_themes_update_version_status( $post_id, $current_version, $new_st
// There can only be one version with these statuses:
case 'new':
case 'live':
case 'approved':
// Discard all previous versions with that status.
foreach ( array_keys( $meta, $new_status ) as $version ) {
if ( version_compare( $version, $current_version, '<' ) ) {
Expand Down
Loading