63a8f45b340d2158b4ddf0eb7e3bac4776220d49
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b289c6763 |
1.6.2: WP 7.1.2 and WC 11.1.2 declared, and the caller counts the assertions
Kristian asked the family to check WordPress 7.1.2. Run pinned rather than against "latest", because latest drifts and then nobody knows which version a green belonged to: PHP 7.4 69 assertions, 0 failures "none from this plugin" PHP 8.3 69 assertions, 0 failures "none from this plugin" PHP 8.4 69 assertions, 0 failures "none from this plugin" The container reported WordPress 7.1.2 and WooCommerce 11.1.2 on all three, read out of the run rather than from the variable it was given. So there is no problem with 7.1.2 here. The findings were in the reporting, not the code. The headers were stale: "Tested up to: 7.1" and "WC tested up to: 11.1.0" against current 7.1.2 and 11.1.2. The WooCommerce one is the visible half — a shop on 11.1.2 sees an untested warning on the WooCommerce status page, and on this plugin that banner sits in front of somebody deciding whether to update a discount engine. And the suite said "All assertions passed" without a number, which is the exact failure every other check here was fixed for. It now prints the count. 🔴 Then the floor for that count found a bigger hole than it was written for. Putting it at the end of the smoke file does nothing: wp eval-file runs the file in function scope, so a `return` partway through skips everything after it, including the floor. Measured — truncating tests/smoke.php before the shipping section ran two thirds of the assertions and ci/site-test.sh reported GREEN. A file cannot assert about its own early exit. ci/site-test.sh now captures the output and checks the total against MIN_SITE_ASSERTIONS from outside, where an early return cannot reach, and fails when the summary line is missing altogether. The in-file floor stays for a short run that does reach the end. Proven with the same fixture that exposed it: the truncated suite now exits 1 saying it printed no assertion total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cb58ffe26e |
1.5.4: the text follows the rename, and a check keeps it that way
Kristian read the readme and found it still telling people to configure the plugin under WooCommerce, for a page that moved to the Nolimit menu three releases ago. Sweeping for it found seven stale places, and three of them are notices THE PLUGIN SHOWS THE USER — the missing-WooCommerce notice, the activation error and the incomplete-install warning all still called it WooCommerce Email Discount Manager. That is the mimir.guide failure exactly: the code moved, the text stood still, and nothing failed. It was found by somebody reading, which is the only way it could be found, and that is the argument for the check rather than for being more careful next time. ci/static.sh now asserts two things. The name in readme.txt's title matches the Plugin Name header, because the header is what WordPress shows and everything else has to agree with it. And no shipped PHP file contains OLD_PLUGIN_NAME, which is now a value in ci/plugin.conf rather than a habit. The changelog is deliberately exempt: an entry describes what was true for that release, and rewriting it would be a different lie. Only text describing the CURRENT state is checked. Both halves proven red — a readme title that disagrees, and the old name put back into one notice. Not changed: the page stays under Nolimit rather than gaining a second entry under WooCommerce. One menu is the family's shape, and a duplicate entry is clutter rather than a fix for a readme that was simply wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3020338378 |
Take shared-menu.php 1.1.0 and keep its hash in one place
The family froze a new copy of includes/shared-menu.php: 184cebdc, contract 1.1.0. Two changes in it for this plugin, which was still on f4862d5c (1.0.0). The menu icon is now an infinity sign, painted from admin_head behind a wpmgr_menu_glyph filter that defaults to it — Kristian asked for it because the family is called Nolimit. And the file's own header said 1.0.0 while its constant said 1.1.0, and it counted five plugins where there are six; Nolimit SEO found both. Taken today rather than when convenient, and the reason is measured rather than tidiness: two plugins were on 1.1.0 and three on 1.0.0 at the same time, so a shop got the glyph or the cog depending on activation order — while every repository's own hash check was green on its own side. A frozen file is a family-wide fact, and a green local check says nothing about the shop. The API change is additive — a new filter and a new function, nothing removed — so this plugin's use of wpmgr_ensure_menu() and wpmgr_menu_title is unaffected, and the default glyph means there is nothing to opt into. The hash now lives once, as WPMGR_MENU_SHA in ci/plugin.conf, checked by ci/static.sh and required by ci_load_conf so an unset value cannot turn the comparison into a no-op. It was previously a literal inside tests/smoke.php as well; two copies of one constant drift, and the copy that drifts is the one nobody remembers. The runtime assertion that remains proves a different fact — that the file is loaded at all — because wpmgr_ensure_menu() only exists if it was. Proven red by appending one line to the shared file: the check names both hashes and says not to edit it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ae4112c773 |
Publish releases to kw/nolimit-discounts-dist and point the header there
The family shape is source private, releases in a separate public repo so a shop can fetch an update without credentials and without the source being readable. This plugin was the only one still serving its own update feed out of the repository that holds ci/, docs/ and tests/. kw/nolimit-discounts-dist created (public, zips only). RELEASE_REPO in ci/plugin.conf now names it, and ci/publish-release.sh honours it over GITHUB_REPOSITORY — falling back to the running repo, which is what a plugin without a split still wants. 🔴 The "Update URI:" header moves in the SAME commit as the publishing target, and ci/static.sh now fails when the two disagree. The header is what every installed copy polls; if it names a repository nothing is published to any more, the shop stops being offered updates and nothing announces it, because latest_release() simply returns false. That is the same failure as a privacy page standing still while the code moves, and the check is what makes it impossible to forget. Proven red before being believed: pointing RELEASE_REPO at another repository fails with both sides named. RELEASE_REPO is required by ci_load_conf, because an unset value would turn the comparison into a no-op and restore exactly the bug it prevents. Not done here, and it is the blocker for actually cutting a release: publishing to another repository needs a token with write access to it. The job token only covers the repo it runs in, so this needs a cross-repo PAT in the repository secrets, and that is Kristian's to add. Also not done, deliberately: the source repo stays public for now. Making it private before the dist repo has releases would break the feed for the copy installed on the live shop, whose header still points at the old address. 1.4.6 and earlier keep working through Gitea's rename redirect - measured, the old name still answers 200 - and that redirect is the only thing holding them up, so it must not be removed until a build carrying the new header has propagated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bd058566f5 |
Parse the workflows on Gitea too, and keep the compat matrix off its runner
Both found by the subscriptions session in its copy of this kit, and both reproduce here. They share the shape everything else in ci/ has had: the failure is invisible on GitHub, which is where anyone would be looking. 1. ci/workflows-in-sync.sh never parsed the workflow files on Gitea, and printed "Workflows: in sync." anyway. It skipped with a note when PyYAML was missing — and PyYAML is missing in exactly one place. Measured here rather than taken on report: `import yaml` on gitea/runner-images:ubuntu-latest raises ModuleNotFoundError. So the check was switched off on the single forge where an unparseable workflow produces no run at all, no failed run and no error to click on, which looks exactly like a runner that has not picked the job up. ci_python_yaml in ci/lib.sh now falls back to a container the way ci_php already did — alpine + py3-yaml, no network install, one apk call. There is no skip path left: workflows that cannot be parsed are a failure, not a note. Both routes verified to give byte-identical output. 2. compat.yml ran its five-cell matrix on Gitea for every PR touching ci/ or tests/. The `schedule` was gated to GitHub for exactly this reason and `pull_request` was forgotten. The subscriptions repo measured six jobs dying together with exit code 137 — SIGKILL, runner out of memory, two seconds in, without one error message in the log. Latent here only because this repo merges through GitHub and never opens a Gitea PR; one Gitea PR touching ci/ would have found it. New ci/check-workflows.py enforces both, called through ci_python_yaml. It parses every workflow and fails when a Gitea job with more than one matrix cell is not gated, and it is fatal when it parsed no file at all. 🔴 Its first version asked whether the `if:` CONTAINED the server_url test, and passed the very condition it exists to catch: `!= 'schedule' || server_url == ...` contains the gate while letting pull_request through. Substring matching is adjacent to the signal rather than the signal. It now compares the whole normalised condition against an allowlist, and was checked both ways in one run — red against the unfixed compat.yml naming the job and the cell count, green after. Three self-test assertions added, 29 total: an unparseable workflow fails, an ungated multi-cell matrix fails, and parsing zero files fails. Two notes from building it, both the same trap this kit keeps setting. The container path could not find check-workflows.py because ci_in_container tars `git ls-files` and the file was untracked. And the ungated-matrix fixture used | as a sed delimiter while the gate itself contains ||, so sed silently changed nothing; the assertion reported "could not stage the fixture" instead of passing, which is the only reason it was noticed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9b0b0ab475 |
Make the CI checks say how many files they actually read
Four checks in ci/ printed a reassuring line having examined nothing. Found independently by the woo-ai-manager and woo-cache-manager sessions in their own copies of this kit; all four reproduce here. A check that examined nothing prints the same sentence as one that examined everything, and only the passing side can tell them apart. That is why these survived being verified: a pattern that is too broad fails loudly the first time it meets real code, while one that finds nothing passes forever. So every success line now carries a count — the old text domain line could not tell 142 from 0. - static.sh, direct-access guard: counts what the loop examined and fails below MIN_SHIPPED_PHP_FILES. shipped_files() swallows a dist-files.sh failure with `|| true`, so an empty list is one broken .distignore away, and this is the check standing between a PHP file and being requested straight from a browser outside every capability check the code assumes has happened. The floor is above 1 because a pattern that wrongly excludes includes/ collapses the list without emptying it. - static.sh, credential scan: counts what it read, fatal on zero. A pushed key is compromised even after the commit is deleted, so a silent no-op here is not a weak check but the absence of one wearing a green tick. The floor is 1 and is not configurable: this list has no filter that could go partially wrong, so the only way it misreports is by coming out empty. - check-textdomain.php: three zeros, three verdicts. No PHP files at all is always fatal and deliberately not caller-configurable — it says the scan root is wrong, not that the plugin is small. Below --min is fatal. Zero calls with no --min is a warning, since a small plugin may legitimately have no translatable strings, but it is never silent. - check-textdomain.php: fix the annotated path. str_replace( $root, '', $path ) with the default root of "." removed every dot anywhere in the path, so a real error in includes/class-db.php was reported on "includes/class-dbphp" — not a file, not clickable, and not greppable. Every error this script printed with the default root was mangled that way. The floors live in ci/plugin.conf, the only file under ci/ that knows anything about this plugin, rather than hardcoded in static.sh: static.sh is copied verbatim between plugins too, so a number in it would make this plugin's shape everybody's constraint. ci_load_conf now requires them — an unset floor turns its comparison into a no-op and silently restores the bug. Each floor was broken once and watched to fail before being believed: empty shipped list, wrong scan root, --min above the real count, zero calls without --min, empty credential list, and a plugin.conf missing both floors. Static checks green, and now report 13 shipped PHP files, 142 translation calls across 17 files, 44 tracked files read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a1f6e54f9d |
The smoke test could not fail, and other people's eyes
Four fixes, two of them serious, three of them found by the session porting
this into subscriptions.
1. THE SMOKE TEST COULD NOT FAIL. `wp eval-file` runs the file through eval()
INSIDE A METHOD, so the top level is function scope. `$wc_edm_failures = 0`
at the top and `global $wc_edm_failures` in the helper were two different
variables: the helpers incremented the real global, the verdict read the
local, and the local was always zero.
Measured rather than reasoned. A deliberately false assertion added to the
unmodified file:
FAIL DELIBERATE FAILURE — this run must go red
All assertions passed on WordPress 7.1 with WooCommerce 11.1.0.
EXIT=0
and with the fix, the same forced failure:
FAIL DELIBERATE FAILURE — this run must go red
1 assertion(s) failed on WordPress 7.1.
EXIT=1
The FAIL lines always printed, so failures found by reading the output were
real finds. What was broken is the CI gate: a regression on either forge
would have gone green. Four lines at the top now prove the counter reaches
the verdict before anything relies on it — this is the one file whose whole
job is to be able to go red, so it checks that it can.
2. `git ls-files` QUOTES NON-ASCII PATHS. "TESTMILJØ.md" comes back as the
literal string "TESTMILJ\303\230.md", which matches no pattern in
.distignore, so the file is kept — an internal document walking into the
release zip with nothing said. Fixed in ci/dist-files.sh and in
ci/static.sh's trailing-?> check, which had the same bug and would have
silently skipped the file it exists to check. Verified by adding such a
filename and watching it be excluded. `done < <(...)` rather than a pipe, or
the loop runs in a subshell and the failure counter stops incrementing.
3. THE FEED CACHE WAS SIX HOURS, and Kristian hit it: a release published and
his shop shown nothing. The reasoning ("WordPress checks twice a day") was
wrong. WordPress gates its own check by screen in wp_update_plugins() — one
minute on Dashboard → Updates, one hour on Plugins, 2–12 hours in the
background — so WordPress is already the rate limiter and a longer cache
only hides releases underneath it. Fifteen minutes now, pinned by an
assertion.
4. The wp_update_plugins() assertion made a real HTTP request, because the
force-check tests had just deleted the transient. It passed only because
this feed repo is public, which made it measure the environment rather than
the code. Re-primed first.
ci_in_container() replaces the inline docker plumbing in ci_php(), taken from
the subscriptions port: the script goes in as a file rather than interpolated
into `sh -c`, and --entrypoint sh matters for images whose entrypoint swallows
its arguments.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4a5e3d8879 |
Run the text-domain check in a container when the runner has no PHP
The first Gitea run failed on `php: command not found`. GitHub's hosted runners ship PHP; the Gitea runner image does not, and the whole static job went red on one line after everything else in it had passed. ci_php() now uses the local php when there is one — a laptop, GitHub's runners — and a php:8.4-cli container otherwise. The repository goes in over a tarball and `docker cp` rather than a bind mount, because a Gitea Actions job runs inside a container talking to the HOST's docker daemon, so a host path there lands on an empty directory nobody created. Also: the include scan now says when it found nothing to check instead of reporting success. Running the suite with a deliberately crippled PATH showed it printing "0 plugin-relative include(s) checked" and then "green" — a check passing by checking nothing, which is the same failure the PHP lint step already guards against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eca2851d35 |
Test against the newest WordPress and WooCommerce, and ship updates
Three things this repository could not do: prove the plugin still works on a
WordPress or WooCommerce released after it, get a fix onto a shop without
someone uploading a zip, and do either of those for the next plugin without
starting over.
ci/ is the test harness. ci/site-test.sh builds a throwaway WordPress +
WooCommerce in containers at any versions you name — including `nightly` and
`beta` — installs exactly the files the release zip will contain, and runs
tests/smoke.php inside it with wp eval-file. Real cart, real coupons, real
WC_Shipping_Rate objects: a mocked cart keeps passing after WooCommerce has
changed underneath it, which is the one failure worth catching here. WP_DEBUG
is on and any notice logged from this plugin's directory fails the build,
because that is the shape an upgrade actually breaks in — nothing fails, a
notice appears, and two releases later it is a fatal.
compat.yml runs that weekly against today's releases, the next WordPress and
the next WooCommerce, the oldest PHP the header promises and the newest PHP.
When it is green it opens a PR bumping "Tested up to" and "WC tested up to" —
only when green, because WooCommerce shows that second value to the shop owner
as an untested-plugin banner, and that banner is what they read before deciding
whether to press update.
includes/class-updater.php is the distribution half. It hooks
update_plugins_{hostname} for the host in the new "Update URI" header and reads
the Gitea release API, so WordPress handles the notice, the download, the
"Enable auto-updates" link and wp plugin update with no further code. Update
URI earns its place twice: since 5.8 a plugin that declares one is never asked
about on wordpress.org, so a .org plugin sharing this slug cannot be pushed
onto a shop running this one. The check sends nothing about the site and
overrides WordPress's default user agent, which carries the site's own URL.
release.yml tags, tests, builds, publishes — then reads the feed back with no
credentials. A private repository and a release with no zip attached both leave
a page that looks perfect to anyone logged in and an update no shop is ever
offered.
Measured before committing, all green: WP 7.1 + WC 11.1 on PHP 8.3 and 7.4,
WP 7.2-alpha on 8.3, and the beta cell falling back to stable on 8.4. No
deprecations from this plugin on any of them.
Three guards are there because the bug they catch is invisible without them:
the annotated tag is re-fetched before its message is read (checkout turns it
into a lightweight tag pointing at the merge commit), the two workflow roots
are kept in sync (.gitea/workflows hides .github/workflows entirely), and every
file the code loads must be in the release zip — which caught class-updater.php
being referenced and uncommitted while every local test passed.
docs/REUSE.md is the port to the next plugin: copy ci/, rewrite five lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|