Deliver Private WordPress Plugin Updates Reliably

A private WordPress plugin updater needs to do more than find the newest GitHub tag. It must select an installable package, compare versions correctly, authenticate the download, and explain failures to the site administrator. When adding private GitHub updates to a plugin, I separated those rules from the admin screen so they could be tested independently. I also checked the final ZIP through WordPress’s update path. That catches issues a release page cannot show, such as the wrong top-level folder, missing build files, or credentials that work for metadata but fail when downloading the package.
Translate GitHub releases into WordPress update data
WordPress asks plugins for update metadata through a specific set of hooks and data shapes. GitHub represents releases, tags, prereleases, assets, and authorization differently. I built a service that translates between them instead of spreading API logic across the dashboard controller and view.
The separate service made the rules testable: which releases count, how a version is normalized, which asset is installable, and what the plugin shows when no valid release exists.
Validate the built plugin ZIP
A valid-looking version tag can point to source archives that do not contain the production plugin layout. A release can be a draft, a prerelease, missing its built ZIP, or carrying an asset with the wrong top-level directory. Selecting “the latest tag” is therefore not enough.
I made the built ZIP a required part of the release. The updater looks for the expected asset and rejects ambiguous candidates. The release workflow creates and verifies that artifact before publication, so update discovery and packaging share one definition of ready.
Keep private credentials out of browser responses
The GitHub request needs credentials, but the browser and ordinary administrators do not need to see them. Token storage, API calls, and asset downloads stay server-side. The dashboard exposes status and safe diagnostics rather than echoing request headers or raw API responses.
I also treated authentication failure differently from “no update.” If those states collapse, a site can remain outdated indefinitely while appearing healthy.
Offer a manual update check
WordPress sensibly caches update checks, and remote APIs impose limits. During a release, that can make a correct new version appear absent. I added a manual check that invalidates the relevant cache and reports the result without teaching administrators to delete transients or reinstall the plugin.
The button is not only a convenience. It is an operational diagnostic that exercises the same service the scheduled check uses.
Show the reason an update is unavailable
- Current and no newer stable release.
- New version available with a valid package.
- Credentials missing or rejected.
- Release found but expected asset missing.
- Remote service unavailable or rate-limited.
- Downloaded package failed validation.
Each state identifies the problem and who can address it. “Update failed” would push every case back to a developer.
Rollback deserved equal status with upgrade. WordPress can restore the previous plugin files, but a release that changes stored options or schedules migrations may not be reversible by files alone. I keep schema changes backward compatible where practical and document when a downgrade requires a backup. The updater metadata can describe requirements, but it cannot make an unsafe downgrade safe.
I also validate version ordering with prerelease-like tags and a plugin header read from the installed code. String comparison is not enough: version values must agree across the repository tag, built artifact, WordPress metadata, and runtime constant. A mismatch can create an endless update prompt or cause a real release to look older. The release gate checks those values before an administrator ever sees the package.
Finally, private distribution introduces continuity risk. If one personal token or account is the only way to retrieve releases, the organization does not own its update channel. Service credentials, documented scopes, multiple maintainers, and a recovery plan are part of making the feature sustainable.
Network failure testing rounded out the feature. I simulated timeouts, malformed metadata, expired credentials, missing assets, and interrupted downloads. In every case the installed plugin remained intact and WordPress received a concise error without sensitive details. An updater earns trust as much by refusing a bad package safely as by installing a good one.
Update notices should also include compatibility requirements before the download starts. If a release needs a newer PHP or WordPress version, the site should learn that from verified metadata and remain on the current working build. Discovering the requirement after replacement is far more expensive.
Verify the release through installation
Before announcing a private release, verify the plugin version, attached ZIP, directory structure, requirements, and checksum. Install the package through the normal WordPress update path on a test site. Exercise missing credentials, timeouts, invalid metadata, and failed downloads, then confirm the installed plugin remains usable. Document whether rollback needs only the previous files or a database backup too. The administrator should be able to tell whether the site is current or whether an update check failed.
Photo by Andrey Matveev on Pexels.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.



