Skip to content

Themes: Add a 24-hour release cooldown between approval and serving to users#651

Closed
dd32 wants to merge 15 commits into
WordPress:trunkfrom
dd32:add/claude/theme-release-cooldown
Closed

Themes: Add a 24-hour release cooldown between approval and serving to users#651
dd32 wants to merge 15 commits into
WordPress:trunkfrom
dd32:add/claude/theme-release-cooldown

Conversation

@dd32
Copy link
Copy Markdown
Member

@dd32 dd32 commented May 25, 2026

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:

  • A compromised committer account (phished credentials, leaked SVN password, a co-committer turning malicious) on an established theme can push code that auto-installs on every site running that theme before anyone notices, since theme updates take the auto-approval path.
  • A hostile maintainer of an established theme can ship a malicious release at any time; current automated checks and the themes team's review currently catch issues after the release is already in the field. By the time something is flagged, sites have already updated.
  • A theme author who notices their own mistake — a botched release, a regression, a missing file, a credential left in a bundled file — has no grace period to fix it. The bad version is already shipping.

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

  • A grace period to catch your own mistakes. Theme updates auto-approve as you commit them today — if you notice an issue within the cooldown window, uploading a corrected version replaces the in-cooldown release before it ever reaches user sites.
  • A buffer against account compromise. If your SVN credentials are leaked, an attacker can no longer go from "credentials in hand" to "code on every site" within seconds. You (or the themes team) have hours to detect and revoke before any user-facing harm.
  • No change to your workflow. You upload as you always have. Auto-approved updates still happen automatically; first-time themes still flow through Trac review. The only visible difference is an "approved, going live in ~N hours" email up front so you're not left wondering why your new version isn't live yet.
  • A clear escape hatch for security fixes. If a release is patching an actively-exploited vulnerability and the cooldown is in the way, reach out to themes@wordpress.org. A trusted reviewer can mark it live in seconds on Trac.

How this helps WordPress.org

  • A real window for scanners to do their job. Theme Check, security scanners, and any future automated checks now have hours instead of seconds to evaluate a release before it propagates — including for the auto-approved update path that bypasses human review today.
  • A real window for human moderation. The themes team can act on a flagged release while it is still gated — pull it, contact the author, request changes — without needing to chase a version already installed on millions of sites.
  • Containment of a worst-case supply-chain event. If a malicious release does slip through the auto-approval path, the blast radius is bounded by the cooldown window rather than "every site running this theme within hours".
  • Layered defense, not a replacement. First-time theme review on Trac still applies on top; suspend/delist tooling continues to work unchanged; rollback still bypasses the cooldown for emergency restores.
  • Built on the existing Trac workflow. The cooldown holding state is Trac's existing approved status, 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 drives latest_version().

How it works

The cooldown holding state is Trac's existing approved ticket status (the step between reviewing and closed/live), rather than a WordPress-only meta state.

  • Auto-approved updates no longer close their ticket as live on upload. Instead themetracbot runs a new new_no_review_delay workflow action (new → approved), and the upload class sets the version's _status to approved. The previously-live version keeps being served by the API.
  • First-time submissions continue to flow through review and land in approved via the reviewer's existing approve action. They are not promoted on a timer — a trusted reviewer still marks them live by hand (approve_and_live / live).
  • A new theme_directory_release_to_live cron runs every 15 minutes (via the theme directory's Jobs Manager, alongside the existing trac-sync). It queries Trac for theme update tickets in the approved status, and for any that have been approved for at least WPORG_THEMES_RELEASE_COOL_DOWN_DELAY (24h, measured from the ticket's Trac changetime), it:
    1. marks the version live via wporg_themes_update_version_status(), firing the existing publish / wp-themes.com update / GlotPress import / "now live" author email machinery; and
    2. closes the Trac ticket as resolution=live (via the widened new_no_review action, now new,approved → closed) so it leaves the approved state.
  • trac-sync learns the approved status (mirroring it into the _status meta) and allows the approved → live transition, so a reviewer force-releasing on Trac is reflected on the next sync.
  • An "approved, going live in ~N hours" email is sent to the author when an update enters the cooldown (first-time approvals don't get it, since they aren't on a timer).

Trac workflow changes

This PR does update trac.wordpress.org/conf/workflow-themes.ini:

  • adds new_no_review_delay (new → approved, set_owner_to_self, TICKET_CREATE) — the bot action for auto-approving an update into the cooldown; and
  • widens new_no_review from new → closed to new,approved → closed (still resolution=live, TICKET_CREATE) so the release-to-live cron can mark approved updates live.

The trusted-reviewer approve_and_live and live actions are unchanged and serve as the force-release path.

Disabling

Setting WPORG_THEMES_RELEASE_COOL_DOWN_DELAY to 0 (e.g. via the shared WPORG_PLUGIN_THEME_RELEASE_DELAY) makes auto-approved updates mark live immediately on upload again (the original new_no_review path), and no cooldown email is sent.

Mirrors #650 for themes.

Test plan

  • Upload an auto-approved update for an existing live theme. Confirm the Trac ticket lands in the approved status (not closed/live), _status[$version] is approved, the previous live version is still returned by the themes API, and the author receives the "approved, going live in ~24h" email.
  • Run / wait for the theme_directory_release_to_live cron after 24h. Confirm the version transitions to live, _live_version updates, wp-themes.com is updated, the GlotPress import fires, the author gets the "now live" email, and the Trac ticket is closed as resolution=live.
  • Confirm the cron does not promote a ticket that has been approved for less than 24h.
  • Approve a first-time theme on Trac (reviewer approveapproved). Confirm it is not auto-promoted by the cron, and that a trusted reviewer's live / approve_and_live action still publishes it (reflected on the next trac-sync).
  • Upload a second update during an active cooldown. Confirm the older approved version is demoted to old, the newer version becomes approved, and the cron promotes the newer version.
  • Force-release a cooling-down update via Trac's live action as a trusted reviewer. Confirm trac-sync marks the version live on its next run.
  • Roll back a live version. Confirm the previous live version is restored immediately without entering cooldown.
  • Reopen a ticket on Trac while a version is approved. Confirm _status[$version] returns to new.
  • Set WPORG_THEMES_RELEASE_COOL_DOWN_DELAY to 0. Confirm auto-approved updates close as live immediately on upload, no cooldown email is sent, and the metabox shows no approved option for non-approved versions.
  • Change a version's status manually from the Theme Versions admin metabox to Live. Confirm it takes effect immediately.

…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>
Copilot AI review requested due to automatic review settings May 25, 2026 03:01
@github-actions
Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props dd32.

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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 live transitions into an internal approved status, then promotes to live via 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.

Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
dd32 and others added 2 commits May 25, 2026 13:09
…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>
Copilot AI review requested due to automatic review settings May 25, 2026 03:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
dd32 and others added 2 commits May 25, 2026 13:35
…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>
Copilot AI review requested due to automatic review settings May 25, 2026 03:37
…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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
dd32 added a commit to dd32/wordpress.org that referenced this pull request May 25, 2026
…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>
dd32 and others added 2 commits May 25, 2026 13:46
…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>
Copilot AI review requested due to automatic review settings May 25, 2026 03:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
Comment thread wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php Outdated
dd32 and others added 2 commits May 28, 2026 03:03
… 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>
Copilot AI review requested due to automatic review settings May 28, 2026 06:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

dd32 and others added 2 commits May 28, 2026 07:16
…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>
Copilot AI review requested due to automatic review settings June 2, 2026 03:02
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

dd32 and others added 2 commits June 3, 2026 03:48
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>
Copilot AI review requested due to automatic review settings June 3, 2026 05:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment on lines +79 to +90
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 1585 to 1596
/*
* 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
Comment on lines +1354 to +1355
$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 +176 to +178
// 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 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants