Skip to content

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

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

Themes: Add a 24-hour release cooldown between approval and serving to users#651
dd32 wants to merge 9 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 closes 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 (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

  • 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 (a typo, a regression, a missing file, a credential left in), uploading a corrected version replaces the in-cooldown release before it ever reaches user sites. Today, the broken version is already in the field by the time you spot it.
  • 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, review, and approve as you always have. Auto-approved theme updates work identically; 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 reviewer can force-release in seconds with the reason logged for audit.

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. Findings can arrive before the release ships, not after.
  • 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. Today the team has effectively no opportunity to intervene on auto-approved updates.
  • Containment of a worst-case supply-chain event. If a malicious release does slip through the auto-approval path (compromised committer, hostile maintainer of an established theme), 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.
  • Operational simplicity. Implemented as an internal approved intermediate state on the existing _status post 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 drives latest_version().

How it works

When a version transitions to live — whether via the upload class's auto-approval path for theme updates, trac-sync's resolution=live for first-time themes, or the rollback path — wporg_themes_update_version_status() redirects it into a new internal approved holding state. A wporg_themes_release_to_live cron event (with the theme post ID passed as the single arg) is scheduled for WPORG_THEMES_RELEASE_COOL_DOWN_DELAY (24h) from approval. When it fires, the cron handler re-enters with old_status='approved' and falls through to live, at which point the existing wporg_themes_approve_version() handler publishes the post, updates wp-themes.com, fires the GlotPress import, and emails the author.

  • Per-version _approval_time and _release_delay meta capture the cooldown active at approval, so future constant changes don't retroactively affect in-flight cooldowns. Force-release zeroes _release_delay and writes through immediately.
  • Rollback and direct admin metabox saves pass bypass_cooldown=true — the operator is explicitly pushing a version live and shouldn't wait.
  • A reviewer Force-release control on the Theme Versions metabox (requires the theme_admin_edit meta cap (mapped to the existing suspend_themes primitive on moderator roles) and a reason, logged as an internal-note comment) shortcuts the cron for emergency security fixes.
  • Setting WPORG_THEMES_RELEASE_COOL_DOWN_DELAY to 0 disables the feature: no redirect, no cron, no extra UI, and explicit approved selections are folded to live so 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 internal approved state. A follow-up could add an approved resolution 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

  • Upload an auto-approved theme update for an existing live theme. Confirm _status[$version] ends up as approved (not live), _approval_time and _release_delay are populated, a wporg_themes_release_to_live cron 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.
  • Wait for / manually fire the cron event. Confirm _status[$version] transitions to live, _live_version is updated, wp-themes.com is updated, the GlotPress import fires, and the author receives the existing "now live" email.
  • Reviewer closes a Trac ticket with resolution=live for a first-time theme. Confirm the same flow: approved state, cron scheduled, email sent; after 24h the post is published and the directory shows the theme.
  • Upload a second new version during an active cooldown. Confirm the older approved version is demoted to old, the new version becomes approved, and the cron is replaced so the deferred event targets the newer version.
  • As a user with theme_admin_edit (i.e. a moderator role carrying suspend_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 to live immediately, the existing approve_version handler runs, and the internal-note comment includes the reason.
  • As a non-reviewer admin, confirm the Force-release UI is not rendered and the action is rejected if attempted directly.
  • 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 in approved state. Confirm _status[$version] returns to new and the scheduled cron is cleared.
  • Set WPORG_THEMES_RELEASE_COOL_DOWN_DELAY to 0. Confirm: no redirect to approved, no cron scheduled, no metabox cooldown section, no extra email, and the Approved (in cooldown) dropdown option is hidden. Manually selecting approved in the metabox while disabled folds to live (no stranded version, no 0-hour email).
  • Change a version's status manually from the Theme Versions admin metabox to Live. Confirm it bypasses cooldown and 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
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
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.

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.

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