Deploys currently mean downloading a CI artifact zip and uploading it into wp-admin by hand. After this change, tagging vX.Y.Z in Gitea (or creating a release in the UI) is the deploy: the release appears in the sidebar with the built plugin zip attached, and every installed site picks the update up through WordPress's normal update mechanism — one click, or unattended with auto-updates enabled.
How it works
Site side
New Update URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler plugin header. Since WP 5.8 this blocks wordpress.org from ever serving a same-slug update and makes core fire the update_plugins_git.unsupervised.ca filter during update checks.
New Update\UpdateChecker (wired in Plugin::boot()) answers that filter: fetches the Gitea releases/latest API (which excludes drafts and pre-releases, so -rc builds are never offered), caches the lookup — including failures — in a transient for 6 hours, strips the v from the tag, and offers the release's .zip asset when version_compare says it's newer than USC_VERSION. Any API failure, malformed body, or asset-less release degrades silently to "no update". Core handles everything downstream (Plugins-screen notice, one-click update, WP-Cron auto-updates).
Transient cleaned up in uninstall.php.
Release side
New .gitea/workflows/release.yml fires on v* tag pushes, including tags created through Gitea's "New Release" UI (it reuses that release instead of erroring). It fails fast if the tag doesn't match the plugin Version: header — a mismatch would make sites see a phantom update forever or never see a real one — then runs the tests, builds the zip via composer build, and attaches it as a release asset. The asset matters: Gitea's auto-generated source archives have the wrong top-level folder and no vendor/, so WordPress couldn't install them. Hyphenated versions (1.2.3-rc.1) are flagged pre-release. Uses the runner's auto-injected GITHUB_TOKEN; no secret setup needed.
Release checklist (documented in docs/features/plugin-self-update.md)
Bump Version: header and USC_VERSION in unsupervised-schedular.php, merge to main.
Tag the merge commit vX.Y.Z.
Sites see the update within ~12 h (or immediately via Dashboard → Updates → Check Again).
Tests
tests/Unit/Update/UpdateCheckerTest.php — 9 tests covering newer/equal/older versions, v-prefix stripping, other-plugins passthrough, transient caching (including negative caching of API failures), non-200 responses, malformed JSON, zip-asset selection, and asset-less releases.
composer test — 361 tests pass
composer lint — PHPStan level 10 clean
composer cs — PHPCS clean
No local WordPress runtime exists in this repo (tests stub WP via Brain\Monkey), and the release workflow can only truly run on a real tag push. Suggested smoke test after merge: tag v1.0.0-rc.3, confirm the workflow attaches the zip and flags it pre-release, then install the current rc on a staging site and tag a full v1.0.0 to watch the update appear.
Closes #65
## What & why
Deploys currently mean downloading a CI artifact zip and uploading it into wp-admin by hand. After this change, tagging `vX.Y.Z` in Gitea (or creating a release in the UI) **is** the deploy: the release appears in the sidebar with the built plugin zip attached, and every installed site picks the update up through WordPress's normal update mechanism — one click, or unattended with auto-updates enabled.
## How it works
**Site side**
- New `Update URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler` plugin header. Since WP 5.8 this blocks wordpress.org from ever serving a same-slug update and makes core fire the `update_plugins_git.unsupervised.ca` filter during update checks.
- New `Update\UpdateChecker` (wired in `Plugin::boot()`) answers that filter: fetches the Gitea `releases/latest` API (which excludes drafts and pre-releases, so `-rc` builds are never offered), caches the lookup — including failures — in a transient for 6 hours, strips the `v` from the tag, and offers the release's `.zip` asset when `version_compare` says it's newer than `USC_VERSION`. Any API failure, malformed body, or asset-less release degrades silently to "no update". Core handles everything downstream (Plugins-screen notice, one-click update, WP-Cron auto-updates).
- Transient cleaned up in `uninstall.php`.
**Release side**
- New `.gitea/workflows/release.yml` fires on `v*` tag pushes, including tags created through Gitea's "New Release" UI (it reuses that release instead of erroring). It fails fast if the tag doesn't match the plugin `Version:` header — a mismatch would make sites see a phantom update forever or never see a real one — then runs the tests, builds the zip via `composer build`, and attaches it as a release asset. The asset matters: Gitea's auto-generated source archives have the wrong top-level folder and no `vendor/`, so WordPress couldn't install them. Hyphenated versions (`1.2.3-rc.1`) are flagged pre-release. Uses the runner's auto-injected `GITHUB_TOKEN`; no secret setup needed.
## Release checklist (documented in `docs/features/plugin-self-update.md`)
1. Bump `Version:` header and `USC_VERSION` in `unsupervised-schedular.php`, merge to `main`.
2. Tag the merge commit `vX.Y.Z`.
3. Sites see the update within ~12 h (or immediately via Dashboard → Updates → Check Again).
## Tests
`tests/Unit/Update/UpdateCheckerTest.php` — 9 tests covering newer/equal/older versions, `v`-prefix stripping, other-plugins passthrough, transient caching (including negative caching of API failures), non-200 responses, malformed JSON, zip-asset selection, and asset-less releases.
- `composer test` — 361 tests pass
- `composer lint` — PHPStan level 10 clean
- `composer cs` — PHPCS clean
> No local WordPress runtime exists in this repo (tests stub WP via Brain\Monkey), and the release workflow can only truly run on a real tag push. Suggested smoke test after merge: tag `v1.0.0-rc.3`, confirm the workflow attaches the zip and flags it pre-release, then install the current rc on a staging site and tag a full `v1.0.0` to watch the update appear.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes#65
Declare an Update URI header and answer core's update_plugins_{hostname}
filter from a new Update\UpdateChecker that offers the latest published
Gitea release's zip asset when it is newer than the installed version,
with transient caching and silent degradation on API failures.
Add a release workflow that fires on v* tag pushes: verifies the tag
matches the plugin Version header, runs the tests, builds the plugin zip,
and attaches it to the release (reusing a UI-created release, flagging
hyphenated versions as pre-release so /releases/latest skips them).
Co-Authored-By: Claude Fable 5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #65
What & why
Deploys currently mean downloading a CI artifact zip and uploading it into wp-admin by hand. After this change, tagging
vX.Y.Zin Gitea (or creating a release in the UI) is the deploy: the release appears in the sidebar with the built plugin zip attached, and every installed site picks the update up through WordPress's normal update mechanism — one click, or unattended with auto-updates enabled.How it works
Site side
Update URI: https://git.unsupervised.ca/Unsupervised/unsupervised-schedulerplugin header. Since WP 5.8 this blocks wordpress.org from ever serving a same-slug update and makes core fire theupdate_plugins_git.unsupervised.cafilter during update checks.Update\UpdateChecker(wired inPlugin::boot()) answers that filter: fetches the Giteareleases/latestAPI (which excludes drafts and pre-releases, so-rcbuilds are never offered), caches the lookup — including failures — in a transient for 6 hours, strips thevfrom the tag, and offers the release's.zipasset whenversion_comparesays it's newer thanUSC_VERSION. Any API failure, malformed body, or asset-less release degrades silently to "no update". Core handles everything downstream (Plugins-screen notice, one-click update, WP-Cron auto-updates).uninstall.php.Release side
.gitea/workflows/release.ymlfires onv*tag pushes, including tags created through Gitea's "New Release" UI (it reuses that release instead of erroring). It fails fast if the tag doesn't match the pluginVersion:header — a mismatch would make sites see a phantom update forever or never see a real one — then runs the tests, builds the zip viacomposer build, and attaches it as a release asset. The asset matters: Gitea's auto-generated source archives have the wrong top-level folder and novendor/, so WordPress couldn't install them. Hyphenated versions (1.2.3-rc.1) are flagged pre-release. Uses the runner's auto-injectedGITHUB_TOKEN; no secret setup needed.Release checklist (documented in
docs/features/plugin-self-update.md)Version:header andUSC_VERSIONinunsupervised-schedular.php, merge tomain.vX.Y.Z.Tests
tests/Unit/Update/UpdateCheckerTest.php— 9 tests covering newer/equal/older versions,v-prefix stripping, other-plugins passthrough, transient caching (including negative caching of API failures), non-200 responses, malformed JSON, zip-asset selection, and asset-less releases.composer test— 361 tests passcomposer lint— PHPStan level 10 cleancomposer cs— PHPCS clean🤖 Generated with Claude Code