Skip to content

Plugins: Add a release delay between SVN commit of update, and publish#650

Closed
dd32 wants to merge 15 commits into
WordPress:trunkfrom
dd32:fix/claude/plugin-update-cooldown
Closed

Plugins: Add a release delay between SVN commit of update, and publish#650
dd32 wants to merge 15 commits into
WordPress:trunkfrom
dd32:fix/claude/plugin-update-cooldown

Conversation

@dd32
Copy link
Copy Markdown
Member

@dd32 dd32 commented May 25, 2026

The problem

Today, a plugin release goes from SVN commit to "available via the update API" within seconds — as soon as the import pipeline finishes. There is no human-scale window between the commit and millions of sites pulling the new ZIP on their next
update check.

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) can push code that auto-installs on every site running that plugin before anyone notices.
  • Scanners (Gandalf, PCP) and the plugins team do run, but their findings currently arrive after the release is already in the field. By the time a malicious release is flagged, sites have already updated.
  • A plugin author who notices their own mistake — a botched release, a regression, an accidental debug statement, a leaked secret in a bundled file — has no grace period to fix it. The bad version is already shipping.

This PR introduces a short cooldown between commit 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.

How this helps plugin authors

  • A grace period to catch your own mistakes. If you commit a release and notice an issue within the cooldown window — a typo, a regression, a missing file, a credential left in — committing again resets the timer and your sites never see
    the broken version. 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 plugins team) have hours to detect and revoke before any user-facing harm.
  • No change to your release workflow. You commit as you always have. Stable tag bumps, trunk releases, release-confirmation flows all work identically. The only visible difference is a small notice on your plugin page during the cooldown window telling you when the release will reach sites.
  • A clear escape hatch for security fixes. If a release is patching an actively-exploited vulnerability and the cooldown is in the way, the notice explicitly tells you to contact plugins@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. Gandalf, PCP, and any future automated checks have hours instead of seconds to evaluate a release before it propagates. Findings now arrive before the release ships, not after.
  • A real window for human moderation. The plugins team can act on a flagged release while it is still gated — pull it, contact the author, request changes — without needing to chase versions that are already installed on millions of sites.
  • Containment of a worst-case supply-chain event. If a malicious release does slip through (compromised committer, hostile maintainer), the blast radius is bounded by the cooldown window rather than "everyone on auto-update within 24 hours."
  • Layered defense, not a replacement. The existing manual-updates-24hr rollout strategy still applies on top — authors who opted in get the cooldown plus their auto-update delay. Closure and existing review tooling continue to work unchanged.
  • Operational simplicity. Implemented as a deferred write to update_source, with no changes required to the closed-source api.wordpress.org update-check endpoint or its caches. The previous version simply continues to be served until the new row lands.

Test plan

  • Commit a plugin version. Confirm update_source continues to show the previous version. Confirm a release_to_update_api:{slug} cron event is scheduled for release_time + 24h.
  • Wait for / manually fire the cron event. Confirm the new version lands in update_source and the cron event is cleared.
  • Commit a second version during an active cooldown. Confirm the scheduled cron is replaced and the deferred event targets the newer version.
  • On a plugin page, logged in as a committer of a plugin currently in cooldown, confirm the notice renders with version + remaining time + contact line. Confirm it disappears once cooldown elapses.
  • As a reviewer (plugin_review cap), confirm the "Force release" button + reason textarea render in the Controls metabox during cooldown. Submit with a reason. Confirm: row in update_source is rewritten immediately with the new
    version, release_delay on the release meta is zeroed, audit log entry includes the reason.
  • As a non-reviewer admin user, confirm the force-release UI is not rendered and the action is rejected if attempted directly.
  • Set RELEASE_COOL_DOWN_DELAY to 0. Confirm: no notice on plugin pages, no force-release UI in metabox, no deferral on commit.
  • Commit a release on a plugin in disabled status. Confirm the cooldown applies the same way.
  • On a plugin using manual-updates-24hr, confirm both gates apply in sequence: 24h cooldown (no version served), then 24h after release with disable_autoupdate = true.

dd32 and others added 12 commits May 15, 2026 18:00
…releases.

Introduces a release cooldown that holds new versions back from the update API
for RELEASE_COOL_DOWN_DELAY (48hrs) after commit / final author confirmation.
The previous version continues to be served during the window, giving scanners
and reviewers time to flag supply-chain attacks before the new version ships
to sites.

Plugin reviewers can bypass the cooldown for urgent security fixes via a new
Force-release control on the Plugin Controls metabox (requires a reason; logged
via Tools::audit_log).

Closed/disabled plugins, rebuild scripts, and other status-change paths
bypass the cooldown so closures take effect immediately. release_time is
re-anchored to the moment the version actually becomes public so the existing
phased_rollout manual-updates-24hr window still measures from public
availability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… just publish.

Disabled plugins keep available=1 in update_source and continue to serve
updates through the API. The cooldown gate was only checking for publish,
so a new version committed to a disabled plugin would write through
immediately, defeating the purpose for that subset. Closed plugins remain
excluded -- available flips to 0, so there is nothing to gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoist the force_released early-exit into the caller so the helper signature
shrinks to (release_time, existing_version, new_version) plus an injectable
now. Drop the post_status check from both the helper and the release_time
anchor -- callers are responsible for bypassing when their context calls
for it. Status_Transitions::flush_caches() now passes bypass=true so
closures and disables take effect immediately rather than waiting for an
in-flight cooldown window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… is 0; tidy.

When RELEASE_COOL_DOWN_DELAY is 0 the feature is off -- skip the deferral,
the release_time anchor, the wp-admin Controls metabox section, the
shortcode info banner, and the per-release status line. Sites running with
the cooldown disabled see no related UI at all.

Simplifications:
- Inline get_cooldown_defer_time() (single caller; pure cooldown math is 4
  lines).
- Drop the metabox force-released audit line; Tools::audit_log writes an
  internal note that the Internal Notes metabox already renders.
- Drop the "Serving to sites since X ago" line in the shortcode -- the
  existing "Released X ago by Y" line above already covers that state.
- Drop the unused $user arg to audit_log() in force_release(); it defaults
  to wp_get_current_user() which is what we set $user to anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The form only renders for users with the plugin_review capability, the
save_post handler re-checks the same capability, and the whole thing
posts inside wp-admins post.php form which is already nonced by core.
The extra _force_release_nonce was just noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ease_delay.

Stores the cooldown delay on the release at creation time via add_release()s
defaults, captured from the RELEASE_COOL_DOWN_DELAY constant. The cooldown
gate and release_time anchor both read release_delay (?? 0) instead of the
global constant -- pre-feature releases coalesce to 0 and write through
unchanged, future constant changes do not retroactively affect in-flight
releases.

Force-release just sets release_delay = 0 on the release, replacing the
four force_released, force_released_by, force_released_at, and
force_released_reason fields. The reviewer, time, and reason are already
captured by Tools::audit_log() so storing them twice was redundant.

Drops the author-facing Force-released by a plugin reviewer X ago message
along with that meta -- authors just see the cooldown countdown disappear.

Verifies core post.php nonce explicitly in save_post to satisfy phpcs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cron schedule helper collapses to wp_clear_scheduled_hook +
wp_schedule_single_event -- the per-plugin hook name makes args unnecessary
for dedupe and the always-clear approach is shorter than the next-scheduled
dance. The cron handler recovers the slug from current_filter() so no args
need flow through.

bypass_cooldown is no longer load-bearing once release_delay is per-release:

  - cron_trigger_release: fires at cooldown_until, gate check
    (cooldown_until > time()) is false by definition.
  - force_release: sets release_delay = 0 before update_single_plugin,
    which short-circuits the gate.
  - rebuild script: cooldown gate correctly preserves in-flight cooldowns
    instead of stomping them.
  - status_transitions: status changes that do not bump version match
    existing_version == new_version and write through naturally. Status
    changes that coincide with a deferred new version write through at
    cooldown_until (worst-case 48h delay) -- the OLD safe version
    continues to be served in the meantime, which is the right call.

Also reorders force_release() to call audit_log BEFORE the add_release that
zeroes release_delay, so the log line captures the original delay rather
than relying on a stale in-memory copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… the tests.

Replaces the static info banner on the release-confirmation shortcode page
with a targeted notice that only appears for committers when one of their
plugins has a release currently inside the cooldown window. Matches the
existing frontend_unconfirmed_releases_notice() pattern (same metabox
plumbing via template-tags.php and plugin-single.php).

Notice copy names the version, the relative time until it ships, the
cooldown duration, and the plugins@wordpress.org contact for security
expedites.

Drops the Plugin_Update_Cooldown_Test.php file -- only covered
compute_release_time() which is straightforward inline logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 02:34
@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.

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

This PR introduces a “release cooldown” window between when a plugin release is committed (or finally confirmed) and when the new version is written to update_source (and thus becomes available via the plugin update API). It also surfaces the cooldown state to plugin committers on the public plugin page and provides reviewers an audited “force release” escape hatch.

Changes:

  • Add a per-release release_delay (seeded from RELEASE_COOL_DOWN_DELAY) and defer update_source writes for new versions until the cooldown elapses.
  • Implement a per-plugin deferred cron hook (release_to_update_api:{slug}) and a reviewer-only force-release flow with required reason + audit log entry.
  • Display cooldown status in release confirmation UI and show an informational notice to committers on the public plugin page during cooldown.

Reviewed changes

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

Show a summary per file
File Description
wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/template-parts/plugin-single.php Renders the new cooldown notice block on plugin pages.
wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/inc/template-tags.php Adds a theme template tag wrapper for the cooldown notice.
wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php Shows cooldown status in release confirmation output + adds frontend committer notice.
wordpress.org/public_html/wp-content/plugins/plugin-directory/plugin-directory.php Defines the global cooldown constant.
wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-manager.php Registers the new colon-hook cron handler (release_to_update_api).
wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php Implements deferral logic, scheduling, release-time computation, and reviewer force-release.
wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php Persists release_delay onto newly created release records.
wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-controls.php Adds admin cooldown status display + reviewer force-release controls + save handler.
wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-status-transitions.php Minor comment punctuation adjustment.
wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php Hooks the Controls metabox save handler.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

)
);

$release_delay = (int) ( $release['release_delay'] ?? 0 );
* and humans a window to flag bad releases. Plugin reviewers can bypass the cooldown
* via the wp-admin force-release action; see Jobs\API_Update_Updater::update_single_plugin().
*/
define( __NAMESPACE__ . '\RELEASE_COOL_DOWN_DELAY', 24 * HOUR_IN_SECONDS );
dd32 and others added 2 commits May 25, 2026 13:45
…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>
Introduce get_release_cooldown_delay( $plugin_slug ) which returns the
RELEASE_COOL_DOWN_DELAY default passed through the new
`wporg_plugins_release_cooldown_delay` filter, with the plugin slug
passed along when known. The filter can shorten, extend, or remove
(return 0) the delay per-plugin. Captured onto the release in
add_release() exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 3, 2026 03:49
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 10 out of 10 changed files in this pull request and generated 3 comments.

* sites by the api.wordpress.org plugin update-check API. The previous version remains
* served until the cooldown elapses. Mitigates supply-chain attacks by giving scanners
* and humans a window to flag bad releases. Plugin reviewers can bypass the cooldown
* via the wp-admin force-release action; see Jobs\API_Update_Updater::update_single_plugin().
…verride.

Default 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>
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