Rename the repository to woo-discount-manager, keep the installed slug

Kristian named the five plugins as a family, and three repos disagreed with what
he calls them. His decision: rename the repo, never the installed slug.

MEASURED BEFORE DOING IT, because four published releases carry the old URL in
their Update URI header. A throwaway repo was created, renamed and deleted:

    old API path                    301 -> new
    old web path                    301 -> new
    old /releases/download/... path 301 -> new

and WordPress follows up to 5 redirects by default (class-wp-http.php:191), on
GET. Confirmed afterwards against the real thing: the old feed URL resolves to
v1.4.5 and the old download path still serves 48185 bytes.

So a shop on 1.4.2 keeps updating through the redirect, and 1.4.6 moves it onto
the new address directly.

PLUGIN_SLUG and PLUGIN_MAIN are deliberately untouched and now disagree with the
repository name. That is permanent: the installed directory has been
woocommerce-email-discount-manager since the first release and is on a live
shop, and changing it would not move that installation — it would put a SECOND
copy beside it and leave the old one active. ci/plugin.conf says so in the file,
because otherwise somebody will helpfully align them.

Also in, both for the family rather than for this plugin alone:

- docs/REUSE.md gains the hostname-keyed hook as a first-class section. It is
  the only bug we have found that is CAUSED by there being more than one plugin,
  so it is invisible to anyone reading a single repo — along with the same shape
  in a shared-*.php file copied into five plugins, which fatals with "Cannot
  redeclare" on the second activation without function_exists() guards.
- ci/static.sh fails on anything shaped like a credential in a tracked file.
  Suggested by the AI-manager port: a pushed key cannot be unpushed, only
  rotated. Deliberately narrow — sk-ant-, sk-, ghp_, gho_, github_pat_, AKIA,
  PRIVATE KEY blocks — because a broad entropy heuristic fires on minified
  assets and a check that cries wolf gets commented out.

Verified failing before being believed: a file containing sk-ant-api03-... makes
the step report the filename and the suite exit 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-16 18:18:50 +02:00
co-authored by Claude Opus 5
parent 5e95893c1a
commit f14c51dcf2
5 changed files with 91 additions and 5 deletions
+59
View File
@@ -140,6 +140,65 @@ None of them were caught by reading the code, and a green build is exactly what
a broken check looks like. So: break it on purpose once, watch it go red, then
put it back. It costs one run.
## The bug you cannot find by reading your own repo
🔴 **`update_plugins_{$hostname}` is keyed on the hostname, not the
repository.** Every plugin whose `Update URI` points at the same host shares one
hook, and WordPress runs **every** callback on it for **every** one of those
plugins:
```php
// wp-includes/update.php
$update = apply_filters( "update_plugins_{$hostname}", false, $plugin_data, $plugin_file, $locales );
```
Answer unconditionally and you offer your release to your neighbour. Measured on
a live shop: a subscriptions plugin was shown this one's 1.4.4 and told it was
an update for itself. Load order decides who wins, so which plugin gets
corrupted is arbitrary — and pointed at one with auto-updates enabled, it
installs a stranger's zip over itself, unattended, on a shop taking money.
```php
add_filter( "update_plugins_{$host}", array( $this, 'offer_update' ), 10, 3 );
public function offer_update( $update, $plugin_data, $plugin_file = '' ) {
if ( '' !== $plugin_file && plugin_basename( SELF_FILE ) !== $plugin_file ) {
return $update;
}
```
Both halves. Leave the accepted-argument count at 2 and the third argument never
arrives, so the guard compares against an empty string and does nothing — the
same shape of bug it exists to fix. `plugins_api` is safe if it guards on
`$args->slug`.
**No integration test can find this.** A site test installs one plugin from that
host, so the collision cannot arise there. The assertion is the entire defence:
```php
wc_edm_ok( 'another plugin on the same host is not offered our release',
false === $updater->offer_update( false, $data, 'someone-else/someone-else.php' ) );
```
The same shape applies to anything else shared between sibling plugins. A
`shared-*.php` file copied byte-identically into five plugins fatals with
`Cannot redeclare` on the second activation unless everything in it is wrapped
in `function_exists()` / `class_exists()` / `defined()` — and then the copy that
loads first wins, which is alphabetical order, which is arbitrary. Freeze the
contract rather than versioning the file.
## The repository name and the installed slug are different things
The repo may be renamed freely — Gitea keeps a 301 on both the API and the deep
release-download paths (measured), and WordPress follows up to 5 redirects by
default, so installed copies keep updating through the old `Update URI`.
The **installed** slug can never change. It is `<slug>/<main file>` in
`wp-content/plugins`, and a new one installs a second copy beside the first
while the old one stays active. So `PLUGIN_SLUG` in `ci/plugin.conf` is allowed
to disagree with the repository name forever, and if it does, say so in the file
or somebody will helpfully align them.
## Two guards worth understanding before you move them
**`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the