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 9 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>
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 closes 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 (auto-approval at upload for updates, or reviewer ticket-close for new themes) 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 reviewer can force-release in seconds with the reason logged for audit.How this helps WordPress.org
approvedintermediate state on the existing_statuspost meta, with no changes required to the themes API or to Trac workflows. 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
When a version transitions to
live— whether via the upload class's auto-approval path for theme updates, trac-sync'sresolution=livefor first-time themes, or the rollback path —wporg_themes_update_version_status()redirects it into a new internalapprovedholding state. Awporg_themes_release_to_livecron event (with the theme post ID passed as the single arg) is scheduled forWPORG_THEMES_RELEASE_COOL_DOWN_DELAY(24h) from approval. When it fires, the cron handler re-enters withold_status='approved'and falls through tolive, at which point the existingwporg_themes_approve_version()handler publishes the post, updateswp-themes.com, fires the GlotPress import, and emails the author._approval_timeand_release_delaymeta capture the cooldown active at approval, so future constant changes don't retroactively affect in-flight cooldowns. Force-release zeroes_release_delayand writes through immediately.bypass_cooldown=true— the operator is explicitly pushing a version live and shouldn't wait.theme_admin_editmeta cap (mapped to the existingsuspend_themesprimitive on moderator roles) and a reason, logged as an internal-note comment) shortcuts the cron for emergency security fixes.WPORG_THEMES_RELEASE_COOL_DOWN_DELAYto0disables the feature: no redirect, no cron, no extra UI, and explicitapprovedselections are folded toliveso nothing strands or sends a "0 hours until live" notification.Trac workflow note
This PR does not require any Trac workflow changes. Trac continues to close tickets with
resolution=live, and the WordPress.org side translates that to the internalapprovedstate. A follow-up could add anapprovedresolution to Trac itself for parity on the reviewer UI (so reviewers see the cooldown state on Trac directly), but it isn't required for the gate to work.Mirrors #650 for themes.
Test plan
_status[$version]ends up asapproved(notlive),_approval_timeand_release_delayare populated, awporg_themes_release_to_livecron event (args[ $post_id ]) is scheduled for ~24h out, the previous live version continues to be returned by the themes API during the window, and the author receives the "approved, going live in 24h" email._status[$version]transitions tolive,_live_versionis updated,wp-themes.comis updated, the GlotPress import fires, and the author receives the existing "now live" email.resolution=livefor a first-time theme. Confirm the same flow:approvedstate, cron scheduled, email sent; after 24h the post is published and the directory shows the theme.approvedversion is demoted toold, the new version becomesapproved, and the cron is replaced so the deferred event targets the newer version.theme_admin_edit(i.e. a moderator role carryingsuspend_themes), confirm the Force-release control + reason textarea render on the Theme Versions metabox during cooldown. Submit with a reason. Confirm_release_delay[$version]is zeroed,_status[$version]set toliveimmediately, the existing approve_version handler runs, and the internal-note comment includes the reason.approvedstate. Confirm_status[$version]returns tonewand the scheduled cron is cleared.WPORG_THEMES_RELEASE_COOL_DOWN_DELAYto0. Confirm: no redirect toapproved, no cron scheduled, no metabox cooldown section, no extra email, and theApproved (in cooldown)dropdown option is hidden. Manually selectingapprovedin the metabox while disabled folds tolive(no stranded version, no 0-hour email).Live. Confirm it bypasses cooldown and takes effect immediately.