diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml
index c89a69c..f012b5a 100644
--- a/.github/workflows/archive.yml
+++ b/.github/workflows/archive.yml
@@ -32,7 +32,9 @@ jobs:
/phpcs.xml /phpcs.xml.dist /phpstan.neon /phpstan.neon.dist
/phpunit*.xml* /.php-cs-fixer* /.gitignore /.gitattributes
/composer.review*.json /composer.review*.lock
- main_file: nginx-cache.php
+ package_name: ns-nginx-cache
+ package_folder: ns-nginx-cache
+ main_file: ns-nginx-cache.php
package_version: ${{ github.event.inputs.version }}
secrets:
COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }}
@@ -42,5 +44,6 @@ jobs:
uses: sympress/workflows/.github/workflows/wordpress-archive-check.yml@78d083a9a0176cf0252318b0c69fc64938a727d6
with:
artifact_name: ${{ needs.archive.outputs.artifact }}
+ package_folder: ns-nginx-cache
secrets:
COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee0bb14..44d97fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
where applicable.
+## Unreleased
+
+### Fixed
+
+- Add a GitHub `Update URI` header so WordPress.org cannot offer updates for an unrelated plugin with a similar name, including while this plugin is inactive.
+
+### Changed
+
+- Use `ns-nginx-cache/ns-nginx-cache.php` as the WordPress plugin basename and **NS Nginx Cache** as its display name. Composer installation metadata, release archive names and folders, plugin settings links, asset URLs and version lookups follow the new entry. Existing installations require the [documented activation and directory migration](docs/migration.md#upgrading-the-sympress-plugin-slug).
+
## 1.0.3 — 2026-10-16
### Fixed
diff --git a/README.md b/README.md
index cbc72f2..a6d4694 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# SymPress Nginx Cache
+# NS Nginx Cache
[](composer.json) [](composer.json)
@@ -23,14 +23,26 @@ The package is discoverable by `sympress/kernel` through Composer metadata:
```json
{
"extra": {
+ "installer-name": "ns-nginx-cache",
"kernel": {
"bundle": "SymPress\\NginxCache\\NginxCacheBundle",
- "entry": "nginx-cache/nginx-cache.php"
+ "entry": "ns-nginx-cache/ns-nginx-cache.php"
}
}
}
```
+The WordPress plugin slug is `ns-nginx-cache`; release archives use the same
+folder and `ns-nginx-cache.php` entry file. The Composer package remains
+`sympress/nginx-cache`. Its `installer-name` sets the folder when using
+[composer/installers](https://github.com/composer/installers#custom-install-names).
+The [Update URI header](https://make.wordpress.org/core/2021/06/29/introducing-update-uri-plugin-header-in-wordpress-5-8/)
+identifies this GitHub-distributed plugin and prevents WordPress.org from
+offering updates for an unrelated plugin, including while this plugin is inactive.
+This header does not install a GitHub updater; updates still use Composer or
+the release archive. See [upgrading an existing installation](docs/migration.md#upgrading-the-sympress-plugin-slug)
+before switching from the old `nginx-cache` folder.
+
## WP-CLI
```bash
diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml
index 00fd48e..cfe4ca7 100644
--- a/Resources/config/services.yaml
+++ b/Resources/config/services.yaml
@@ -122,7 +122,7 @@ services:
- { name: kernel.hook, hook: 'admin_post_sympress_nginx_cache_purge', method: handlePurgeAction }
- { name: kernel.hook, hook: 'admin_post_sympress_nginx_cache_flush_queue', method: handleQueueAction }
- { name: kernel.hook, hook: 'admin_bar_menu', method: addAdminBarNode, accepted_args: 1, priority: 100 }
- - { name: kernel.hook, hook: 'plugin_action_links_nginx-cache/nginx-cache.php', method: addPluginActionLinks, type: filter, accepted_args: 1 }
+ - { name: kernel.hook, hook: 'plugin_action_links_ns-nginx-cache/ns-nginx-cache.php', method: addPluginActionLinks, type: filter, accepted_args: 1 }
SymPress\NginxCache\Admin\NetworkAdminPage:
tags:
diff --git a/composer.json b/composer.json
index a85354a..59021cb 100644
--- a/composer.json
+++ b/composer.json
@@ -96,10 +96,11 @@
}
},
"extra": {
+ "installer-name": "ns-nginx-cache",
"wordpress-install-dir": "vendor/wordpress/wordpress",
"kernel": {
"bundle": "SymPress\\NginxCache\\NginxCacheBundle",
- "entry": "nginx-cache/nginx-cache.php"
+ "entry": "ns-nginx-cache/ns-nginx-cache.php"
}
},
"minimum-stability": "stable",
diff --git a/docs/migration.md b/docs/migration.md
index 23cd334..16615ae 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -1,5 +1,38 @@
# Migrating cache plugins
+## Upgrading the SymPress plugin slug
+
+The SymPress plugin now appears as **NS Nginx Cache** with the plugin basename
+`ns-nginx-cache/ns-nginx-cache.php`. The Composer package remains
+`sympress/nginx-cache`; settings, stored credentials, queues, tag-index tables,
+cron hooks, REST routes, translation domain and `wp nginx-cache` commands retain
+their existing identifiers.
+
+1. Deactivate the old **SymPress** installation before changing its directory.
+ Verify its author and source; the unrelated WordPress.org plugin may also
+ appear as Nginx Cache.
+2. For Composer installations, update `sympress/nginx-cache`. Remove any explicit
+ root-project installer-path override that forces this package into the old
+ `nginx-cache` folder; the package's `installer-name` is `ns-nginx-cache`.
+ For archive installations, install the new release under
+ `wp-content/plugins/ns-nginx-cache/` and retain the deactivated old files
+ outside the plugins directory as a backup. Do not use WordPress's Delete
+ action for this migration: an enabled delete-on-uninstall setting can remove
+ the stored configuration and queues.
+3. Activate **NS Nginx Cache** again, using the previous per-site or network
+ activation scope. WordPress stores activation by plugin basename, so the old
+ activation does not follow the renamed entry file. Update any deployment
+ allowlist or explicit kernel entry to `ns-nginx-cache/ns-nginx-cache.php`.
+4. Open Dashboard → Updates and choose **Check again** to refresh a previously
+ cached update notice. Verify the plugin settings and Site Health version.
+
+The main file includes the GitHub `Update URI`, which tells WordPress.org to
+exclude this plugin from its update responses even while it is inactive.
+Installations still running an older release without that header need the new
+files before this protection applies. No GitHub update client is added.
+
+## Importing settings from another cache plugin
+
Preview Nginx Helper settings before importing:
```sh
diff --git a/docs/side-effects.md b/docs/side-effects.md
index 3aa284c..63efe30 100644
--- a/docs/side-effects.md
+++ b/docs/side-effects.md
@@ -17,6 +17,10 @@ request to local deletion and follow-up work.
| Anonymous cacheable response | `SurrogateTagSubscriber` → `TagIndexRepository` | Registers canonical URL/tag mappings; fresh identical mappings issue no writes | Schedules tag maintenance |
| Tag-maintenance cron / side-effect tick | `TagIndexMaintenance` | Deletes at most 500 index rows per call under the index mutation lock, enforcing TTL and retention limits | Schedules another maintenance event; no filesystem deletion or HTTP |
+The plugin-list settings link is registered through
+`plugin_action_links_ns-nginx-cache/ns-nginx-cache.php`, matching the Composer
+and archive entry. It only links to the settings page and triggers no purge.
+
## Tag retention, setup and translated requests
Tag retention no longer prunes on each insertion. Maintenance commits the
diff --git a/nginx-cache.php b/ns-nginx-cache.php
similarity index 77%
rename from nginx-cache.php
rename to ns-nginx-cache.php
index 2e942b3..eecc280 100644
--- a/nginx-cache.php
+++ b/ns-nginx-cache.php
@@ -3,7 +3,9 @@
declare(strict_types=1);
/**
- * Plugin Name: Nginx Cache
+ * Plugin Name: NS Nginx Cache
+ * Plugin URI: https://github.com/SymPress/nginx-cache
+ * Update URI: https://github.com/SymPress/nginx-cache
* Description: Kernel-integrated purge controls for Nginx FastCGI, proxy and uWSGI cache zones.
* Version: 1.0.3
* Requires at least: 6.9
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index cc3e446..3610367 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -13,7 +13,7 @@
- nginx-cache.php
+ ns-nginx-cache.php
uninstall.php
src
diff --git a/src/Admin/CacheMetricsPanel.php b/src/Admin/CacheMetricsPanel.php
index fd9f883..40900e2 100644
--- a/src/Admin/CacheMetricsPanel.php
+++ b/src/Admin/CacheMetricsPanel.php
@@ -10,10 +10,10 @@
{
public function enqueueStyles(): void
{
- wp_enqueue_style('sympress-nginx-cache-simple', plugins_url('Resources/assets/cache-simple.css', __DIR__ . '/../../nginx-cache.php'), [], (string) filemtime(__DIR__ . '/../../Resources/assets/cache-simple.css'));
+ wp_enqueue_style('sympress-nginx-cache-simple', plugins_url('Resources/assets/cache-simple.css', dirname(__DIR__, 2) . '/ns-nginx-cache.php'), [], (string) filemtime(__DIR__ . '/../../Resources/assets/cache-simple.css'));
wp_enqueue_style(
'sympress-nginx-cache-metrics',
- plugins_url('Resources/assets/cache-metrics.css', __DIR__ . '/../../nginx-cache.php'),
+ plugins_url('Resources/assets/cache-metrics.css', dirname(__DIR__, 2) . '/ns-nginx-cache.php'),
[],
(string) filemtime(__DIR__ . '/../../Resources/assets/cache-metrics.css'),
);
diff --git a/src/Admin/SettingsPage.php b/src/Admin/SettingsPage.php
index c95dc47..ee0229a 100644
--- a/src/Admin/SettingsPage.php
+++ b/src/Admin/SettingsPage.php
@@ -264,7 +264,7 @@ public function render(): void
$lastPurge = is_array($diagnostics['last_purge'] ?? null) ? $diagnostics['last_purge'] : null;
$metrics = is_array($diagnostics['metrics'] ?? null) ? $diagnostics['metrics'] : [];
$scanUsable = $status->exists && $status->directory && $status->error === null;
- $pluginData = get_file_data(__DIR__ . '/../../nginx-cache.php', ['version' => 'Version']);
+ $pluginData = get_file_data(__DIR__ . '/../../ns-nginx-cache.php', ['version' => 'Version']);
$configMissing = $diagnostics['nginx_config']['missing_directives'];
$environment = $diagnostics['environment'];
$queueCount = (int) $diagnostics['queue']['pending'];
diff --git a/src/Inspection/SiteHealth.php b/src/Inspection/SiteHealth.php
index d87939d..dad989c 100644
--- a/src/Inspection/SiteHealth.php
+++ b/src/Inspection/SiteHealth.php
@@ -86,7 +86,7 @@ public function check(string $name): array
*/
public function debug(array $information): array
{
- $data = get_file_data(__DIR__ . '/../../nginx-cache.php', ['version' => 'Version']);
+ $data = get_file_data(__DIR__ . '/../../ns-nginx-cache.php', ['version' => 'Version']);
$values = ['version' => $data['version'], 'backend' => $this->backend->string('purge_backend'), 'profile' => $this->settings->profile()->value, 'path' => $this->settings->cachePath(), 'pending_purges' => $this->queue->count(), 'pending_follow_up' => $this->effects->count(), 'scope' => $this->scope->isIsolated() ? 'isolated-site-root' : 'shared-root-site-key-scan', 'worker_heartbeat' => (int) get_option(CacheWorker::HEARTBEAT, 0), 'tag_index' => wp_json_encode($this->tags->stats())];
$fields = [];
foreach ($values as $name => $value) {
diff --git a/tests/Integration/compatibility.php b/tests/Integration/compatibility.php
index f14400f..88a8173 100644
--- a/tests/Integration/compatibility.php
+++ b/tests/Integration/compatibility.php
@@ -30,6 +30,9 @@
throw new RuntimeException('Unexpected consumer HTTP request.');
}));
$container->get(HookLoader::class)->register();
+check(has_filter('plugin_action_links_ns-nginx-cache/ns-nginx-cache.php') !== false, 'compiled kernel registers settings links for the new plugin basename');
+check(str_contains(implode('', apply_filters('plugin_action_links_ns-nginx-cache/ns-nginx-cache.php', [])), 'page=sympress-nginx-cache'), 'plugin-list settings link opens the existing settings page');
+require __DIR__ . '/plugin-identity-checks.php';
do_action('admin_init');
$registered = get_registered_settings();
check(isset($registered[CompatibilitySettings::PREFIX . 'purge_backend'], $registered[CompatibilitySettings::PREFIX . 'prewarm_sitemap'], $registered[CompatibilitySettings::REDIS_PASSWORD]), 'compiled kernel hooks register compatibility settings');
diff --git a/tests/Integration/plugin-identity-checks.php b/tests/Integration/plugin-identity-checks.php
new file mode 100644
index 0000000..977b089
--- /dev/null
+++ b/tests/Integration/plugin-identity-checks.php
@@ -0,0 +1,81 @@
+ ['code' => 200], 'body' => wp_json_encode(['plugins' => ['fixture/fixture.php' => ['slug' => 'fixture', 'plugin' => 'fixture/fixture.php', 'new_version' => '2.0.0', 'package' => 'https://example.test/fixture.zip']], 'no_update' => [], 'translations' => []])];
+ };
+ add_filter('pre_option_active_plugins', $activePlugins);
+ add_filter('pre_http_request', $updateRequest, 100, 3);
+ add_filter('update_plugins_github.com', $externalUpdate, 10, 3);
+ try {
+ wp_register_plugin_realpath($pluginDirectory . '/' . basename($basename));
+ wp_clean_plugins_cache(false);
+ $plugins = get_plugins();
+ check(isset($plugins[$basename]) && $plugins[$basename]['UpdateURI'] === $headers['UpdateURI'] && !is_plugin_active($basename), 'WordPress discovers the protected plugin while inactive');
+ $plugins['fixture/fixture.php'] = array_replace($headers, ['Name' => 'Unrelated update fixture', 'UpdateURI' => '', 'Version' => '1.0.0']);
+ wp_cache_set('plugins', ['' => $plugins], 'plugins');
+ foreach ([[], [$basename]] as $active) {
+ delete_site_transient('update_plugins');
+ wp_update_plugins();
+ $updates = get_site_transient('update_plugins');
+ check(!isset($updates->response[$basename]) && isset($updates->response['fixture/fixture.php']), 'protected plugin receives no directory update while unrelated updates remain visible');
+ }
+ check($requestCount === 2 && $externalChecks === 2, 'inactive and active plugins both retain the external update identity');
+ (new CacheMetricsPanel())->enqueueStyles();
+ foreach (['simple', 'metrics'] as $style) {
+ check(str_ends_with(wp_styles()->registered['sympress-nginx-cache-' . $style]->src, '/' . $slug . '/Resources/assets/cache-' . $style . '.css'), 'dashboard stylesheet resolves under the new plugin folder: ' . $style);
+ }
+ } catch (Throwable $error) {
+ fwrite(STDERR, (string) $error . PHP_EOL);
+ throw $error;
+ } finally {
+ remove_filter('pre_option_active_plugins', $activePlugins);
+ remove_filter('pre_http_request', $updateRequest, 100);
+ remove_filter('update_plugins_github.com', $externalUpdate);
+ $GLOBALS['wp_plugin_paths'] = $originalPaths;
+ wp_cache_delete('plugins', 'plugins');
+ if ($originalCache !== false) { wp_cache_set('plugins', $originalCache, 'plugins'); }
+ if ($originalUpdates === false) { delete_site_transient('update_plugins'); }
+ else { set_site_transient('update_plugins', $originalUpdates); }
+ unlink($pluginDirectory);
+ }
+})();
diff --git a/tests/Integration/v1-admin-checks.php b/tests/Integration/v1-admin-checks.php
index b4dc29b..3769d7f 100644
--- a/tests/Integration/v1-admin-checks.php
+++ b/tests/Integration/v1-admin-checks.php
@@ -105,6 +105,7 @@
update_option(CompatibilitySettings::REDIS_PASSWORD, (new SecretCipher())->encrypt('fixture-debug-redis', CompatibilitySettings::REDIS_PASSWORD));
update_option($settings::OPTION_CLOUDFLARE_API_TOKEN, (new SecretCipher())->encrypt('fixture-debug-cloudflare', $settings::OPTION_CLOUDFLARE_API_TOKEN));
$health = new SiteHealth($settings, new CompatibilitySettings($settings), new \SymPress\NginxCache\Filesystem\CachePathValidator($fs), $queue, $effects, $scope, $clock, $index);
+check($health->debug([])[$settings::TEXT_DOMAIN]['fields']['version']['value'] === get_file_data(dirname(__DIR__, 2) . '/ns-nginx-cache.php', ['version' => 'Version'])['version'], 'Site Health reads the version from the renamed plugin entry');
$debug = serialize($health->debug([]));
check(!str_contains($debug, 'fixture-debug-redis') && !str_contains($debug, 'fixture-debug-cloudflare') && !str_contains($debug, SecretCipher::PREFIX) && !str_contains($debug, 'redis_password'), 'Site Health export whitelists fields and omits credentials');
check(count($health->tests([])['direct']) === 6 && $health->check('worker')['status'] === 'good', 'direct Site Health checks recognize the recent worker heartbeat');