diff --git a/CHANGELOG.md b/CHANGELOG.md index 05cb6b3..e7b6bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.1.1] +### Fixed +- The **Enable auto-updates** toggle now appears for the plugin on the Plugins screen. The self-updater now reports the plugin to WordPress even when it is already current, so core marks it update-supported and shows the toggle; previously the toggle was hidden between releases. + ## [1.1.0] ### Added diff --git a/docs/features/plugin-self-update.md b/docs/features/plugin-self-update.md index fb76ab5..55bc1af 100644 --- a/docs/features/plugin-self-update.md +++ b/docs/features/plugin-self-update.md @@ -50,9 +50,16 @@ update for a same-slug plugin and makes core fire the 4. When newer, returns the release's first `.zip` asset as the update package. Core takes over from there: Plugins-screen notice, one-click update, and WP-Cron auto-updates if enabled. +5. When not newer — the site is current, or the lookup failed — returns a + `no_update` payload (installed version, empty package). This keeps the + plugin in core's `update_plugins` transient so core's `update-supported` + flag stays set and the **Enable auto-updates** toggle shows on the + Plugins screen. Without it, an off-directory plugin is absent from the + transient between releases and the toggle never appears. Any API failure, malformed response, or asset-less release degrades to -"no update available" — never an error surfaced to the site. +"no update available" (the `no_update` payload) — never an error surfaced +to the site, and never a lost auto-update toggle during a Gitea blip. ## Cutting a Release 1. Bump the version in `unsupervised-schedular.php` (both the `Version:` diff --git a/src/Update/UpdateChecker.php b/src/Update/UpdateChecker.php index 1d5ac80..dc3b672 100644 --- a/src/Update/UpdateChecker.php +++ b/src/Update/UpdateChecker.php @@ -75,9 +75,18 @@ class UpdateChecker { } /** - * `update_plugins_{hostname}` filter callback. Returns the incoming - * value untouched unless a newer release with a zip asset exists, in - * which case it returns the update array core expects. + * `update_plugins_{hostname}` filter callback. + * + * For a newer release with a zip asset, returns the update array core + * files under the transient's `response` list (the update offer). + * Otherwise — the plugin is current, or the release lookup failed — it + * returns a payload with the installed version and no package, which core + * files under `no_update`. That `no_update` entry is what sets core's + * `update-supported` flag and makes the "Enable auto-updates" toggle + * appear on the Plugins screen; without it, an off-directory plugin is + * absent from the transient between releases and the toggle never shows. + * + * The incoming value is only passed through untouched for other plugins. */ public function provideUpdate( mixed $update, mixed $plugin_data, mixed $plugin_file ): mixed { if ( plugin_basename( USC_PLUGIN_FILE ) !== $plugin_file ) { @@ -86,19 +95,24 @@ class UpdateChecker { $release = $this->latestRelease(); - if ( '' === $release['version'] || '' === $release['package'] ) { - return $update; - } - - if ( version_compare( $release['version'], USC_VERSION, '<=' ) ) { - return $update; + if ( '' !== $release['version'] && '' !== $release['package'] + && version_compare( $release['version'], USC_VERSION, '>' ) ) { + return [ + 'slug' => 'unsupervised-schedular', + 'version' => $release['version'], + 'url' => self::REPO_URL, + 'package' => $release['package'], + ]; } + // No newer release: answer with a `no_update` payload so core keeps + // the plugin in the update transient and shows the auto-update toggle. + // The empty package leaves core nothing to auto-install, as intended. return [ 'slug' => 'unsupervised-schedular', - 'version' => $release['version'], + 'version' => USC_VERSION, 'url' => self::REPO_URL, - 'package' => $release['package'], + 'package' => '', ]; } diff --git a/tests/Unit/Update/UpdateCheckerTest.php b/tests/Unit/Update/UpdateCheckerTest.php index 457b885..6f9092a 100644 --- a/tests/Unit/Update/UpdateCheckerTest.php +++ b/tests/Unit/Update/UpdateCheckerTest.php @@ -32,6 +32,22 @@ class UpdateCheckerTest extends TestCase return ['name' => $name, 'browser_download_url' => self::PACKAGE_URL]; } + /** + * The payload provideUpdate() returns when no newer release is offered. + * Core files this under the transient's `no_update` list, which is what + * makes the "Enable auto-updates" toggle appear. USC_VERSION is 1.0.0 in + * the test bootstrap. + */ + private function noUpdatePayload(): array + { + return [ + 'slug' => 'unsupervised-schedular', + 'version' => '1.0.0', + 'url' => UpdateChecker::REPO_URL, + 'package' => '', + ]; + } + public function testRegisterHooksHostnameFilter(): void { Filters\expectAdded('update_plugins_git.unsupervised.ca')->once(); @@ -109,7 +125,7 @@ class UpdateCheckerTest extends TestCase self::assertFalse($result); } - public function testNoUpdateWhenReleaseIsNotNewer(): void + public function testNoUpdatePayloadWhenReleaseIsNotNewer(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); Functions\when('get_transient')->justReturn(false); @@ -118,7 +134,9 @@ class UpdateCheckerTest extends TestCase $result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE); - self::assertFalse($result); + // Current version → core files this under `no_update` so the + // auto-update toggle stays visible; no package to install. + self::assertSame($this->noUpdatePayload(), $result); } public function testUsesCachedReleaseWithoutHittingApi(): void @@ -134,7 +152,7 @@ class UpdateCheckerTest extends TestCase self::assertSame('2.0.0', $result['version']); } - public function testApiFailureIsCachedAndReturnsUpdateUnchanged(): void + public function testApiFailureIsCachedAndStillReportsUpdateSupport(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); Functions\when('get_transient')->justReturn(false); @@ -150,10 +168,12 @@ class UpdateCheckerTest extends TestCase $result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE); - self::assertFalse($result); + // Even with the lookup failed we still return the `no_update` payload, + // so the auto-update toggle does not flicker away during a Gitea blip. + self::assertSame($this->noUpdatePayload(), $result); } - public function testNon200ResponseReturnsUpdateUnchanged(): void + public function testNon200ResponseReturnsNoUpdatePayload(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); Functions\when('get_transient')->justReturn(false); @@ -162,7 +182,7 @@ class UpdateCheckerTest extends TestCase $result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE); - self::assertFalse($result); + self::assertSame($this->noUpdatePayload(), $result); } public function testPicksFirstZipAssetAndSkipsOthers(): void @@ -181,7 +201,7 @@ class UpdateCheckerTest extends TestCase self::assertSame(self::PACKAGE_URL, $result['package']); } - public function testReleaseWithoutZipAssetOffersNoUpdate(): void + public function testReleaseWithoutZipAssetOffersNoUpdatePayload(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); Functions\when('get_transient')->justReturn(false); @@ -192,10 +212,12 @@ class UpdateCheckerTest extends TestCase $result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE); - self::assertFalse($result); + // No installable package means no update to offer, but we still keep + // the plugin in `no_update` so the toggle shows. + self::assertSame($this->noUpdatePayload(), $result); } - public function testMalformedApiBodyOffersNoUpdate(): void + public function testMalformedApiBodyOffersNoUpdatePayload(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); Functions\when('get_transient')->justReturn(false); @@ -207,6 +229,6 @@ class UpdateCheckerTest extends TestCase $result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE); - self::assertFalse($result); + self::assertSame($this->noUpdatePayload(), $result); } }