Skip to content

Plugin Directory: Sync plugin status to the update API during a release cooldown - #783

Closed
obenland wants to merge 4 commits into
WordPress:trunkfrom
obenland:fix/update-source-cooldown-status-sync
Closed

Plugin Directory: Sync plugin status to the update API during a release cooldown#783
obenland wants to merge 4 commits into
WordPress:trunkfrom
obenland:fix/update-source-cooldown-status-sync

Conversation

@obenland

@obenland obenland commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a gap in the release cooldown: while a new version is being held back, API_Update_Updater::update_single_plugin() defers the whole update_source write — so a status change made during those hours never reaches the update API. Closing a plugin mid-cooldown keeps offering it to sites; reopening one keeps it withdrawn; disabling one never records its closure meta.

The fix

Only the version bump waits for the cooldown. A status change made mid-cooldown syncs the existing row immediately — availability, closure meta (closed_at/closed_reason), and last_updated — via a new update_row_availability(), while the row keeps serving the previous release's data, so version-specific columns (version, stable_tag, release_time, rollout) never describe the held version. A plugin whose first-ever release is in cooldown has no row and gets none until the cooldown expires.

Shared logic is extracted rather than duplicated: get_close_meta() (closure fields, used by the full write and the sync) and clear_plugin_caches() (the update-check/info-API cache purge, which every row write must be followed by).

Note that cron_trigger()'s out-of-date query still re-selects an in-cooldown plugin every hourly run — the row's version/stable_tag differ from the post's by construction until the cooldown expires, and that mismatch can't be resolved without releasing the version. Each re-pass is an idempotent no-op: the status sync re-writes values the row already holds.

Review fixes (second commit)

  • The pre-sync guard tested the truthiness of the row's version string, so a stored '0' (or '') read as "no row" and skipped the status sync. It now tests row existence, and the row's version and meta are fetched in one query, dropping update_row_availability()'s redundant second SELECT.
  • clear_plugin_caches() now reuses Plugins_Info_API::flush_plugin_information_cache() instead of hand-building plugin_information:{slug}:{locale} keys from raw GP_Locales casing — the API stores those keys lowercased, so the hand-built purge was a no-op for every mixed-case locale (de_DE, pt_BR, …).
  • The availability predicate is extracted into is_available() instead of being duplicated (with strictness drift) across both writers.

Testing

New tests/Update_Source_Cooldown_Test.php: the version bump stays deferred; a closure withdraws the row immediately (still on the served version); a disable records closure meta with the row still available; a reopen restores the row; a first release in cooldown creates no row.

The stub update_source/svn_access tables and the PLUGINS_TABLE_PREFIX constant come from the wp-env test environment: the after-start script imports the shared SQL file on every start (locally and in CI), and the constant is written into wp-tests-config.php. The WP test installer drops only core tables, so the stub tables persist across runs, and the test clears leftover rows per plugin.

Notes

This is split out of #777, which builds its scan-driven release block on the same "hold the version, sync the status" write path. #777 will be rebased onto this once it lands.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 11, 2026 15:24
@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 obenland.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes a release-cooldown edge case in the Plugin Directory’s Update API sync: while a new version is held back, plugin status/availability changes (close/disable/reopen) are now synced immediately to the existing update_source row, instead of being deferred along with the version bump.

Changes:

  • Update cooldown handling so only the version bump is deferred; status-dependent fields (availability + closure meta) sync immediately via a new update_row_availability().
  • Extract shared logic into helpers (get_close_meta(), clear_plugin_caches()) so both full writes and availability-only syncs stay consistent.
  • Add a PHPUnit test suite for cooldown behavior, and enhance the test bootstrap to define PLUGINS_TABLE_PREFIX and create required stub tables.

Reviewed changes

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

File Description
wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php Splits cooldown behavior into deferred version updates vs immediate availability/meta sync; extracts shared helpers.
wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php Defines PLUGINS_TABLE_PREFIX and creates stub external tables for tests using the shared SQL file.
wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/Update_Source_Cooldown_Test.php Adds coverage for cooldown scenarios (deferred version bump, close/disable/reopen sync, and no-row-first-release behavior).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php Outdated
…se cooldown.

update_single_plugin() used to defer the whole update_source write while a
new version sat inside its release cooldown, so closing, disabling, or
reopening the plugin never reached the row — sites kept being offered a
closed plugin, and a reopened one stayed withdrawn — until the cooldown
expired. Now only the version bump waits: status changes sync the row's
availability, closure meta, and freshness immediately, while it keeps
serving the previous release's data.

The tests bootstrap defines PLUGINS_TABLE_PREFIX for runners whose test
config lacks it; the stub tables themselves are created by the test
environment's after-start script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@obenland
obenland force-pushed the fix/update-source-cooldown-status-sync branch from 5cec7df to 8d32d6c Compare August 11, 2026 15:59
- Test row existence instead of version-string truthiness before syncing:
  a stored version of '0' (or '') read as "no row" and skipped the status
  sync — the exact gap the cooldown sync closes.
- Fetch the row's version and meta in one query and pass the meta into
  update_row_availability(), dropping its redundant second SELECT.
- Replace the hand-built plugin_information cache purge with
  Plugins_Info_API::flush_plugin_information_cache(): the hand-built keys
  used raw GP_Locales casing while the API stores lowercased locales,
  making the purge a no-op for every mixed-case locale.
- Extract the availability predicate into is_available() instead of
  duplicating it (with strictness drift) across both writers.
- Note in the cooldown comment that cron_trigger() keeps re-selecting the
  plugin until the cooldown expires; the re-pass is an idempotent no-op.
- Add the missing native return type to the test helper.

See WordPress#783.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PLUGINS_TABLE_PREFIX fallback define is dropped from the tests
bootstrap — wp-env writes the constant into wp-tests-config.php from
.wp-env.test.json, and nothing in this suite reads it — which reverts
the bootstrap to trunk. Also trims the test-runner exposition from the
class docblock and tightens the cooldown gate comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

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 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php:96

  • $existing_row can be null when there is no update_source row; using $existing_row->version ?? '' still attempts the property read and can emit a PHP warning. Use the nullsafe operator (or an explicit conditional) to avoid warnings in the common “no row yet” case.
		$existing_version = (string) ( $existing_row->version ?? '' );

wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-api-update-updater.php:125

  • During a release cooldown, cron_trigger() will keep selecting the plugin (version mismatch is expected), so this call path runs every hour. Calling update_row_availability() unconditionally means an UPDATE query is executed on every pass even when availability/close meta/last_updated haven’t changed. Consider short-circuiting before the UPDATE when the computed availability + close meta match the current row, to reduce steady-state DB write load during long cooldowns.
				}

				return true;

…held.

Stamping last_updated in the partial status sync consumed one of
cron_trigger()'s staleness signals. Recovery from a lost deferred cron
event then rested solely on the version clauses, which go blind when a
stored version matches the new one's 128-character truncation. Leaving
last_updated behind preserves the backup path; the repeated re-select is
an idempotent no-op.

Co-Authored-By: Claude Fable 5 <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