Merge pull request #32 from kristianwind/shared-menu-1-1-0
CI / Static checks (push) Successful in 49s
CI / WordPress latest + WooCommerce latest on PHP 8.3 (push) Successful in 1m2s

Take shared-menu.php 1.1.0 and keep its hash in one place
This commit is contained in:
Kristian Wind
2026-09-20 09:33:02 +02:00
committed by GitHub
5 changed files with 117 additions and 23 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ ci_load_conf() {
# leaves the floors empty, every comparison becomes a no-op, and the hollow # leaves the floors empty, every comparison becomes a no-op, and the hollow
# checks these were added to fix come back silently — the same shape of bug. # checks these were added to fix come back silently — the same shape of bug.
local missing= local missing=
for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO; do for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO WPMGR_MENU_SHA; do
[ -n "${!var:-}" ] || missing="$missing $var" [ -n "${!var:-}" ] || missing="$missing $var"
done done
if [ -n "$missing" ]; then if [ -n "$missing" ]; then
+12
View File
@@ -66,3 +66,15 @@ MIN_TRANSLATION_CALLS=60
# job token only covers the repo it runs in, so this is a cross-repo PAT and # job token only covers the repo it runs in, so this is a cross-repo PAT and
# Kristian's to add. # Kristian's to add.
RELEASE_REPO=kw/nolimit-discounts-dist RELEASE_REPO=kw/nolimit-discounts-dist
# The sha256 of includes/shared-menu.php, which is byte-identical in all six
# plugins. Which copy a site loads is decided by activation order and PHP
# cannot un-declare a function, so the first one wins for the whole request —
# a shop with two plugins on one version and three on another gets whichever
# booted first, while every repository's own check is green on its own side.
# The frozen hash is therefore a family-wide fact, not a local one, and taking
# a new one is something all six do in the same window.
#
# 184cebdc = contract 1.1.0 (the ∞ glyph, and the header/count corrections
# Nolimit SEO found). Previous frozen hash was f4862d5c = 1.0.0.
WPMGR_MENU_SHA=184cebdcd86024d4e864c529bc68ead0f7d63656d9bb3dfe0e1fdf3203e364ae
+18
View File
@@ -174,6 +174,24 @@ case "$update_uri" in
*) note_fail "Update URI is '$update_uri' but releases are published to '$RELEASE_REPO'. Installed copies poll the header, so they would stop being offered updates without a word." ;; *) note_fail "Update URI is '$update_uri' but releases are published to '$RELEASE_REPO'. Installed copies poll the header, so they would stop being offered updates without a word." ;;
esac esac
ci_step "the shared menu file is the family copy"
# includes/shared-menu.php is byte-identical in all six plugins. Editing it
# here would not make this plugin's version win — which copy a site loads is
# decided by activation order, and PHP cannot un-declare a function — it would
# make the menu differ between shops depending on which plugin booted first,
# and change its mind when one is deactivated. So the file is frozen, and
# taking a new version is something all six do in the same window.
shared_menu=includes/shared-menu.php
if [ ! -f "$shared_menu" ]; then
echo "::warning::$shared_menu is not present, so this plugin registers no shared menu."
else
have=$(sha256sum "$shared_menu" | cut -d" " -f1)
echo " sha256: $have"
[ "$have" = "$WPMGR_MENU_SHA" ] \
&& echo " matches the frozen family copy" \
|| note_fail "$shared_menu does not match the frozen family copy. Expected $WPMGR_MENU_SHA, got $have. Do not edit it — take the new version the others took."
fi
ci_step "no secrets in tracked files" ci_step "no secrets in tracked files"
# Suggested by the AI-manager port, and it costs nothing: a pushed key cannot be # Suggested by the AI-manager port, and it costs nothing: a pushed key cannot be
# unpushed. It has to be rotated, and the window between the push and noticing # unpushed. It has to be rotated, and the window between the push and noticing
+79 -11
View File
@@ -6,18 +6,21 @@
* │ THIS FILE IS BYTE-IDENTICAL IN EVERY MANAGER PLUGIN. │ * │ THIS FILE IS BYTE-IDENTICAL IN EVERY MANAGER PLUGIN. │
* │ Do not edit it to add something for one plugin. Extend it through the │ * │ Do not edit it to add something for one plugin. Extend it through the │
* │ `wpmgr_dashboard_cards` filter, which is what it is for. │ * │ `wpmgr_dashboard_cards` filter, which is what it is for. │
* │ Contract version 1.0.0 — frozen. See "Why it is frozen" below. │ * │ Contract version 1.1.0. See "Why it is frozen" below — and note that │
* │ the two changes it has had were both paid for in load-order confusion. │
* └──────────────────────────────────────────────────────────────────────────┘ * └──────────────────────────────────────────────────────────────────────────┘
* *
* The problem it solves: five plugins from the same family, each adding a * The problem it solves: a family of plugins each adding a top-level menu is
* top-level menu, is five entries in a sidebar that already has fifteen. So * a handful of entries in a sidebar that already has fifteen. So they share
* they share one parent, "Manager", and each adds a submenu under it. * one parent and each adds a submenu under it. Six of them as of this
* writing, which is why the count is not stated anywhere it would have to be
* maintained.
* *
* Three things about how it does that are load-bearing, and none is obvious. * Three things about how it does that are load-bearing, and none is obvious.
* *
* **1. Every declaration is guarded.** Five plugins each `require_once` their * **1. Every declaration is guarded.** Each plugin `require_once`s its own
* own copy of this file, in one request. `require_once` only deduplicates by * copy of this file, in one request. `require_once` only deduplicates by
* resolved path, and these are five different paths — so without the * resolved path, and these are different paths — so without the
* `function_exists()` guard, activating the second plugin is * `function_exists()` guard, activating the second plugin is
* `Fatal error: Cannot redeclare wpmgr_ensure_menu()`. A white screen on a * `Fatal error: Cannot redeclare wpmgr_ensure_menu()`. A white screen on a
* shop, caused by nothing except owning two of these plugins. It is the same * shop, caused by nothing except owning two of these plugins. It is the same
@@ -29,8 +32,16 @@
* un-declare a function, so there is no honest way for a newer copy to replace * un-declare a function, so there is no honest way for a newer copy to replace
* an older one that has already been loaded. The only stable answer is that * an older one that has already been loaded. The only stable answer is that
* the file never changes. Hence: **frozen**. The parent slug, the capability, * the file never changes. Hence: **frozen**. The parent slug, the capability,
* the position and the filter name are a contract between five repositories * the position and the filter name are a contract between every repository in
* now, and anything new arrives through the filter. * the family, and anything new arrives through the filter.
*
* 🔴 It has moved twice, and both times the cost was the same: a half-adopted
* change means the menu is whatever the alphabetically-first plugin's copy
* says, and every repository's own hash check goes green on its own side
* while the family disagrees. Nolimit SEO measured exactly that today — two
* plugins on 1.1.0 and three on 1.0.0, so a shop got the infinity glyph or
* the old cog depending on activation order. If this file has to change,
* every plugin takes it in the same hour or the change is not made.
* *
* **3. The dashboard is built from a filter, not owned by a plugin.** If the * **3. The dashboard is built from a filter, not owned by a plugin.** If the
* plugin that happened to register the menu is deactivated, the menu is * plugin that happened to register the menu is deactivated, the menu is
@@ -58,7 +69,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
if ( ! defined( 'WPMGR_MENU_VERSION' ) ) { if ( ! defined( 'WPMGR_MENU_VERSION' ) ) {
define( 'WPMGR_MENU_VERSION', '1.0.0' ); define( 'WPMGR_MENU_VERSION', '1.1.0' );
} }
if ( ! defined( 'WPMGR_MENU_SLUG' ) ) { if ( ! defined( 'WPMGR_MENU_SLUG' ) ) {
@@ -92,19 +103,76 @@ if ( ! function_exists( 'wpmgr_ensure_menu' ) ) {
*/ */
$title = apply_filters( 'wpmgr_menu_title', 'Manager' ); $title = apply_filters( 'wpmgr_menu_title', 'Manager' );
/**
* The glyph on the shared parent menu.
*
* An infinity sign, because the family is called Nolimit. A cog said
* nothing, and there is no infinity dashicon.
*
* Drawn as TEXT rather than as an SVG passed to add_menu_page(), and
* the reason is colour. WordPress renders a data-URI SVG as a CSS
* background-image, and a background-image cannot inherit a colour —
* so it stays one fixed grey while every other icon lightens on hover
* and turns white on the current item. It looks subtly broken under
* every admin colour scheme, including the default. A `::before`
* glyph is text: it takes the colour it is given and follows all of
* those states for free, with no image request.
*
* Filterable so that changing it later needs no edit to this file.
* This file is byte-identical across the family and hash-frozen in
* each plugin's CI, so an edit here is a coordinated release for all
* of them; a filter is one line in one plugin.
*
* @param string $glyph A single character, or '' for the old cog.
*/
$glyph = apply_filters( 'wpmgr_menu_glyph', '∞' );
add_menu_page( add_menu_page(
$title, $title,
$title, $title,
'manage_options', 'manage_options',
WPMGR_MENU_SLUG, WPMGR_MENU_SLUG,
'wpmgr_render_dashboard', 'wpmgr_render_dashboard',
'dashicons-admin-generic', // 'none' makes WordPress emit the icon wrapper with no dashicon
// class on it, which is the hook the style below needs. A dashicon
// name here would win instead, because its class carries its own
// ::before content.
'' === $glyph ? 'dashicons-admin-generic' : 'none',
// Just below WooCommerce (55.x) and above Appearance (60). The // Just below WooCommerce (55.x) and above Appearance (60). The
// fractional position is not decoration: an integer collides with // fractional position is not decoration: an integer collides with
// whatever core or another plugin put there, and the loser is // whatever core or another plugin put there, and the loser is
// simply dropped. // simply dropped.
58.9 58.9
); );
if ( '' !== $glyph ) {
add_action( 'admin_head', 'wpmgr_menu_glyph_style' );
}
}
/**
* The one style rule the glyph needs.
*
* Printed in the head on every admin page, because the menu is on every
* admin page — a stylesheet enqueued only on this family's own screens
* would leave a blank square everywhere else.
*
* `font-family` has to be overridden: WordPress sets the icon wrapper to
* the Dashicons font, and Dashicons has no infinity glyph, so inheriting
* it renders nothing at all.
*/
function wpmgr_menu_glyph_style() {
$glyph = apply_filters( 'wpmgr_menu_glyph', '∞' );
if ( '' === $glyph ) {
return;
}
printf(
'<style>#adminmenu .toplevel_page_%1$s .wp-menu-image::before{content:"%2$s";font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:20px;font-weight:400;line-height:1.4}</style>',
esc_attr( WPMGR_MENU_SLUG ),
esc_attr( $glyph )
);
} }
/** /**
+7 -11
View File
@@ -314,17 +314,13 @@ wc_edm_is(
isset( $wc_edm_mine[0]['colour'] ) ? $wc_edm_mine[0]['colour'] : null isset( $wc_edm_mine[0]['colour'] ) ? $wc_edm_mine[0]['colour'] : null
); );
// includes/shared-menu.php is byte-identical in all five plugins, and which // The frozen hash of includes/shared-menu.php is asserted in ci/static.sh
// copy a site loads is decided by activation order. Editing it here would make // against WPMGR_MENU_SHA in ci/plugin.conf, and deliberately NOT repeated
// a site with three of five updated show one label or another depending on who // here: two copies of the same constant drift, and the one that drifts is the
// booted first — and PHP cannot un-declare a function, so the first copy wins // one nobody remembers to update. What is worth asserting at runtime is a
// for the whole request. The hash is the guard: change the file and this goes // different fact — that the file is actually loaded in a real install, which
// red rather than the behaviour going strange on somebody's shop. // the parent-menu assertion above already proves, since wpmgr_ensure_menu()
wc_edm_is( // only exists if it was.
'and the shared menu file is the family copy, unedited',
'f4862d5c3a2d24dadddaaec90c9221d61f2923964e164fd525a800ddcec63185',
hash_file( 'sha256', WC_EDM_PLUGIN_DIR . 'includes/shared-menu.php' )
);
/* ------------------------------------------------------------ woocommerce -- */ /* ------------------------------------------------------------ woocommerce -- */