You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unify the release cooldown (#650) and the Release CPT (#407) into one congruent publishing process
Summary
We currently have two in-flight efforts that point at the same underlying redesign from different angles:
Plugins: Add a release delay between SVN commit of update, and publish #650 — release cooldown. Inserts a delay between an SVN commit and the new version being served, by deferring the write into the update_source table (and a cron + force-release path). Mitigates supply-chain risk by giving scanners and humans a window before a release reaches sites.
Add a release page #407 — Release page / Release CPT. Introduces a private plugin_release CPT (child of the plugin post), an internal API (add_release/update_release/update_releases/get_release/publish_release/maybe_backfill_releases), a plugins/v2/.../publish REST route, the plugin_manage_releases cap, and a "Releases" tab UI behind the show_release_beta flag.
This issue proposes treating them as one process: import into a release object first, and make "go live" an explicit promotion of that object onto the canonical plugin post. The cooldown then becomes "how long a release sits in pending before it auto-promotes," and #407's CPT becomes the durable record that replaces the releases postmeta.
Why merge them
The cooldown's hard problem is that the canonical plugin post is the single source for download (class-template.phpdownload_link() reads stable_tag), the update API (class-api-update-updater.php reads version/stable_tag), the info API, and the rendered page — and the importer overwrites all of it in place (cli/class-import.php:404 + :426-578). #650 gates only the update_source write, so a fresh download and the plugin page still jump to the new, unvetted version immediately.
If instead the importer writes to a release object and a separate promote step copies the snapshot onto the canonical post, then gating promotion gates all four surfaces at once — download included — with no split-brain on the page. That is precisely the object #407 already models. The two PRs are the same redesign seen from two ends.
Proposed process (target state)
Phase A — Import → Release object. A commit parses SVN, builds ZIPs, and runs checks/scanners, writing the result onto a plugin_release CPT (draft for trunk, pending for a tagged release in cooldown). The canonical post is untouched; nothing user-visible changes. ZIPs are version-specific ({slug}.{version}.zip) and simply sit on disk, unlinked, until promotion.
Phase B — Promote. When the cooldown elapses (cron) or a reviewer force-releases, Plugin_Release::promote() applies the release's snapshot onto the canonical plugin post (the moved :368-578 logic), refreshes update_source, flips statuses (new → published, previous → superseded), re-fires wporg_plugins_imported (so i18n/Gandalf/PCP chain), and sends the "now live" email.
Status machine:draft → pending → published → superseded, with rejected/reverted branches. Exactly one published release per plugin = the current canonical state; revert = re-promote the previous published snapshot (instant, no rebuild).
The release CPT stores three buckets of metadata per version, kept forever: provenance (version, tag, committer, revision range, commit log — mostly already in #407), the canonical snapshot (sections, headers, requires/tested, assets, blocks, taxonomy sets — the fields the importer writes today), and lifecycle/audit (status, cooldown_until, captured release_delay, plugin_check_result, scan verdicts, promoted-by/at, force-release reason).
Compatibility is via a shim: reimplement Plugin_Directory::get_release(s)/add_release to read/write the CPT while returning the same array shape, with dual-read + lazy backfill (maybe_backfill_releases()), all behind the show_release_beta flag so import behaves exactly as today when off.
Scanners gate releases first-class. Gandalf/PCP/Plugin Check run in Phase A and write verdicts onto the pending release before it can promote.
Durable per-version history. Each release keeps its own checks, commit log, requires-matrix, and the exact readme/assets snapshot as shipped — queryable, instead of a single overwritten blob. Replaces the releases postmeta.
Trivial, recorded rollbacks. Revert re-promotes a prior snapshot; no rebuild, fully audited.
Touches the hottest path in the directory. The importer is load-bearing; redirecting its writes and adding a promote step is higher-stakes than Plugins: Add a release delay between SVN commit of update, and publish #650's localized change. Mitigated by the feature flag + dual-read and a staged rollout (delay 0 first).
Snapshot fidelity. Promotion must reproduce exactly what the in-place importer wrote (:368-578), including taxonomy/term sets and asset/blocks bookkeeping. Missing a field = subtle drift between committed and served state. Needs careful field-mapping + tests.
Migration surface. Backfilling CPTs for every existing plugin, and the dual-read window, need care; historical releases won't have snapshots (acceptable — they render from canonical as today).
Two writers during transition. While the flag rolls out, some plugins use the new path and some the old; the shim must keep both correct.
Add a release page #407 is still draft-quality. It's behind a flag with TODOs (caps/checks/warnings in publish_release(), a noted release-page race condition) and a large UI surface; depending on its finish blocks this work (see paths below).
Open product calls: custom post statuses vs. release_status meta; per-slug cron vs. a sweep over pending; snapshot-at-import vs. re-export-at-promote; whether trunk-stable releases get a cooldown.
Two paths forward
Path A — Finish #407 first, then build the process on top
Land #407 as-is (CPT + internal API + Releases tab UI behind show_release_beta), then add the status machine, promote(), importer redirection, and cooldown in follow-ups. Pros: one coherent feature; the UI to observe/drive releases lands with the backend. Cons: couples this security-relevant work to finishing a large, still-draft UI PR (race condition, many blocks, TODOs); slower to the supply-chain benefit; bigger surface to review at once.
Path B — Extract the non-UI core of #407 now; leave the UI as a second stab
Pull just the backend primitives out of #407 — the plugin_release CPT, the Plugin_Release internal API, backfill, the publish route + cap — and land them minimal and headless. Build the import-redirect + promote() + cooldown directly on that, replacing #650. The Releases-tab UI (blocks, release page, the race condition) becomes a separate follow-up PR on top. Pros: decouples the security win from UI polish; smaller, safer reviews; the cooldown ships sooner; the UI can iterate without gating the backend. Cons: two PRs to coordinate; the headless period has no admin UI to observe releases (CLI/WP-CLI only); some rework of #407's commit history to split UI from core.
Recommendation: Path B. The cooldown is the time-sensitive, security-relevant piece; the CPT + promote backend is the minimal thing it needs; the UI is valuable but shouldn't gate it. Suggested phasing: (1) extract CPT + API headless behind the flag; (2) shim get_release(s) → CPT with dual-read + backfill (no behavior change); (3) add status machine + promote() with immediate auto-promote (delay 0, still no visible change); (4) redirect the importer into Phase A; (5) turn on the cooldown + force-release and retire #650; (6) land the Releases UI as its own PR.
Related meta-trac tickets this would resolve or advance
This redesign isn't only about the cooldown — modelling releases as first-class objects with an explicit promotion step unblocks or advances a cluster of long-standing Plugin Directory tickets. Grouped by how directly the plan addresses them.
Directly the same work (the redesign is the implementation)
#5484 — Add a "Tag and release" button — Asks for one-click "tag from /trunk to /tags/x.y, bump Stable Tag, validate version > latest." That is precisely Add a release page #407's publish_release() + the plugins/v2/.../publish route, which become the promotion entry point here. Fully addressed.
Architecturally unblocked (the plan removes the core blocker)
#3236 — Allow for beta channels for plugins and themes — The blocker is stated in-ticket: "99% of the update functionality is built around the idea that there'll only ever be a single 'current release.'" The release CPT + status machine decouple "a release" from "the current release" (many release records, one published pointer), which is the prerequisite a beta track needs. Scope: this delivers the data model and removes the architectural blocker; serving a beta track to opt-in users via the update API remains a further step. Strong enabler, not a complete fix on its own.
#5903 — Warn when a plugin tag is made, but never released — Wants an alert "after X hours of the tag being created, but the release not being made." "Tagged but not promoted" is a first-class, timestamped pending/unpromoted release status here — directly queryable for that warning. The companion "you forgot to bump the version header" check fits the Phase-A check set. Cleanly resolved.
Strongly advanced by the per-release record + cooldown window
#6108 — Automated guideline checks and constant feedback to authors — Already part-implemented ("Run Plugin Check over new plugin releases"). The plan makes it first-class: plugin_check_result/scan verdicts stored per release, surfaced by Add a release page #407's release-checks UI, with the cooldown providing a real window to act on findings before promotion.
#8009 — Phased releases and roll-outs — Its MVP is the manual-updates-24hr strategy that PR Plugins: Add a release delay between SVN commit of update, and publish #650 already builds on. This plan fills two named gaps: rollback (the ticket notes "No Rollback: authors must release a new version or change the stable tag to revert" — the model gives instant, recorded revert by re-promoting the prior published snapshot) and a home for per-release rollout state/tooling. It does not itself add percentage-based serving.
Unify the release cooldown (#650) and the Release CPT (#407) into one congruent publishing process
Summary
We currently have two in-flight efforts that point at the same underlying redesign from different angles:
update_sourcetable (and a cron + force-release path). Mitigates supply-chain risk by giving scanners and humans a window before a release reaches sites.plugin_releaseCPT (child of the plugin post), an internal API (add_release/update_release/update_releases/get_release/publish_release/maybe_backfill_releases), aplugins/v2/.../publishREST route, theplugin_manage_releasescap, and a "Releases" tab UI behind theshow_release_betaflag.This issue proposes treating them as one process: import into a release object first, and make "go live" an explicit promotion of that object onto the canonical plugin post. The cooldown then becomes "how long a release sits in
pendingbefore it auto-promotes," and #407's CPT becomes the durable record that replaces thereleasespostmeta.Why merge them
The cooldown's hard problem is that the canonical plugin post is the single source for download (
class-template.phpdownload_link()readsstable_tag), the update API (class-api-update-updater.phpreadsversion/stable_tag), the info API, and the rendered page — and the importer overwrites all of it in place (cli/class-import.php:404+:426-578). #650 gates only theupdate_sourcewrite, so a fresh download and the plugin page still jump to the new, unvetted version immediately.If instead the importer writes to a release object and a separate promote step copies the snapshot onto the canonical post, then gating promotion gates all four surfaces at once — download included — with no split-brain on the page. That is precisely the object #407 already models. The two PRs are the same redesign seen from two ends.
Proposed process (target state)
Phase A — Import → Release object. A commit parses SVN, builds ZIPs, and runs checks/scanners, writing the result onto a
plugin_releaseCPT (draftfor trunk,pendingfor a tagged release in cooldown). The canonical post is untouched; nothing user-visible changes. ZIPs are version-specific ({slug}.{version}.zip) and simply sit on disk, unlinked, until promotion.Phase B — Promote. When the cooldown elapses (cron) or a reviewer force-releases,
Plugin_Release::promote()applies the release's snapshot onto the canonical plugin post (the moved:368-578logic), refreshesupdate_source, flips statuses (new →published, previous →superseded), re-fireswporg_plugins_imported(so i18n/Gandalf/PCP chain), and sends the "now live" email.Status machine:
draft → pending → published → superseded, withrejected/revertedbranches. Exactly onepublishedrelease per plugin = the current canonical state; revert = re-promote the previouspublishedsnapshot (instant, no rebuild).The release CPT stores three buckets of metadata per version, kept forever: provenance (version, tag, committer, revision range, commit log — mostly already in #407), the canonical snapshot (sections, headers, requires/tested, assets, blocks, taxonomy sets — the fields the importer writes today), and lifecycle/audit (status,
cooldown_until, capturedrelease_delay,plugin_check_result, scan verdicts, promoted-by/at, force-release reason).Compatibility is via a shim: reimplement
Plugin_Directory::get_release(s)/add_releaseto read/write the CPT while returning the same array shape, with dual-read + lazy backfill (maybe_backfill_releases()), all behind theshow_release_betaflag so import behaves exactly as today when off.Benefits
update_sourcemachinery, dynamic per-slug cron,release_timere-anchoring, and the force-release-rewrites-the-row path in Plugins: Add a release delay between SVN commit of update, and publish #650 collapse into "promote a pending release."pendingrelease before it can promote.releasespostmeta.update_sourceis still filled by our own code; the api.wordpress.org update-check endpoint and the download infra are untouched — the same property Plugins: Add a release delay between SVN commit of update, and publish #650 was protecting.Risks / costs
:368-578), including taxonomy/term sets and asset/blocks bookkeeping. Missing a field = subtle drift between committed and served state. Needs careful field-mapping + tests.publish_release(), a noted release-page race condition) and a large UI surface; depending on its finish blocks this work (see paths below).release_statusmeta; per-slug cron vs. a sweep overpending; snapshot-at-import vs. re-export-at-promote; whether trunk-stable releases get a cooldown.Two paths forward
Path A — Finish #407 first, then build the process on top
Land #407 as-is (CPT + internal API + Releases tab UI behind
show_release_beta), then add the status machine,promote(), importer redirection, and cooldown in follow-ups. Pros: one coherent feature; the UI to observe/drive releases lands with the backend. Cons: couples this security-relevant work to finishing a large, still-draft UI PR (race condition, many blocks, TODOs); slower to the supply-chain benefit; bigger surface to review at once.Path B — Extract the non-UI core of #407 now; leave the UI as a second stab
Pull just the backend primitives out of #407 — the
plugin_releaseCPT, thePlugin_Releaseinternal API, backfill, the publish route + cap — and land them minimal and headless. Build the import-redirect +promote()+ cooldown directly on that, replacing #650. The Releases-tab UI (blocks, release page, the race condition) becomes a separate follow-up PR on top. Pros: decouples the security win from UI polish; smaller, safer reviews; the cooldown ships sooner; the UI can iterate without gating the backend. Cons: two PRs to coordinate; the headless period has no admin UI to observe releases (CLI/WP-CLI only); some rework of #407's commit history to split UI from core.Recommendation: Path B. The cooldown is the time-sensitive, security-relevant piece; the CPT + promote backend is the minimal thing it needs; the UI is valuable but shouldn't gate it. Suggested phasing: (1) extract CPT + API headless behind the flag; (2) shim
get_release(s)→ CPT with dual-read + backfill (no behavior change); (3) add status machine +promote()with immediate auto-promote (delay 0, still no visible change); (4) redirect the importer into Phase A; (5) turn on the cooldown + force-release and retire #650; (6) land the Releases UI as its own PR.References
Related meta-trac tickets this would resolve or advance
This redesign isn't only about the cooldown — modelling releases as first-class objects with an explicit promotion step unblocks or advances a cluster of long-standing Plugin Directory tickets. Grouped by how directly the plan addresses them.
Directly the same work (the redesign is the implementation)
/trunkto/tags/x.y, bumpStable Tag, validate version > latest." That is precisely Add a release page #407'spublish_release()+ theplugins/v2/.../publishroute, which become the promotion entry point here. Fully addressed.Architecturally unblocked (the plan removes the core blocker)
publishedpointer), which is the prerequisite a beta track needs. Scope: this delivers the data model and removes the architectural blocker; serving a beta track to opt-in users via the update API remains a further step. Strong enabler, not a complete fix on its own.pending/unpromoted release status here — directly queryable for that warning. The companion "you forgot to bump the version header" check fits the Phase-A check set. Cleanly resolved.Strongly advanced by the per-release record + cooldown window
plugin_check_result/scan verdicts stored per release, surfaced by Add a release page #407's release-checks UI, with the cooldown providing a real window to act on findings before promotion.pendingrelease during the cooldown is the natural trigger for this notification.plugin_check_resultmakes the badge derivable from the published release's stored verdict.Phased rollouts — partial overlap
manual-updates-24hrstrategy that PR Plugins: Add a release delay between SVN commit of update, and publish #650 already builds on. This plan fills two named gaps: rollback (the ticket notes "No Rollback: authors must release a new version or change the stable tag to revert" — the model gives instant, recorded revert by re-promoting the priorpublishedsnapshot) and a home for per-release rollout state/tooling. It does not itself add percentage-based serving.Secondary / partial
draftrelease snapshot is a natural "preview before publish" surface (Add a release page #407 includes a release-draft block).releasespostmeta.pendingrelease status is directly surfaceable in the edit screen.