1752bcf8ddeec4d622e4bf7c067cc3bc37f2d2f3
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dccb87d15e |
Document the plugin, and check that the documentation still describes it
Five files under docs/, the names the family agreed: README, SETUP,
ARCHITECTURE, CONTRACT, RELEASE. Plus ci/check-docs.php, ported from
woo-security-manager 52dd99d, wired into the static job.
CONTRACT.md is generated from the source rather than written from memory, and
that was the right instruction: my first sweep read only single-quoted hook
names and missed two rows, one of them update_plugins_{$hostname} — the hook
behind the live-shop collision, written with interpolation and therefore
invisible to a pattern built from the concept of the line. Nine hooks
documented: six wc_edm_* filters this plugin offers, three wpmgr_* it both
offers and answers through its copy of the frozen shared-menu.php.
Two hooks the checker structurally cannot see are listed by hand with a note
saying so: wc_edm_prune_audit_log (add_action on a class constant) and
update_plugins_{$hostname} (interpolated). A generated list beats a remembered
one and is still a list of what the generator can see.
The checker needed two fixes before it was safe to run here.
🔴 The published version reads HOOK_PREFIX from ci/plugin.conf for the
documented side, and refuses without it — then tests the source side against a
hardcoded 'nlsec_'. Ported verbatim that is not a green over nothing, which
would be easy to spot. Measured in this tree with CONTRACT.md documenting no
hook at all:
3 hook(s) in the shipped source and not in docs/CONTRACT.md:
wpmgr_dashboard_cards, wpmgr_menu_glyph, wpmgr_menu_title
Red, specific, and blind to all six wc_edm_* hooks — because 'wpmgr_' is the
same string in every tree and 'nlsec_' is the one that is not. Fix those three
and it goes green over six undocumented contract hooks, with a red run behind
you as evidence that it works. A partially blind check that fails is more
convincing than one that passes.
Note which step disarmed the guard. The floor at 20 shipped PHP files refused
to run in this 14-file tree, and that refusal was the only thing standing in
the way; the porting instructions were to add HOOK_PREFIX and adjust the floor.
Doing as told is what removed the protection — two wrongs cancelling into a
pass, in the order CLAUDE.md predicts, because the floor is the error you can
see and the prefix is not.
Both numbers now come from the conf (HOOK_PREFIX, MIN_SHIPPED_PHP_FILES) and
both refuse rather than defaulting. The inherited literal 20 is gone: a second
hardcoded number per plugin is a second thing to re-measure on every port.
Ten selftest assertions, 58 total. Four failure modes, the two refusals, the
counts on the pass, and the row that matters most:
🔴 A hook name in a COMMENT must not read as a hook call. CONTRACT.md documents
a filter by showing the apply_filters() a consumer makes, and the same example
sits in a docblock — so a checker that reads comments reports this plugin
consuming a filter it answers, in the file explaining why it must not. That
assertion is a control asserting a PASS, which is the kind that can quietly
become unable to fail, so it names the specific success line rather than
reading an exit code, and it is proven both ways: with token_get_all's
T_COMMENT branch made inert, exactly one of 58 assertions fails and it is that
one.
docs/TESTING-AND-RELEASES.md is folded into RELEASE.md and deleted rather than
left beside it — two copies of a rule is how the copies start to disagree.
Folding surfaced that it still documented the pre-rename Update URI
(kw/woocommerce-email-discount-manager) and still recommended, as future work,
the split into a releases-only repository that has already happened. Both
corrected in place, with the correction dated.
docs/ is not in ci/dist-files.sh — measured, 0 docs files in the zip.
static=0 selftest=0 site=0; site still 69 assertions on WordPress 7.1.2 with
WooCommerce 11.1.2. ci/ and docs/ ship nothing, so no version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
db0cdd4e19 |
1.6.3: the tested-WooCommerce version now agrees with itself
The plugin header said "WC tested up to: 11.1.2" and README.txt said 11.1.0 — since 1.6.2, which is the release whose whole content was bumping that value, and whose commit message says both were bumped. Only one was. The check that exists for this printed green the entire time, and both of its readings were correct: WordPress: README.txt says 7.1.2, current release is 7.1.2 WooCommerce: plugin header says 11.1.2, current release is 11.1.2 It reads each value from the file that value belongs in, for a good reason written in the comment above it: WordPress reads "Tested up to" only from readme.txt, WooCommerce reads "WC tested up to" only from the plugin header. So each check asks "is this value current" of the authoritative copy — and a SECOND copy of the same value in the other file is compared with nothing. A correct answer to a different question, which is the shape most false greens here have. Two checks, two questions, kept apart: - "Is this value current" stays advisory, per the comment already there: a build that goes red on release day for a header nobody has had a chance to update is a build that gets ignored. - "Do our own two copies say the same thing" is a fact about this tree with no release-day excuse, so it is fatal. Three pairs are actually duplicated here — WC tested up to, Requires at least, Requires PHP — so the step has live rows rather than one. Proven red one field at a time with a landing check each, and the count is on the green line, because a readme carrying none of these fields has nothing to disagree and would print the same silence as one where every pair matches. 50 selftest assertions. static=0 selftest=0 site=0; site still 69 on WordPress 7.1.2 with WooCommerce 11.1.2. README.txt ships, so this is a version bump: 1.6.3. The changelog keeps 1.6.2's entry — my first pass sed'd 1.6.2 to 1.6.3 across the file and rewrote that heading, which would have made the changelog describe a release that never happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
539abf946c |
Stop reading a verdict out of xargs' exit status
`if git ls-files -z … | xargs -0 grep -nE …; then` has a false green in it. xargs exits 123 when ANY of its invocations exited 1-125, and grep exits 1 on no-match, so a run where one batch matched and another did not exits 123 — which `if` reads as clean, after grep has already printed the hit. Measured: one batch, a match xargs exit 0 -> caught one batch, no match xargs exit 123 -> clean, correct two batches, one match xargs exit 123 -> CLEAN, and it printed the hit Three checks here used that form: debug leftovers, short open tags, and the git-identity scan added two days ago. All three are one batch at this tree's size, so none of them is wrong today — this is a latent false green that arrives with the file list, not a bug anyone would see. Nothing in the output distinguishes the cases. scan_tracked() replaces it: one grep per file, verdict from the OUTPUT rather than any exit status, and a grep failure (exit > 1) reported instead of swallowed. Batching stops existing. The loop also yields the file count, which is the other thing all three were missing — "none" over zero files and "none" over nineteen were the same green. They now read "none in 19 tracked PHP files" and fail below a floor of one. The identity scan is also widened from ci/*.sh to every tracked file. A workflow step writes an identity into the runner's clone just as permanently, and ci/*.sh was scope chosen from where the first instance happened to live — the same "what are your checks pointed at" question as the contrast check. 13 files to 48. The new selftest row plants a write into a workflow file, which the old scope could not see at all. Reported by the Nolimit Security session, who hit the xargs case after widening their own scan from 13 files to 102. Three of my own probes were wrong before they were right, all the same class: the assertions grep for the check's message text, and widening the scope changed the wording from "A ci/ script sets …" to "A tracked file writes …". Five rows reported NOT FLAGGED while the check was working perfectly. They now match the stable half of the message, with the reason written beside them. And the form check matched the comment in static.sh that quotes the bad form in order to explain it, so it is anchored with ^[^#]* — the paragraph documenting a rule is text the rule applies to. That form check is a drift check on the shape and says so: it asserts the pattern is gone, not that the replacement is correct, because this tree's 48 files are one batch and nothing here can measure the fault. 46 selftest assertions. static=0 selftest=0 site=0; site still 69 assertions on WordPress 7.1.2 with WooCommerce 11.1.2. ci/ ships nothing, so no version bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
97818ec4db |
Let the identity check tell a write from a read and from its own repair
Widening the scope group to --[a-z-]+ was wrong in a way the previous
commit's matrix could not see, because it had no repair row: --unset is
matched as a scope flag, and user. follows it, so
git config --local --unset user.name
is flagged as a violation. That is the remedy this check recommends in its
own failure message. A build that fails on its own repair is how a correct
check gets deleted — the failure accuses the check, and the next person
believes it. The same widening flags a read, echo "$(git config user.name)".
Two halves, both needed. Enumerate the three real scopes, and require a
VALUE after the key: user\.[a-zA-Z]+[[:space:]]+[^[:space:])]. The trailing
class is what makes a read legal — $(git config user.name) has ) where a
write has a value. Sixteen rows measured one at a time, 16/16, and eleven of
them now run in ci/selftest.sh (44 assertions).
Reported by the Nolimit Security session; three of us reached for
--[a-z-]+ independently, so the comment above the step says explicitly not
to, with the reason, rather than leaving it to be re-derived. The read row
came from nolimit-seo.
Two things found while proving it, both about the probe rather than the
check:
ci/selftest.sh is itself a ci/*.sh file, so its fixtures trip the check they
test. Spelled out in full, the first version of this block reported six
false positives and no count, while the same rows measured standalone were
16/16 correct — a failing probe is evidence about the probe until you have
looked at it. Fixed the way the credential fixtures three hundred lines up
in the same file already were: assemble the value through $gc so the source
matches nothing while the written file matches fully. The labels need it
too, so they name the shape instead of quoting the line.
And a plant that lands in a SOURCED file is not inert text. nolimit-seo
planted into ci/lib.sh, which ci/static.sh sources, so the line ran and set
the clone's identity for real. The target here is ci/build-zip.sh and only
ci/lib.sh is sourced — verified rather than assumed — and a final assertion
checks the clone still has no local identity, because if a fixture had
executed then every verdict above it was measured against a tree nobody
intended.
static=0 selftest=0 site=0; site still reports 69 assertions on WordPress
7.1.2 with WooCommerce 11.1.2. ci/ ships nothing, so no version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cc0aaf9272 |
Scope the compatibility bump's git identity to its own commit
`ci/bump-tested-up-to.sh` ran `git config user.name "compatibility check"` before committing. That writes .git/config and outlives the script, so from the first run onward every commit in that working copy is authored by "compatibility check <noreply@localhost>" with no Co-Authored-By trailer — including a release made by a person days later about something else. git announces an identity it cannot FIND and says nothing about one that is wrong, so the missing case is loud and this one is silent. Nothing fails, nothing warns, and the author is only visible to somebody reading `git log` for another reason. Measured across the family by the Nolimit Security session: six misattributed hand commits in one sibling including two releases, two in another, one in a third. This tree was clean — no local identity, 0 misattributed — and one run of the script away. The commit still signs itself as the robot, which is right: that one is not a person's. It just does it with `git -c … commit`, which configures the command and not the clone. Guarded in ci/static.sh, because the fix is one line and reverting it would be invisible for exactly as long as nobody reads an author field. Widened past what was reported: the pattern accepts any scope flag, since --global is the same fault one scope wider and reaches every repository on the box. Proven with a full matrix rather than one trigger — no flag, --local, --global and --system each go red with the named message and exit 1; a commented mention, `git -c user.name=… commit`, `git config core.autocrlf` and `git config --global core.pager` all stay green. The correct form being a control matters: a check that flagged the fix would get one of the two deleted. Five of those assertions are now in ci/selftest.sh (36 total), each with a landing check that the plant is in the file before its verdict is believed, and the passing line carries the script count — "every script is clean" reads the same over zero files as over eleven. ci/ ships nothing, so there is no version bump and no release here. 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> |
||
|
|
d121005c0a |
Replace the two growing | grep -q checks with case
Under `set -o pipefail` a grep that MATCHES can make the pipeline exit 141: grep closes the pipe on its first hit, the producer takes SIGPIPE, and pipefail reports that as failure. A match becomes an error. Security found the shape; measured here before believing it, because the earlier pipefail lesson went the other way and I did not want to carry a second claim I had not seen: yes hittet | grep -q hittet under pipefail -> exit 141 printf hittet | grep -q hittet -> exit 0 So it needs the producer to outrun the 64 KB pipe buffer, which is why it is invisible until it is not. Two of this repo's sites can get there, and both were converted. Neither is reachable today — that is the reason to do it now rather than an argument against: ci/publish-release.sh checked the release feed for a download URL. The feed is 3.5 KB in the new repo and 13.5 KB in the old one across seven releases, so about 1.9 KB each: it reaches 64 KB around release 34. That failure would land in the publishing path and say the feed has no zip attached while it plainly has one, and nothing about the commit that tripped it would have anything to do with it. A bug on a timer is worse than a bug. ci/static.sh checked whether each plugin-relative include is in the shipped list, once per include. Same growth, plus a fork per iteration. Both now use `case`, which has no pipe, no fork and no producer to signal, and cannot be reversed by a shell option. The static.sh one wraps both sides in newlines to keep the exact-line match `grep -qx` gave. Proven both ways: thirteen includes checked on a clean tree, and excluding one from the zip still names it. The remaining `| grep -q` sites were measured rather than assumed safe: zip_entries is 1.3 KB, the shipped list 478 bytes, static.sh's own output 1.6 KB, and site-test.sh and update-smoke.sh grep a single line of wp-cli output. Left alone; converting those would be churn against producers that cannot plausibly reach the buffer. 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> |
||
|
|
d1703dcc03 |
Check the direct-access guard by tokenising, not by grepping for it
The check asked whether "defined( 'ABSPATH' )" appeared anywhere in the file. A comment containing those words therefore passed it. Measured with a planted file whose only mention was in a comment: 15 shipped PHP files examined, every one guarded Green over a file with no guard at all — one that runs for anyone who fetches it directly, outside WordPress and outside every capability check the code assumes has already happened. This is the check with the worst consequence of being hollow, and it was hollow in the one way a grep always can be. From the cache session by way of security: a check that must RECOGNISE its own code has one blind spot per recognition method, and one that reads what the source actually says has none of that kind. PHP's tokeniser cannot be fooled by a comment, because it is the thing that decides what a comment is. ci/check-abspath.php parses each shipped file and looks for a real defined() call on ABSPATH or WP_UNINSTALL_PLUGIN. Comments and doc blocks are tokens of their own and are simply never examined. The count and the floor stay, for the reason every check here has them. Verified through the container path with php hidden from `command -v`, since static.sh runs on a Gitea runner that has no php: identical output either way. A 31st self-test assertion plants the same fixture. It is grade 2 evidence — a planted instance of exactly the failure the check describes, rather than an arbitrary trigger that only proves the check can go red at all. Two things that assertion caught about itself on its first run: The check's success line now says "parsed" where it said "examined", and the existing assertion on the count was still matching the old word. It went red rather than quietly matching nothing, which is why the wording is asserted at all. And `static.sh | grep -q` reports STATIC.SH's exit status under `set -o pipefail`, not grep's — and static.sh is expected to fail in that fixture, so the assertion read "not found" however the check behaved. The mirror image of the `go test | grep` trap already in the playbook. Output is captured to a variable first now, with the reason inline. Swept the kit for text belonging to other plugins while here, after cache found a comment in its copy describing security's tree: no NLSEC_/NLCAC_ references, and every path named in ci/ and tests/ either exists here or is WordPress core. 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> |
||
|
|
f3126b57e6 |
Stop the credential scan silently skipping binary files
Adding the name-anchored rule in the previous commit also put `-I` on the
value-pattern grep, which it did not have before. grep -I SILENTLY SKIPS a file
it decides is binary, so a PEM header inside one is waved through without a
word. Without -I the same file reports "binary file matches" and fails, which
is the right direction to be wrong in for a check whose whole subject cannot be
unpushed.
Nothing went red to announce this. The tree has no binary tracked files today,
so the narrowing was invisible from both sides — the clean run stayed green and
no probe covered it, because every probe so far planted a text file at the repo
root. It is the same class as the hole this scan already had: a check that
cannot match anything and a check that matches nothing print the same line.
The two rules now differ on -I deliberately, each with the reason inline. The
name rule keeps it: that one is heuristic, so a chance match inside a compiled
asset is noise rather than a finding.
Also proven, rather than asserted, after a sibling plugin found its own scan was
reading only the SHIPPED file list and therefore missing 36 of 73 tracked files
— every CI script, workflow and test fixture, which is exactly where a key gets
pasted to test it. This scan reads `git ls-files` and always has, but the claim
had never been measured: probes in ci/, tests/ and .github/workflows/ are all
caught while `dist-files.sh` lists none of them.
Battery now 19, both directions, through ci/static.sh:
13 fire — AKIA, PEM, sk-ant-, sk-, ghp_, gho_, github_pat_, array form,
define() form, the same key in ci/ and tests/ and .github/, and a
PEM header inside a binary file
6 quiet — a bare hash, an unfilled placeholder, a masked value, an empty or
short value, a comma after 'token' in ordinary code, and a define
whose value is still TODO
The comma control comes from the sibling plugin: it put `,` into the shared
alternation and had to prove `wp_die( 'token', 'a long sentence' )` stays
quiet. This tree anchors the comma case on `define(` instead, so the same
string was never at risk — but it is worth a control either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
82d6cc7d7d |
Close the inert-pattern hole in the credential scan
A count proves the scan READ a file. It says nothing about whether the pattern can match anything — and "now it reports a number" and "now it only reports a number" print the identical line. The woo-ai-manager session proved this the hard way: it planted an AWS key, watched its file count rise from 37 to 38, and the scan passed green. Its prefix list had no AKIA entry at all. This tree's list did have AKIA, but the proof behind that claim was a grep of the regex in isolation, not a run of ci/static.sh — a stub harness that proves the pattern compiles, not that the check fires. Re-done properly: every shape the pattern claims to catch is now planted in a tracked file and run through the whole script. That exercise found a real hole. A prefix list matches the shape of a VALUE, so it is blind to any credential with no vendor marker: an api_key assigned twenty opaque characters walked straight past it. Added a second, name-anchored rule that fires only where the code itself calls the value a secret, covering both the array/assignment form and define(). It is not an entropy heuristic — the file's existing refusal of those still stands, because they fire on minified assets and hashes and a check that cries wolf gets commented out. Placeholders are excluded, or the rule would go red on the one thing a settings screen is guaranteed to contain. Proven from both sides, through ci/static.sh rather than in isolation: 9 shapes planted and all 9 flagged (AKIA, PEM, sk-ant-, sk-, ghp_, gho_, github_pat_, array form, define form), and 4 lookalikes correctly ignored (a bare hash, an unfilled placeholder, a masked value, an empty/short value). 13/13. The one false positive measured was this change's own explanation: the first version of the comment spelled the example out in full and the rule flagged it. Fixed by writing it in prose. Deliberately no bypass marker — an unexercised escape hatch is its own hollow check; add one when a real documentation case needs it. Note for the sibling plugins: an untracked fixture is invisible here, since the scan reads git ls-files. A probe planted without `git add -N` passes clean and looks like the scan working. 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> |
||
|
|
f14c51dcf2 |
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>
|
||
|
|
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>
|
||
|
|
056be0e917 |
Credit the plugin to its actual author, and 1.4.3
The header went out as "Author: WEDSMP" with a Plugin URI of example.com. That is what wp-admin shows to whoever installs it, which is why it is worth a release of its own rather than waiting for the next change. Author is now Kristian Wind, with an Author URI pointing at where the source lives, and the readme credits kristianwind rather than wedsmp. ci/static.sh now fails on a placeholder in the header. It catches the generic markers — example.com, TODO, yourname, an unfilled <...> — and it would have caught the Plugin URI. It would NOT have caught "WEDSMP": a made-up vendor name is indistinguishable from a real one to any check, so an empty field is warned about and a wrong-but-plausible one cannot be. 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>
|