Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db0cdd4e19 | ||
|
|
63a8f45b34 | ||
|
|
539abf946c | ||
|
|
5151204b12 | ||
|
|
97818ec4db | ||
|
|
7b1d667b5a | ||
|
|
cc0aaf9272 | ||
|
|
f6ea46a93e | ||
|
|
2b289c6763 |
+10
-3
@@ -2,11 +2,11 @@
|
||||
Contributors: kristianwind
|
||||
Tags: woocommerce, discount, email, shipping, coupon
|
||||
Requires at least: 5.8
|
||||
Tested up to: 7.1
|
||||
Tested up to: 7.1.2
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.6.1
|
||||
Stable tag: 1.6.3
|
||||
WC requires at least: 6.0
|
||||
WC tested up to: 11.1.0
|
||||
WC tested up to: 11.1.2
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
@@ -96,6 +96,13 @@ The most specific match wins: exact email, then domain, then partial match.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.6.3 =
|
||||
* The WooCommerce version this plugin declares as tested is now the same in both places it is written. The readme still said 11.1.0 while the plugin itself said 11.1.2.
|
||||
|
||||
= 1.6.2 =
|
||||
* Declares compatibility with WordPress 7.1.2 and WooCommerce 11.1.2, both tested. No functional change.
|
||||
|
||||
|
||||
= 1.6.1 =
|
||||
* The status label on the Nolimit dashboard card is now readable. It was white text on a light overlay, which on this plugin's green gave 3.5:1 where 4.5:1 is the accessibility minimum.
|
||||
|
||||
|
||||
@@ -60,10 +60,15 @@ done
|
||||
git diff --quiet && { echo "Nothing actually changed on disk — check the sed patterns." >&2; exit 1; }
|
||||
|
||||
branch="compat/tested-up-to-$current_wp-wc-$current_wc"
|
||||
git config user.name "compatibility check"
|
||||
git config user.email "noreply@localhost"
|
||||
git checkout -b "$branch"
|
||||
git commit -am "Tested against WordPress $current_wp and WooCommerce $current_wc
|
||||
# The identity is scoped to THIS commit with `git -c`, never written to the
|
||||
# clone. `git config user.name` persists in .git/config, so every later commit
|
||||
# in this working copy — a release days afterwards, by a person — would be
|
||||
# authored by the robot with no Co-Authored-By trailer. git announces a
|
||||
# MISSING identity and says nothing about a wrong one, so that is silent.
|
||||
# Signing this one commit as the robot is right: it is not a person's.
|
||||
git -c user.name="compatibility check" -c user.email="noreply@localhost" \
|
||||
commit -am "Tested against WordPress $current_wp and WooCommerce $current_wc
|
||||
|
||||
$(printf '%s\n' "${changed[@]}")
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ ci_load_conf() {
|
||||
# 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.
|
||||
local missing=
|
||||
for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO WPMGR_MENU_SHA OLD_PLUGIN_NAME; do
|
||||
for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO WPMGR_MENU_SHA OLD_PLUGIN_NAME MIN_SITE_ASSERTIONS; do
|
||||
[ -n "${!var:-}" ] || missing="$missing $var"
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
|
||||
@@ -103,3 +103,9 @@ WPMGR_MENU_SHA=75b04eb4fe9eda0119312075accf2fc77de244e5c6b759d624a8b1d254c0fc99
|
||||
# can fail when it reappears in text a user reads — a sentence that goes stale
|
||||
# breaks nothing by itself, which is why it survives.
|
||||
OLD_PLUGIN_NAME="WooCommerce Email Discount Manager"
|
||||
|
||||
# The fewest assertions a complete run of tests/smoke.php prints. Verified by
|
||||
# ci/site-test.sh rather than only inside the file: a smoke file runs in
|
||||
# function scope, so a `return` partway through skips its own floor and the
|
||||
# run reports green having tested two thirds of what it claims.
|
||||
MIN_SITE_ASSERTIONS=60
|
||||
|
||||
+165
@@ -205,6 +205,171 @@ ann=$(ci_php ci/check-textdomain.php "$TEXT_DOMAIN" selftest-dotted 2>&1 \
|
||||
&& ok "the annotated path exists on disk: $ann" \
|
||||
|| bad "the annotated path is not a file: '$ann'"
|
||||
|
||||
# ------------------------------------------------------- the clone's git identity
|
||||
# 🔴 Grade 2 evidence: row 1 is the exact line ci/bump-tested-up-to.sh carried
|
||||
# until 2026-09-25, not an arbitrary `git config`. That line writes .git/config,
|
||||
# so every later commit in the clone — a release, by a person — is authored by
|
||||
# the robot with no trailer. Six such commits in one sibling plugin, two in
|
||||
# another. git warns about an identity it cannot FIND and never about a wrong
|
||||
# one, so nothing in the repository ever says so.
|
||||
#
|
||||
# The controls are the larger half here, and three of them were live faults in
|
||||
# earlier versions of this check: `--[a-z-]+` as the scope group reads `--unset`
|
||||
# as a scope and flags the REPAIR, and without a trailing-value requirement a
|
||||
# `$(git config user.name)` READ is a violation. A build that fails on its own
|
||||
# remedy is how a correct check gets deleted — the failure accuses the check,
|
||||
# and the next person believes it.
|
||||
#
|
||||
# 🔴 The fixtures are assembled through $gc rather than written out, because
|
||||
# THIS file is a ci/*.sh file and the check reads every one of them. Spelled in
|
||||
# full, its own rows match: 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 — and the labels have to dodge it too, which is why they name the shape
|
||||
# instead of quoting the line. Identical to the credential fixtures three
|
||||
# hundred lines up, one file over.
|
||||
#
|
||||
# The plant goes into ci/build-zip.sh because nothing here SOURCES it: static.sh
|
||||
# and this script source ci/lib.sh only. A plant in a sourced file is not inert
|
||||
# text — it runs, sets the clone's identity for real, and the commit fixing the
|
||||
# fault is authored by the plant. nolimit-seo lost a run to exactly that. So the
|
||||
# last assertion checks the clone rather than the output.
|
||||
echo
|
||||
echo "→ ci/static.sh must catch a written identity and ignore reads and repairs"
|
||||
cd "$work/repo"
|
||||
cp ci/build-zip.sh "$work/build-zip.orig"
|
||||
|
||||
identity_row() { # $1 = RED|green (want), $2 = label, $3 = line to plant
|
||||
cp "$work/build-zip.orig" ci/build-zip.sh
|
||||
printf '%s\n' "$3" >> ci/build-zip.sh
|
||||
# The plant must BE there before any verdict is believed. A printf that went
|
||||
# somewhere else leaves the control state, and then every row passes while
|
||||
# proving nothing — and the all-green rows are exactly the ones whose zeroes
|
||||
# you want to believe.
|
||||
if [ "$(grep -cF "$3" ci/build-zip.sh)" -lt 1 ]; then
|
||||
bad "could not stage the git-identity fixture: $2"
|
||||
return
|
||||
fi
|
||||
row_out=$(./ci/static.sh 2>&1 || true)
|
||||
# The stable half of the message. The wider phrasing moved from "A ci/ script
|
||||
# sets …" to "A tracked file writes …" when the scan was widened, and these
|
||||
# rows all reported NOT FLAGGED while the check was working perfectly — an
|
||||
# assertion on a log line is coupled to its wording.
|
||||
if printf '%s' "$row_out" | grep -q 'git identity that outlives it'; then
|
||||
got=RED
|
||||
else
|
||||
got=green
|
||||
fi
|
||||
if [ "$got" = "$1" ]; then
|
||||
ok "$2 -> $got"
|
||||
elif [ "$1" = RED ]; then
|
||||
bad "NOT FLAGGED: $2 — the clone keeps the robot's name"
|
||||
else
|
||||
bad "FALSE POSITIVE: $2 — the check goes red on something it must allow"
|
||||
fi
|
||||
}
|
||||
|
||||
gc="git config" # never spelled out below: see the note above
|
||||
identity_row RED 'WRITE no flag, quoted value' "$gc user.name \"compatibility check\""
|
||||
identity_row RED 'WRITE --local scope' "$gc --local user.name \"x\""
|
||||
identity_row RED 'WRITE --global scope' "$gc --global user.email \"x@y\""
|
||||
identity_row RED 'WRITE --system scope' "$gc --system user.name \"x\""
|
||||
identity_row RED 'WRITE unquoted value' "$gc user.email noreply@localhost"
|
||||
identity_row green 'REPAIR --local --unset' "$gc --local --unset user.name"
|
||||
identity_row green 'REPAIR --unset, no scope' "$gc --unset user.email"
|
||||
identity_row green 'READ inside a command substitution' "echo \"\$($gc user.name)\""
|
||||
identity_row green 'FIX git -c ... commit' 'git -c user.name="x" -c user.email=y commit -m z'
|
||||
identity_row green 'other a non-identity key' "$gc core.autocrlf false"
|
||||
identity_row green 'prose a commented-out mention' "# $gc user.name \"x\" is the fault"
|
||||
cp "$work/build-zip.orig" ci/build-zip.sh
|
||||
|
||||
# A violation OUTSIDE ci/*.sh. This is the row the old scope missed entirely:
|
||||
# a workflow step writes an identity into the runner's clone just as
|
||||
# permanently, and the glob the check started with could not see it.
|
||||
cp "$work/build-zip.orig" ci/build-zip.sh
|
||||
wf=.github/workflows/selftest-identity.yml
|
||||
printf 'name: x\njobs:\n y:\n steps:\n - run: %s user.email "x@y"\n' "$gc" > "$wf"
|
||||
git add -N "$wf" >/dev/null # untracked files are invisible to git ls-files
|
||||
wf_out=$(./ci/static.sh 2>&1 || true)
|
||||
printf '%s' "$wf_out" | grep -q 'writes a git identity that outlives it' \
|
||||
&& ok "flagged a write outside ci/ (a workflow step)" \
|
||||
|| bad "NOT FLAGGED: a workflow step writing an identity — the scan is still scoped to ci/"
|
||||
rm -f "$wf"
|
||||
git rm --cached -q --ignore-unmatch "$wf" >/dev/null 2>&1
|
||||
|
||||
# 🔴 A drift check on the FORM, and only that: `if … | xargs -0 grep …; then`
|
||||
# has a false green in it, because xargs exits 123 when any batch found nothing
|
||||
# and `if` reads 123 as no-match. It is invisible while the list fits one exec,
|
||||
# which is why nothing here can measure it — this tree's 48 files are one batch.
|
||||
# So this asserts the shape is gone rather than that the bug is absent, and it
|
||||
# cannot tell you the replacement is correct. scan_tracked greps per file and
|
||||
# reads its verdict from the output, which is what makes batching irrelevant.
|
||||
# ^[^#]* — the first version of this matched the COMMENT in static.sh that
|
||||
# quotes the bad form in order to explain it, which is the stale-docblock trap
|
||||
# in its own right: the paragraph documenting a rule is text the rule applies to.
|
||||
if grep -nE '^[^#]*(if|elif).*\| *xargs.*grep' ci/static.sh; then
|
||||
bad "a check above consumes xargs' exit status — 123 on a partial match reads as clean"
|
||||
else
|
||||
ok "no check in ci/static.sh consumes an xargs exit status"
|
||||
fi
|
||||
|
||||
# And the passing line must carry a number: "every script is clean" reads the
|
||||
# same over zero files as over eleven.
|
||||
clean_out=$(./ci/static.sh 2>&1 || true)
|
||||
printf '%s' "$clean_out" | grep -qE "^ [0-9]+ tracked files leave the clone's git identity alone" \
|
||||
&& ok "the git-identity check reports how many files it read" \
|
||||
|| bad "the git-identity check printed no count — it cannot tell 48 files from 0"
|
||||
|
||||
# The fixtures are lines that SET an identity. If any of them executed rather
|
||||
# than being read, this clone now has one — which would also mean every verdict
|
||||
# above was measured against a tree nobody intended.
|
||||
if git config --local --get-regexp '^user\.' >/dev/null 2>&1; then
|
||||
bad "a git-identity fixture RAN — the clone has a local identity and the rows above are void"
|
||||
else
|
||||
ok "no fixture executed: the clone still has no local identity"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------ compat values in two files
|
||||
# 🔴 The two currency checks read each value from the file that value belongs
|
||||
# in — correct, and therefore blind to a SECOND copy in the other file. The
|
||||
# plugin header said WC 11.1.2 and README.txt said 11.1.0 for four releases
|
||||
# with that step printing green. Each reading was right; the pair was never
|
||||
# compared. So these rows are about our own two copies agreeing, which is a
|
||||
# fact about this tree and fatal, not about being up to date, which is advisory.
|
||||
echo
|
||||
echo "→ a compatibility value in two files must agree"
|
||||
cd "$work/repo"
|
||||
cp README.txt "$work/README.orig"
|
||||
compat_row() { # $1 = field, $2 = value to plant in the readme
|
||||
cp "$work/README.orig" README.txt
|
||||
sed -i "s/^$1: .*/$1: $2/" README.txt
|
||||
if [ "$(grep -c "^$1: $2\$" README.txt)" -lt 1 ]; then
|
||||
bad "could not stage the compat fixture for '$1'"
|
||||
return
|
||||
fi
|
||||
compat_out=$(./ci/static.sh 2>&1 || true)
|
||||
if printf '%s' "$compat_out" | grep -q "'$1' is .* in the plugin header and"; then
|
||||
ok "flagged a drifted '$1' ($2 in the readme)"
|
||||
else
|
||||
bad "NOT FLAGGED: '$1' drifted to $2 in the readme and nothing compared the two copies"
|
||||
fi
|
||||
}
|
||||
compat_row 'WC tested up to' '11.0.0'
|
||||
compat_row 'Requires PHP' '8.0'
|
||||
compat_row 'Requires at least' '5.0'
|
||||
cp "$work/README.orig" README.txt
|
||||
|
||||
# The pass must say how many pairs it compared. A tree where the readme carries
|
||||
# none of these fields has nothing to disagree and prints the same silence as
|
||||
# one where every pair matches.
|
||||
compat_clean=$(./ci/static.sh 2>&1 || true)
|
||||
pairs=$(printf '%s' "$compat_clean" | grep -c "^ both files say ")
|
||||
if [ "$pairs" -ge 3 ]; then
|
||||
ok "$pairs value pairs compared across the two files"
|
||||
else
|
||||
bad "only $pairs pairs compared — the agreement check has almost nothing to look at"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------- plugin.conf
|
||||
echo
|
||||
echo "→ the floors must be required, not defaulted"
|
||||
|
||||
+31
-1
@@ -177,9 +177,39 @@ wp plugin list --name="$PLUGIN_SLUG" --field=status | grep -qx active \
|
||||
|| { echo "The plugin did not stay active after activation." >&2; exit 1; }
|
||||
echo " activated $(wp plugin get "$PLUGIN_SLUG" --field=version)"
|
||||
|
||||
# 🔴 The output is captured and the summary checked HERE, by the caller.
|
||||
#
|
||||
# A smoke file cannot assert about its own early exit: `wp eval-file` runs it
|
||||
# in function scope, so a `return` partway through skips everything after it —
|
||||
# including any floor the file carries at the bottom. Measured: truncating
|
||||
# tests/smoke.php before the shipping section ran two thirds of the assertions
|
||||
# and this script reported green, because the file's own floor was one of the
|
||||
# things never reached.
|
||||
#
|
||||
# So the number the file prints is verified against MIN_SITE_ASSERTIONS out
|
||||
# here, where an early return cannot reach. The floor inside the file stays as
|
||||
# well; it catches a short run that still gets to the end.
|
||||
for f in ${SITE_TESTS:-}; do
|
||||
ci_step "$f"
|
||||
wp eval-file "wp-content/plugins/$PLUGIN_SLUG/$f"
|
||||
out=$(wp eval-file "wp-content/plugins/$PLUGIN_SLUG/$f" 2>&1) || { printf '%s\n' "$out"; exit 1; }
|
||||
printf '%s\n' "$out"
|
||||
|
||||
case "$f" in
|
||||
*smoke*)
|
||||
ran=$(printf '%s' "$out" | sed -n -E 's/^All ([0-9]+) assertions passed.*/\1/p' | tail -1)
|
||||
if [ -z "$ran" ]; then
|
||||
echo
|
||||
ci_fail "$f printed no assertion total. It exited before its summary — which is what a file that returned halfway through looks like, and it cannot report that about itself."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ran" -lt "$MIN_SITE_ASSERTIONS" ]; then
|
||||
echo
|
||||
ci_fail "$f ran only $ran assertions, expected at least $MIN_SITE_ASSERTIONS. Part of the suite did not run."
|
||||
exit 1
|
||||
fi
|
||||
echo " $ran assertions, at or above the floor of $MIN_SITE_ASSERTIONS"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ci_step "PHP notices and deprecations"
|
||||
|
||||
+149
-4
@@ -19,6 +19,51 @@ shipped_files() {
|
||||
./ci/dist-files.sh | grep '\.php$' || true
|
||||
}
|
||||
|
||||
# 🔴 scan_tracked: the only way this file greps a set of tracked files.
|
||||
#
|
||||
# The form it replaces — `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 and `if` reads that as clean. 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
|
||||
#
|
||||
# Harmless only while the whole list fits in one exec. git ls-files hands over
|
||||
# enough paths to split the moment a tree grows or a path gets long, and then
|
||||
# the check passes over a violation it has already printed. Nothing about the
|
||||
# output says which case you are in. Reported by the Nolimit Security session
|
||||
# 2026-09-28, who hit it after widening their own scan from 13 files to 102.
|
||||
#
|
||||
# So: no xargs, and the verdict is the OUTPUT rather than any exit status. The
|
||||
# loop also yields the count for free, which is the other thing every one of
|
||||
# these checks was missing — "none" over zero files and "none" over nineteen
|
||||
# are the same green.
|
||||
#
|
||||
# Sets: scan_hits (matches, one "file:line:text" per line), scan_files (how
|
||||
# many files were read). A grep failure (exit > 1) is reported, never swallowed.
|
||||
scan_tracked() { # $1 = -E|-P, $2 = pattern, rest = git pathspecs
|
||||
local mode=$1 pattern=$2 f m rc
|
||||
shift 2
|
||||
scan_hits=''
|
||||
scan_files=0
|
||||
# -z: git QUOTES a non-ASCII path, and the quoted form is not a path that
|
||||
# exists, so a line-based read silently skips the file it meant to check.
|
||||
while IFS= read -r -d '' f; do
|
||||
[ -n "$f" ] || continue
|
||||
scan_files=$((scan_files + 1))
|
||||
m=$(grep -n "$mode" -e "$pattern" -- "$f")
|
||||
rc=$?
|
||||
if [ "$rc" -gt 1 ]; then
|
||||
note_fail "grep failed on $f (exit $rc) — this scan did not examine it."
|
||||
elif [ "$rc" = 0 ]; then
|
||||
scan_hits="${scan_hits}$(printf '%s\n' "$m" | sed "s|^|$f:|")
|
||||
"
|
||||
fi
|
||||
done < <(git ls-files -z "$@")
|
||||
}
|
||||
|
||||
ci_step "PHP syntax"
|
||||
# The header promises a minimum PHP version; without it in the loop a piece of
|
||||
# newer syntax passes here and fatals on the customer's server instead.
|
||||
@@ -73,10 +118,14 @@ else
|
||||
fi
|
||||
|
||||
ci_step "no debug leftovers"
|
||||
if git ls-files -z '*.php' | xargs -0 grep -nE '\b(var_dump|print_r|dd)[[:space:]]*\(' ; then
|
||||
scan_tracked -E '\b(var_dump|print_r|dd)[[:space:]]*\(' '*.php'
|
||||
if [ "$scan_files" -lt 1 ]; then
|
||||
note_fail "The debug-leftover scan read no PHP files at all, so it proved nothing."
|
||||
elif [ -n "$scan_hits" ]; then
|
||||
printf '%s' "$scan_hits"
|
||||
note_fail "Debug functions must not ship."
|
||||
else
|
||||
echo " none"
|
||||
echo " none in $scan_files tracked PHP files"
|
||||
fi
|
||||
|
||||
ci_step "no short open tags, no closing ?>"
|
||||
@@ -84,9 +133,14 @@ before=$fails
|
||||
# With short_open_tag off the code is printed to the visitor as text instead of
|
||||
# being run; whitespace after a closing tag is sent to the browser and breaks
|
||||
# headers and redirects.
|
||||
if git ls-files -z '*.php' | xargs -0 grep -nP '<\?(?!php|=|xml)' ; then
|
||||
scan_tracked -P '<\?(?!php|=|xml)' '*.php'
|
||||
if [ "$scan_files" -lt 1 ]; then
|
||||
note_fail "The open-tag scan read no PHP files at all, so it proved nothing."
|
||||
elif [ -n "$scan_hits" ]; then
|
||||
printf '%s' "$scan_hits"
|
||||
note_fail "Use <?php, not short open tags."
|
||||
fi
|
||||
open_tag_files=$scan_files
|
||||
# -z: git quotes non-ASCII paths, and the quoted form is not a path that
|
||||
# exists, so this loop would silently skip the file it was meant to check.
|
||||
while IFS= read -r -d '' f; do
|
||||
@@ -94,7 +148,7 @@ while IFS= read -r -d '' f; do
|
||||
[ "$(tail -c 3 "$f" | tr -d '[:space:]')" = "?>" ] \
|
||||
&& { ci_annotate error "$f" 1 "Remove the trailing ?> at the end of the file."; fails=$((fails + 1)); }
|
||||
done < <(git ls-files -z '*.php')
|
||||
[ "$fails" = "$before" ] && echo " clean"
|
||||
[ "$fails" = "$before" ] && echo " $open_tag_files tracked PHP files, no short tags and no trailing ?>"
|
||||
|
||||
ci_step "text domain"
|
||||
# --min is deliberately far below the real count. It is not an assertion about
|
||||
@@ -307,6 +361,72 @@ elif [ "$fails" = "$before" ]; then
|
||||
echo " $secrets_scanned tracked files read, nothing that looks like a key or a private key"
|
||||
fi
|
||||
|
||||
ci_step "no tracked file configures the clone it was handed"
|
||||
# `git config user.name` writes .git/config and OUTLIVES the script. A helper
|
||||
# that sets an identity before committing therefore renames every later commit
|
||||
# in that working copy, including a release made by a person days afterwards
|
||||
# and with no Co-Authored-By trailer. 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.
|
||||
#
|
||||
# ci/bump-tested-up-to.sh did this until it was scoped to `git -c … commit`,
|
||||
# which still signs its own commit as the robot — correct, that one is not a
|
||||
# person's — without touching the clone. Measured across the family: six
|
||||
# misattributed hand commits in one sibling, two in another, including
|
||||
# releases. This tree was clean and one run away.
|
||||
#
|
||||
# The pattern has to separate a WRITE from a read and from the repair, and the
|
||||
# obvious widening gets all three wrong. Measured row by row:
|
||||
#
|
||||
# git config user.name "compatibility check" WRITE caught
|
||||
# git config --local user.name "x" WRITE caught
|
||||
# git config --global user.email "x@y" WRITE caught <- --local only: missed
|
||||
# git config --system user.name "x" WRITE caught
|
||||
# git config --local --unset user.name REPAIR ignored <- --[a-z-]+: flagged
|
||||
# git config --unset user.email REPAIR ignored <- --[a-z-]+: flagged
|
||||
# echo "$(git config user.name)" READ ignored <- --[a-z-]+: flagged
|
||||
# git -c user.name="x" … commit -m y FIX ignored
|
||||
# git config core.autocrlf false other ignored
|
||||
# git config --global core.pager cat other ignored
|
||||
# # git config user.name "x" (a comment) prose ignored
|
||||
# (nothing planted) clean ignored
|
||||
#
|
||||
# ⚠️ Do NOT widen the scope group to `--[a-z-]+`. It reads `--unset` as a scope
|
||||
# and then finds `user.` right after it, so the check goes red on the very
|
||||
# repair this message recommends — and a build that fails on the remedy is how
|
||||
# a correct check gets deleted: the failure accuses the check, and the next
|
||||
# person believes it. Three sessions reached for `--[a-z-]+` independently on
|
||||
# 2026-09-25, which is why it is written here rather than re-derived.
|
||||
#
|
||||
# Enumerating the three real scopes is one half. The other is requiring a VALUE
|
||||
# after the key — `user\.[a-zA-Z]+[[:space:]]+[^[:space:])]` — which is what
|
||||
# makes a read not a violation: `$(git config user.name)` has `)` where a write
|
||||
# has a value. Found by nolimit-seo, whose first version failed on it.
|
||||
#
|
||||
# The rule: a script may configure ITSELF, never the working copy it was
|
||||
# handed. Reported by the Nolimit Security session 2026-09-25.
|
||||
#
|
||||
# Writing the fault in prose inside a tracked file will trip this, by design.
|
||||
# To name it in documentation without going red, write the key as user.<key> —
|
||||
# the pattern needs [a-zA-Z] after the dot, so an angle-bracket placeholder is
|
||||
# prose and a real key is a violation.
|
||||
# Every tracked file, not just ci/*.sh. A workflow step, a hook, a Makefile or
|
||||
# a docs snippet somebody pastes can write an identity 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.
|
||||
# Nolimit Security widened theirs from 13 files to 102 after the thing that
|
||||
# actually ran on this box turned out to be outside that glob.
|
||||
scan_tracked -E '^[^#]*git config([[:space:]]+--(local|global|system))*[[:space:]]+user\.[a-zA-Z]+[[:space:]]+[^[:space:])]' .
|
||||
if [ "$scan_files" -lt 1 ]; then
|
||||
note_fail "The git-identity scan read no tracked files at all, so it proved nothing."
|
||||
elif [ -n "$scan_hits" ]; then
|
||||
printf '%s' "$scan_hits"
|
||||
note_fail "A tracked file writes a git identity that outlives it. Scope it to the one command with 'git -c user.name=… commit', which does not write .git/config."
|
||||
else
|
||||
echo " $scan_files tracked files leave the clone's git identity alone"
|
||||
fi
|
||||
|
||||
ci_step "no placeholders left in the plugin header"
|
||||
# The header is the plugin's business card: wp-admin shows the author and the
|
||||
# links to whoever installs it, so a placeholder there survives for as long as
|
||||
@@ -356,6 +476,31 @@ behind() { # claimed current -> true when claimed is the older of the two
|
||||
# reads that one from the plugin header and nowhere else.
|
||||
|
||||
|
||||
# 🔴 A value in two files drifts, and reading each one from its authoritative
|
||||
# file is a correct answer to a different question. The two checks below ask
|
||||
# "is this value current", one per value, each in the file that value belongs
|
||||
# in — so a SECOND copy of the same value in the other file is never compared
|
||||
# with anything. Measured 2026-09-28: the plugin header said WC 11.1.2 and
|
||||
# README.txt said 11.1.0, four releases after the bump, with this step green
|
||||
# and printing "plugin header says 11.1.2, current release is 11.1.2" — true,
|
||||
# complete, and about the copy that was already right.
|
||||
#
|
||||
# So: any field carried by BOTH files must agree. Not "is it current" (that is
|
||||
# advisory below, and rightly), but "do our own two copies say the same thing",
|
||||
# which is a fact about this tree and therefore fatal.
|
||||
for field in 'Tested up to' 'WC tested up to' 'Requires at least' 'Requires PHP'; do
|
||||
h=$(ci_plugin_header "$field")
|
||||
r=
|
||||
[ -n "$readme" ] && r=$(sed -n -E "s/^$field:[[:space:]]*([^[:space:]]+).*/\1/p" "$readme" | head -1)
|
||||
if [ -n "$h" ] && [ -n "$r" ]; then
|
||||
if [ "$h" = "$r" ]; then
|
||||
echo " both files say '$field: $h'"
|
||||
else
|
||||
note_fail "'$field' is '$h' in the plugin header and '$r' in $readme. One of them is stale and nothing else compares them."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
tested_wp=
|
||||
[ -n "$readme" ] && tested_wp=$(sed -n -E 's/^Tested up to:[[:space:]]*([^[:space:]]+).*/\1/p' "$readme" | head -1)
|
||||
echo " WordPress: ${readme:-readme} says ${tested_wp:-<missing>}, current release is ${current_wp:-<unknown>}"
|
||||
|
||||
+17
-1
@@ -31,6 +31,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
* that it can.
|
||||
*/
|
||||
$GLOBALS['wc_edm_failures'] = 0;
|
||||
$GLOBALS['wc_edm_passes'] = 0;
|
||||
( static function () { ++$GLOBALS['wc_edm_failures']; } )();
|
||||
if ( 1 !== $GLOBALS['wc_edm_failures'] ) {
|
||||
echo "The failure counter does not reach the verdict: this file cannot go red.\n";
|
||||
@@ -47,6 +48,7 @@ $GLOBALS['wc_edm_failures'] = 0;
|
||||
*/
|
||||
function wc_edm_ok( $what, $condition, $detail = '' ) {
|
||||
if ( $condition ) {
|
||||
++$GLOBALS['wc_edm_passes'];
|
||||
printf( " ok %s\n", $what );
|
||||
return;
|
||||
}
|
||||
@@ -799,8 +801,22 @@ if ( $GLOBALS['wc_edm_failures'] > 0 ) {
|
||||
printf( "%d assertion(s) failed on WordPress %s.\n", $GLOBALS['wc_edm_failures'], get_bloginfo( 'version' ) );
|
||||
exit( 1 );
|
||||
}
|
||||
// The number, not just the word. "All assertions passed" is also what a file
|
||||
// that asserted nothing prints — which is the failure this whole suite exists
|
||||
// to catch everywhere else, and it was in the summary line the entire time.
|
||||
// A floor as well, for the same reason every count in ci/ has one: a run that
|
||||
// died early after a handful of assertions would otherwise read as a pass.
|
||||
if ( $GLOBALS['wc_edm_passes'] < 60 ) {
|
||||
printf(
|
||||
"Only %d assertions ran, which is too few to be a complete run.\n",
|
||||
$GLOBALS['wc_edm_passes']
|
||||
);
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
printf(
|
||||
"All assertions passed on WordPress %s with WooCommerce %s.\n",
|
||||
"All %d assertions passed on WordPress %s with WooCommerce %s.\n",
|
||||
$GLOBALS['wc_edm_passes'],
|
||||
get_bloginfo( 'version' ),
|
||||
defined( 'WC_VERSION' ) ? WC_VERSION : '?'
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Plugin URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
||||
* Update URI: https://gitea.nolimit.dk/kw/nolimit-discounts-dist
|
||||
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
||||
* Version: 1.6.1
|
||||
* Version: 1.6.3
|
||||
* Author: Kristian Wind
|
||||
* Author URI: https://gitea.nolimit.dk/kw
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -12,7 +12,7 @@
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 6.0
|
||||
* WC tested up to: 11.1.0
|
||||
* WC tested up to: 11.1.2
|
||||
* Requires Plugins: woocommerce
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'WC_EDM_VERSION', '1.6.1' );
|
||||
define( 'WC_EDM_VERSION', '1.6.3' );
|
||||
define( 'WC_EDM_PLUGIN_FILE', __FILE__ );
|
||||
define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
Reference in New Issue
Block a user