WordPress / WordPress/wordpress.org
Plugins: Unify the release cooldown (#650) and the Release CPT (#407) into one congruent publishing process
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 163
- Forks
- 221
- PR merge metrics
- No merged PRs in 30d
Description
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:
- #650 — release cooldown. Inserts a delay between an SVN commit and the new version being served, by deferring the write into the
update_sourcetable (and a cron + force-release path). Mitigates supply-chain risk by giving scanners and humans a window before a release reaches sites. - #407 — Release page / Release CPT. Introduces a private
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 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.php download_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.
Benefits
- Gates every surface, not just auto-update. Download, update API, info API, and the plugin page all hold on the previous version until promotion — closing the gap #650 can't.
- Net code removal. The deferred-
update_sourcemachinery, dynamic per-slug cron,release_timere-anchoring, and the force-release-rewrites-the-row path in #650 collapse into "promote a pending release." - Scanners gate releases first-class. Gandalf/PCP/Plugin Check run in Phase A and write verdicts onto the
pendingrelease 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
releasespostmeta. - Trivial, recorded rollbacks. Revert re-promotes a prior snapshot; no rebuild, fully audited.
- No closed-infra changes.
update_sourceis still filled by our own code; the api.wordpress.org update-check endpoint and the download infra are untouched — the same property #650 was protecting.
Risks / costs
- Touches the hottest path in the directory. The importer is load-bearing; redirecting its writes and adding a promote step is higher-stakes than #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.
- #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_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_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.
References
- Cooldown PR: https://github.com/WordPress/wordpress.org/pull/650
- Release page / Release CPT PR: https://github.com/WordPress/wordpress.org/pull/407
- Themes cooldown counterpart: https://github.com/WordPress/wordpress.org/pull/651
- Related meta-trac: https://meta.trac.wordpress.org/ticket/7783
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)
- #7783 — Create a Releases page for every plugin — This is PR #407, the foundation this plan builds on. The release CPT + Releases tab are exactly what the ticket asks for; resolved by definition.
- #5484 — Add a "Tag and release" button — Asks for one-click "tag from
/trunkto/tags/x.y, bumpStable Tag, validate version > latest." That is precisely #407'spublish_release()+ theplugins/v2/.../publishroute, 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
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. - #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 #407's release-checks UI, with the cooldown providing a real window to act on findings before promotion. - #5637 — Email committer when a security scanner triggers a change — Phase-A scanners writing verdicts onto a
pendingrelease during the cooldown is the natural trigger for this notification. - #8081 — Plugin Check "Verified" badge on plugin pages — A per-release
plugin_check_resultmakes the badge derivable from the published release's stored verdict.
Phased rollouts — partial overlap
- #8009 — Phased releases and roll-outs — Its MVP is the
manual-updates-24hrstrategy that PR #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
- #6380 — Prevent use of 'trunk' as a Stable Version — the draft(trunk) vs published(tagged) distinction plus an explicit promote gate is a clean enforcement point.
- #2538 — Preview Readme — the trunk
draftrelease snapshot is a natural "preview before publish" surface (#407 includes a release-draft block). - #7696 — Release confirmation: releases made when RC is disabled don't show up upon enabling & #7952 — Incorrect committer attribution with Release Confirmation — every release being a discrete record with its own committer/revision makes both largely fall out, removing reliance on the lossy
releasespostmeta. - #7745 — Admin notice about a pending release confirmation — a
pendingrelease status is directly surfaceable in the edit screen.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading cli/class-import.php:404 and :426-578, then compare the cooldown work in #650 with the Release CPT and API work in #407. Map the importer’s canonical-post writes to the proposed release object and promotion flow; done requires an agreed implementation path that preserves download, update, info, and rendered-page behavior while addressing the listed migration and rollout decisions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php, wordpress
- Domain
- api, backend, database
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100