Themes: Add a 24-hour release cooldown between approval and serving to users#651
Themes: Add a 24-hour release cooldown between approval and serving to users#651dd32 wants to merge 15 commits into
Conversation
…serving to users. Mirrors the plugin-side cooldown (PR WordPress#650) for the themes directory. When a theme version is approved (by a reviewer closing the Trac ticket live, or via the auto-approval path for theme updates), it now enters a new internal 'approved' status that holds the version back from the themes API for WPORG_THEMES_RELEASE_COOL_DOWN_DELAY (24h) before being promoted to 'live'. The previous live version (if any) continues to be served from the existing _status meta during the window. Implementation: - A new 'approved' value alongside new/live/old in the _status meta. The redirect from 'live' to 'approved' happens inside wporg_themes_update_version_status() so all entry points (trac-sync, auto-approved updates in the upload class, rollback) flow through the same gate. The cron handler re-enters with old_status='approved' and writes through. - Per-version _approval_time and _release_delay meta capture the cooldown active at approval time, so future constant changes don't retroactively affect in-flight cooldowns. Reviewers force-release by zeroing _release_delay. - A new wporg_themes_release_to_live:{slug} cron event is scheduled at approval; the colon-based hook (matching the plugin directory pattern) lets wp_clear_scheduled_hook() target a single theme's pending event without args lookup. The themes jobs Manager picks up a wildcard handler matching the plugin directory's mechanism. - A reviewer Force-release control (requires suspend_themes cap) on the Theme Versions metabox, gated by an audit-logged reason via wp_insert_comment(). - Authors receive an "approved, going live in 24h" email when the cooldown starts, in addition to the existing "now live" email when it actually elapses, so the gap between Trac-approved and serving-to-users is explained. - Rollbacks and manual admin metabox saves pass bypass_cooldown=true -- the operator is explicitly pushing a version live and shouldn't wait. Note: this implementation does not require any Trac workflow changes. Trac continues to close tickets with resolution=live; the WordPress.org side translates that to the internal 'approved' state. A follow-up could add an 'approved' resolution to Trac itself for parity on the reviewer UI, but it is not required for this gate to work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
… in 0 hours" emails. When WPORG_THEMES_RELEASE_COOL_DOWN_DELAY is 0 (feature off), the redirect from 'live' to 'approved' is already skipped — but nothing prevented an admin from manually selecting 'approved' in the Theme Versions metabox dropdown. That would have stored a 0-second cooldown, scheduled a same-second cron, and emailed the author "approved, going live in 0 hours". - wporg_themes_update_version_status() now folds an explicit 'approved' into 'live' when the cooldown is disabled at the constant level. - The approval handler bails defensively if it sees a 0 release_delay (so even non-default code paths don't email or schedule). - The 'Approved (in cooldown)' option is hidden from the admin metabox dropdown unless the feature is on (or the version is already in that state, so historical 'approved' rows remain transitionable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements a supply-chain hardening measure for the Theme Directory by introducing a 24-hour “approved” holding state between a version being approved and it becoming the live version served via the themes API, mirroring the plugin-directory release cooldown approach.
Changes:
- Adds a release cooldown gate that redirects
livetransitions into an internalapprovedstatus, then promotes tolivevia a scheduled cron event. - Introduces a colon-based per-theme cron hook (
wporg_themes_release_to_live:{slug}) and a wildcard registration mechanism to resolve and run those hooks. - Adds wp-admin UI for cooldown visibility plus a reviewer “force-release” control with required reason logging.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php | Adds cooldown constant, approved status routing, per-version cooldown meta, cron promotion handler, and force-release implementation. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php | Treats approved as already-processed for Trac sync to avoid repeated live transitions during cooldown. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-manager.php | Registers wildcard handlers for colon-based cron hooks so per-theme scheduled events can execute. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-upload.php | Treats approved similarly to live for Trac ticket priority when a theme is effectively already accepted. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php | Adds “Approved (in cooldown)” status option, renders cooldown countdown + force-release UI, and handles the force-release submit path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ob::get on HyperDB lag. Two findings from the PR review on WordPress#651: - The "release cooldown" email body hard-coded "delays by 24 hours". Replace the literal with the per-version release_delay so the message stays accurate if WPORG_THEMES_RELEASE_COOL_DOWN_DELAY is ever tuned. - Mirror the plugin-directory's Cavalcade Job::get() fallback: if the lookup returns nothing because a HyperDB read replica hasn't caught up yet, retry against the master server before giving up. Without it, colon-based hooks can run with no handler attached when triggered manually via wp cavalcade run while replication is lagging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sed.
Drop the colon-based hook name (wporg_themes_release_to_live:{slug}) and the
wildcard-cron-handler registration that went with it in jobs/class-manager.php.
The handler now lives in theme-directory.php and is hooked directly with
add_action(), with $post_id passed as the single cron arg. Scheduling /
clearing use the same args tuple so they target one theme's pending event
without the dedicated dispatch machinery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cap. Replace the suspend_theme check at the two cooldown call sites (the metabox force-release control + its save_post handler) with theme_review, mapped via map_meta_cap to the same suspend_themes primitive the moderator roles already carry. Same audience as before; the name now reads as a reviewer action rather than a moderation/take-down action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n_edit. Theme review (the moderator workflow on Trac) and admin-edit actions on the theme post (force-release, etc) are conceptually separate even though they fall to the same role audience today. Use theme_admin_edit so the check sites read as "is this user allowed to admin-edit this theme post" rather than implying a tie to the review workflow. Still maps onto the existing suspend_themes primitive, so the audience is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SE_DELAY constant. Lets the plugin and theme directory cooldowns be tuned (or disabled) in lockstep from a single override point. Falls back to the hard-coded 24h limit when the shared constant isn't set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ASE_DELAY constant. Lets the plugin and theme directory cooldowns be tuned (or disabled) in lockstep from a single override point. Falls back to the hard-coded 24h limit when the shared constant isn't set. Mirrors the matching change on the theme side in WordPress#651. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…unction boundary. wporg_themes_force_release_version() is a general utility; don't rely on the caller to sanitize $reason before it lands in comment_content. Wrap the value in sanitize_textarea_field() inside the function so the audit log stays plain text regardless of where the call comes from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rust the caller. Plugin Directory's Tools::audit_log() — the equivalent moderator-action log helper — stores its $note directly into comment_content without sanitizing, on the convention that the caller sanitizes at the user-input boundary (sanitize_textarea_field on $_POST). Match that here rather than re-running a tag-stripping pass inside the function on already-clean input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… release-to-live cron. Replaces the internal-only `approved` meta state and per-theme single-event crons with Trac's existing `approved` status as the holding state, promoted to live by a single recurring cron once the cooldown has elapsed. - workflow-themes.ini: add `new_no_review_delay` (new -> approved) for the themetracbot auto-approval of updates, and widen `new_no_review` to `new,approved -> closed/live` so the cron can mark approved tickets live. - trac-sync: map the `approved` Trac status to the `approved` version status, allow the approved -> live transition, and add release_to_live(): a 15-minute cron (scoped to the `theme update` priority) that marks `approved` updates live once they have been approved for WPORG_THEMES_RELEASE_COOL_DOWN_DELAY, then closes the ticket as resolution=live. - upload: auto-approved updates use `new_no_review_delay` and land in `approved` (instant-live retained when the cooldown is disabled). - Drop the per-version _approval_time/_release_delay meta, the wp-admin force-release control and its meta cap; force-release is now Trac's existing approve_and_live / live actions. The author cooldown email only fires for updates (a theme with a prior live version). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…persede-close stale tickets; drop the approval email. - Re-uploading while the previous version is still on an open ticket (`new` or `approved`) now updates that same ticket instead of opening a new one, so an approved update's release delay restarts from the latest upload and the superseded version is simply demoted to `old`. - The release-to-live cron now closes a superseded `approved` ticket (whose version was demoted to `old`) as closed-newer-version-uploaded via a new themetracbot `new_no_review_superseded` action, rather than mislabelling it live — keeping rollback's resolution=live lookup correct. - Drop the "approved, going live in N hours" author email. - Remove "cooldown" from user-facing strings (metabox label; the cron's Trac comment is now "Marking live."). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…import. release_to_live() now only advances Trac — it closes `approved` theme-update tickets that are past the delay as resolution=live — and is called from cron_trigger() right before the normal sync, which imports the now-live ticket into WordPress like any other. This removes the post-mutation logic, the supersede branch and its `new_no_review_superseded` workflow action, and the separate theme_directory_release_to_live cron (class-manager.php is back to matching trunk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the new_no_review ticket_update args array so each associative value starts on its own line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce wporg_themes_get_release_cooldown_delay( $theme_slug ) which returns the WPORG_THEMES_RELEASE_COOL_DOWN_DELAY default passed through the new `wporg_themes_release_cooldown_delay` filter, with the theme slug passed along when known. The filter can shorten, extend, or remove (return 0) the delay per-theme. Used at upload time and, per-theme, by the release-to-live cron. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erride. Default WPORG_THEMES_RELEASE_COOL_DOWN_DELAY to 0 (cooldown disabled) for now, to be raised once the workflow is ready, and wrap the define() in an if ( ! defined() ) guard so it can be pre-defined from global config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| 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 ); | ||
| } |
| /* | ||
| * 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 |
| $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 ); |
| // 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 ); |
The problem
Today, when a new theme version is uploaded to an established theme, it goes from SVN commit to "available via the update API and to every site running that theme" within seconds — the upload class auto-approves theme updates (when the previous version is live), the Trac ticket is opened and closed in the same request, and the next theme update-check picks up the new ZIP. No human is in the loop. That's most theme directory traffic today.
For first-time theme submissions a reviewer does step in on Trac, but once that ticket is marked live the same near-instant publish happens.
This narrow turnaround leaves the window open for a number of supply-chain risks:
This PR introduces a short cooldown between approval and serve, so there's a real window for both automated scanners and humans (the author included) to catch problems before the update reaches sites. It mirrors the equivalent plugin-side gate in #650.
How this helps theme authors
themes@wordpress.org. A trusted reviewer can mark it live in seconds on Trac.How this helps WordPress.org
approvedstatus, so reviewers see the gate directly on Trac. The previous live version simply continues to be served until the new one is promoted, using the same_status[version] = 'live'lookup that already driveslatest_version().How it works
The cooldown holding state is Trac's existing
approvedticket status (the step betweenreviewingandclosed/live), rather than a WordPress-only meta state.liveon upload. Instead themetracbot runs a newnew_no_review_delayworkflow action (new → approved), and the upload class sets the version's_statustoapproved. The previously-live version keeps being served by the API.approvedvia the reviewer's existingapproveaction. They are not promoted on a timer — a trusted reviewer still marks them live by hand (approve_and_live/live).theme_directory_release_to_livecron runs every 15 minutes (via the theme directory's JobsManager, alongside the existing trac-sync). It queries Trac fortheme updatetickets in theapprovedstatus, and for any that have been approved for at leastWPORG_THEMES_RELEASE_COOL_DOWN_DELAY(24h, measured from the ticket's Trac changetime), it:liveviawporg_themes_update_version_status(), firing the existing publish /wp-themes.comupdate / GlotPress import / "now live" author email machinery; andresolution=live(via the widenednew_no_reviewaction, nownew,approved → closed) so it leaves theapprovedstate.trac-synclearns theapprovedstatus (mirroring it into the_statusmeta) and allows theapproved → livetransition, so a reviewer force-releasing on Trac is reflected on the next sync.Trac workflow changes
This PR does update
trac.wordpress.org/conf/workflow-themes.ini:new_no_review_delay(new → approved,set_owner_to_self,TICKET_CREATE) — the bot action for auto-approving an update into the cooldown; andnew_no_reviewfromnew → closedtonew,approved → closed(stillresolution=live,TICKET_CREATE) so the release-to-live cron can mark approved updates live.The trusted-reviewer
approve_and_liveandliveactions are unchanged and serve as the force-release path.Disabling
Setting
WPORG_THEMES_RELEASE_COOL_DOWN_DELAYto0(e.g. via the sharedWPORG_PLUGIN_THEME_RELEASE_DELAY) makes auto-approved updates mark live immediately on upload again (the originalnew_no_reviewpath), and no cooldown email is sent.Mirrors #650 for themes.
Test plan
approvedstatus (notclosed/live),_status[$version]isapproved, the previous live version is still returned by the themes API, and the author receives the "approved, going live in ~24h" email.theme_directory_release_to_livecron after 24h. Confirm the version transitions tolive,_live_versionupdates,wp-themes.comis updated, the GlotPress import fires, the author gets the "now live" email, and the Trac ticket is closed asresolution=live.approvedfor less than 24h.approve→approved). Confirm it is not auto-promoted by the cron, and that a trusted reviewer'slive/approve_and_liveaction still publishes it (reflected on the next trac-sync).approvedversion is demoted toold, the newer version becomesapproved, and the cron promotes the newer version.liveaction as a trusted reviewer. Confirm trac-sync marks the version live on its next run.approved. Confirm_status[$version]returns tonew.WPORG_THEMES_RELEASE_COOL_DOWN_DELAYto0. Confirm auto-approved updates close asliveimmediately on upload, no cooldown email is sent, and the metabox shows noapprovedoption for non-approved versions.Live. Confirm it takes effect immediately.