Compare commits

...
Author SHA1 Message Date
kristianwindandClaude Opus 5 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>
2026-09-16 13:18:22 +02:00
github-actions[bot] 48119f2034 Merge pull request #8 from kristianwind/author-name
CI / Static checks (push) Successful in 13s
CI / WordPress latest + WooCommerce latest on PHP 8.3 (push) Successful in 1m0s
Release / release (push) Successful in 1m5s
Credit the plugin to its actual author, and 1.4.3
2026-09-16 11:00:47 +00:00
kristianwindandClaude Opus 5 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>
2026-09-16 12:59:44 +02:00
github-actions[bot] 426015bb12 Merge pull request #7 from kristianwind/bump-142
CI / Static checks (push) Successful in 13s
CI / WordPress latest + WooCommerce latest on PHP 8.3 (push) Successful in 1m3s
Release / release (push) Successful in 1m6s
1.4.2 — icon and a Check again that checks
2026-09-16 10:25:51 +00:00
9 changed files with 189 additions and 43 deletions
+5 -2
View File
@@ -1,10 +1,10 @@
=== WooCommerce Email Discount Manager === === WooCommerce Email Discount Manager ===
Contributors: wedsmp Contributors: kristianwind
Tags: woocommerce, discount, email, shipping, coupon Tags: woocommerce, discount, email, shipping, coupon
Requires at least: 5.8 Requires at least: 5.8
Tested up to: 7.1 Tested up to: 7.1
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.4.2 Stable tag: 1.4.3
WC requires at least: 6.0 WC requires at least: 6.0
WC tested up to: 11.1.0 WC tested up to: 11.1.0
License: GPLv2 or later License: GPLv2 or later
@@ -96,6 +96,9 @@ The most specific match wins: exact email, then domain, then partial match.
== Changelog == == Changelog ==
= 1.4.3 =
* The plugin is now credited to Kristian Wind, with a link to where its source lives. It previously carried a placeholder name.
= 1.4.2 = = 1.4.2 =
* The plugin now has an icon, so it no longer shows up as a generic grey plug under Dashboard → Updates. * The plugin now has an icon, so it no longer shows up as a generic grey plug under Dashboard → Updates.
* "Check again" on the updates screen really does check again. It previously reused a cached answer for up to six hours, which made a brand new version look like it had not been published. * "Check again" on the updates screen really does check again. It previously reused a cached answer for up to six hours, which made a brand new version look like it had not been published.
+8 -1
View File
@@ -18,7 +18,14 @@ while IFS= read -r line; do
[ -n "$line" ] && patterns+=("${line%/}") [ -n "$line" ] && patterns+=("${line%/}")
done < .distignore done < .distignore
git ls-files | while IFS= read -r file; do # -z, not plain `git ls-files`: git QUOTES any path containing a non-ASCII
# byte, so "TESTMILJØ.md" comes out as the literal 14-character string
# "TESTMILJ\303\230.md". That form matches no pattern in .distignore, so the
# file is kept — an internal document walking straight into the release zip
# with nothing said. (-z also covers paths with spaces.) Found in a sibling
# plugin that has exactly such a filename; this repo has none today, which is
# precisely why it would have sat here until someone added one.
git ls-files -z | while IFS= read -r -d '' file; do
keep=1 keep=1
for p in "${patterns[@]}"; do for p in "${patterns[@]}"; do
# A bare name excludes the file or directory wherever it appears, which is # A bare name excludes the file or directory wherever it appears, which is
+33 -15
View File
@@ -46,33 +46,51 @@ ci_plugin_header() {
"$(ci_repo_root)/$PLUGIN_MAIN" | head -1 | sed -E 's/[[:space:]]+$//' "$(ci_repo_root)/$PLUGIN_MAIN" | head -1 | sed -E 's/[[:space:]]+$//'
} }
# Run a shell script against a checkout of this repository, inside a container.
#
# The repository goes in over a tarball and `docker cp`, never a bind mount: a
# Gitea Actions job runs inside a container talking to the HOST's docker
# daemon, so a host path here lands on an empty directory nobody created and
# the check passes by checking nothing. The symptom reads like a typo in a path.
#
# Two details are load-bearing. The script is copied in as a FILE rather than
# interpolated into `sh -c`, so it can contain $, quotes and newlines without
# being mangled — which matters the moment it is more than one command. And
# --entrypoint sh is not decoration: some images (composer:2, for one) have an
# entrypoint that swallows its arguments.
#
# Taken from the subscriptions port, which needed it for composer.
ci_in_container() { # image script-text
local image="$1" script="$2"
local dir cid code=0
dir=$(mktemp -d -t wpci-XXXXXX)
( cd "$(ci_repo_root)" && git ls-files -z | tar --null -T - -cf "$dir/src.tar" )
printf '%s\n' "$script" > "$dir/run.sh"
cid=$(docker create --entrypoint sh -w /src "$image" \
-c 'mkdir -p /src && tar -xf /tmp/src.tar -C /src && exec sh /tmp/run.sh')
docker cp "$dir/src.tar" "$cid:/tmp/src.tar" >/dev/null
docker cp "$dir/run.sh" "$cid:/tmp/run.sh" >/dev/null
docker start -a "$cid" || code=$?
docker rm -f "$cid" >/dev/null
rm -rf "$dir"
return "$code"
}
# Run one of this repository's PHP scripts. # Run one of this repository's PHP scripts.
# #
# Uses the local php when there is one — a laptop, GitHub's hosted runners — # Uses the local php when there is one — a laptop, GitHub's hosted runners —
# and a container otherwise. The Gitea runner image has no php, and losing a # and a container otherwise. The Gitea runner image has no php, and losing a
# build to "php: command not found" after everything else passed is a silly way # build to "php: command not found" after everything else passed is a silly way
# to spend an afternoon. Measured: it happened on the very first Gitea run. # to spend an afternoon. Measured: it happened on the very first Gitea run.
#
# The repository goes in over a tarball and `docker cp`, never a bind mount: a
# Gitea Actions job runs inside a container talking to the HOST's docker
# daemon, so a host path here lands on an empty directory nobody created.
ci_php() { ci_php() {
if command -v php >/dev/null 2>&1; then if command -v php >/dev/null 2>&1; then
php "$@" php "$@"
return return
fi fi
local tarball cid code=0 ci_in_container "php:8.4-cli" "exec php $(printf '%q ' "$@")"
tarball=$(mktemp -t wpci-php-XXXXXX.tar)
( cd "$(ci_repo_root)" && git ls-files -z | tar --null -T - -cf "$tarball" )
cid=$(docker create "php:8.4-cli" sh -c \
"mkdir -p /src && tar -xf /tmp/src.tar -C /src && cd /src && exec php $(printf '%q ' "$@")")
docker cp "$tarball" "$cid:/tmp/src.tar" >/dev/null
docker start -a "$cid" || code=$?
docker rm -f "$cid" >/dev/null
rm -f "$tarball"
return "$code"
} }
ci_step() { printf '\n\033[1m→ %s\033[0m\n' "$*"; } ci_step() { printf '\n\033[1m→ %s\033[0m\n' "$*"; }
+28 -2
View File
@@ -76,11 +76,13 @@ before=$fails
if git ls-files -z '*.php' | xargs -0 grep -nP '<\?(?!php|=|xml)' ; then if git ls-files -z '*.php' | xargs -0 grep -nP '<\?(?!php|=|xml)' ; then
note_fail "Use <?php, not short open tags." note_fail "Use <?php, not short open tags."
fi fi
while read -r f; do # -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
[ -n "$f" ] || continue [ -n "$f" ] || continue
[ "$(tail -c 3 "$f" | tr -d '[:space:]')" = "?>" ] \ [ "$(tail -c 3 "$f" | tr -d '[:space:]')" = "?>" ] \
&& { ci_annotate error "$f" 1 "Remove the trailing ?> at the end of the file."; fails=$((fails + 1)); } && { ci_annotate error "$f" 1 "Remove the trailing ?> at the end of the file."; fails=$((fails + 1)); }
done <<< "$(git ls-files '*.php')" done < <(git ls-files -z '*.php')
[ "$fails" = "$before" ] && echo " clean" [ "$fails" = "$before" ] && echo " clean"
ci_step "text domain" ci_step "text domain"
@@ -141,6 +143,30 @@ for readme in readme.txt README.txt; do
|| note_fail "$readme has 'Stable tag: $stable' but the plugin header says $version." || note_fail "$readme has 'Stable tag: $stable' but the plugin header says $version."
done done
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
# nobody happens to look at that screen. This plugin shipped with
# "https://example.com/" as its Plugin URI for several releases.
#
# It catches the generic markers only. A made-up vendor name — this one went
# out as "WEDSMP" — looks exactly like a real one to any check, so an empty
# field is warned about but a wrong-but-plausible value cannot be.
before=$fails
for field in 'Plugin Name' 'Description' 'Author' 'Author URI' 'Plugin URI'; do
value=$(ci_plugin_header "$field")
if [ -z "$value" ]; then
echo "::warning::The plugin header has no '$field:'."
continue
fi
case "$value" in
*example.com*|*example.org*|*yourname*|*YOUR_*|*TODO*|*FIXME*|*changeme*|*'<'*'>'*)
note_fail "$field is still a placeholder: $value"
;;
esac
done
[ "$fails" = "$before" ] && echo " name, description, author and links are all filled in"
ci_step "the compatibility headers still tell the truth" ci_step "the compatibility headers still tell the truth"
# Advisory, never fatal. WordPress ships a new version whether or not anything # Advisory, never fatal. WordPress ships a new version whether or not anything
# here is wrong, and a build that goes red on release day for a header nobody # here is wrong, and a build that goes red on release day for a header nobody
+26 -5
View File
@@ -71,11 +71,14 @@ Three things in that file are load-bearing and none of them look it:
WordPress 5.8. Remove the header and a .org plugin that happens to share the WordPress 5.8. Remove the header and a .org plugin that happens to share the
slug could be pushed onto a shop running yours. slug could be pushed onto a shop running yours.
The answer is cached for six hours, which is right for the check WordPress runs 🔴 **Keep the feed cache short — WordPress is already the rate limiter.** It
twice a day and wrong for the **Check again** link — so `forget_on_force_check()` gates its own check by screen: one minute on Dashboard → Updates, one hour on
drops the cache on `force-check`. Without it, someone who has just published a the Plugins screen, 2–12 hours in the background. This filter only runs when
release clicks that link, sees nothing, and concludes the whole mechanism is that check runs, so any cache longer than that cadence simply hides new
broken. releases underneath it. Ours was six hours "because WordPress checks twice a
day anyway", and the symptom was a release going out and the shop being shown
nothing. It is fifteen minutes now, and `forget_on_force_check()` drops it
entirely on the **Check again** link.
**6. Make the Gitea repository public**, or point `Update URI` at a separate **6. Make the Gitea repository public**, or point `Update URI` at a separate
public releases-only repository. Every shop reads that feed without public releases-only repository. Every shop reads that feed without
@@ -105,6 +108,24 @@ it cannot.
- `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is - `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is
the only way to notice that a workflow stopped running on Gitea. the only way to notice that a workflow stopped running on Gitea.
## Running these locally: `git add` first
Every one of these scripts derives "what ships" from `git ls-files`, which
reads the **index**. An uncommitted file is therefore invisible to
`ci/dist-files.sh`, to the release zip, and to the copy that goes into the test
site.
That is deliberate — it is what catches a file that was written, referenced and
never committed, which is a release that is missing a file while every local
test passes because the file is sitting right there in the working tree. But it
also means running any of this before `git add` tests a release without your
newest file, and the failure is silent: the check does not complain, it just
measures something else. It cost two rounds here, once on
`includes/class-updater.php` and once on `assets/icon.svg`.
If a script reports something you just fixed as still missing, `git status`
before you debug the script.
## Two guards worth understanding before you move them ## Two guards worth understanding before you move them
**`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the **`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the
+15 -5
View File
@@ -185,11 +185,21 @@ the real list table and asserts the flag rather than trusting the reasoning.
### "Check again" really checks again ### "Check again" really checks again
The feed is cached for six hours. That is right for the check WordPress runs on The feed is cached for fifteen minutes, and the cache is dropped entirely when
its own twice a day, and wrong for a link whose entire promise is that it looks wp-admin is loaded with `force-check`.
*now* — so the cache is dropped when wp-admin is loaded with `force-check`.
Without that, publishing a release and then clicking **Check again** shows It was six hours, on the reasoning that WordPress checks twice a day anyway.
nothing for up to six hours, which reads exactly like a broken updater. That reasoning was wrong. WordPress gates its own check **by screen**:
| Screen | How often WordPress re-checks |
| --- | --- |
| Dashboard → Updates | every minute |
| Plugins | every hour |
| Background / cron | every 2–12 hours |
This filter only runs when that check runs, so WordPress is already the rate
limiter. A cache longer than its cadence does nothing but hide new releases
underneath it — a release published, and a shop shown nothing.
From the command line, `wp transient delete wc_edm_release_feed` does the same. From the command line, `wp transient delete wc_edm_release_feed` does the same.
+17 -3
View File
@@ -34,10 +34,24 @@ class WC_EDM_Updater {
const TRANSIENT = 'wc_edm_release_feed'; const TRANSIENT = 'wc_edm_release_feed';
/** /**
* How long that cache lives. WordPress checks for updates twice a day; a * How long that cache lives.
* shorter window would only add requests, a longer one would delay a fix. *
* Six hours, originally, on the reasoning that WordPress checks twice a
* day anyway. That reasoning was wrong, and the symptom was a shop that
* had just been told a new version existed and was shown nothing.
*
* WordPress does not check twice a day when somebody is looking. It gates
* its own check by screen — one minute on Dashboard → Updates, one hour on
* the Plugins screen, twelve hours in the background (wp_update_plugins(),
* switch on current_filter()). This filter only runs when that check runs,
* so WordPress is already the rate limiter, and a cache longer than its
* cadence does nothing but hide new releases underneath it.
*
* Fifteen minutes is left only to absorb a burst — several calls in one
* request, or other code calling wp_update_plugins() with extra stats,
* which bypasses the gate above.
*/ */
const TTL = 6 * HOUR_IN_SECONDS; const TTL = 15 * MINUTE_IN_SECONDS;
/** /**
* Hook into the update check for our own host only. * Hook into the update check for our own host only.
+53 -7
View File
@@ -14,7 +14,29 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
$wc_edm_failures = 0; /*
* The counter lives in $GLOBALS on purpose, and the four lines below prove it
* reaches the verdict before anything relies on it.
*
* `wp eval-file` runs this file through eval() INSIDE A METHOD, so the top
* level here is function scope, not global scope. A plain `$failures = 0` at
* the top and a `global $failures` in a helper below are therefore two
* different variables: the helpers increment the real global, the verdict at
* the bottom reads the local, and the local is always zero. Every assertion
* fails, every FAIL line prints, the file ends with "All assertions passed"
* and exits 0 — and CI goes green. That is not reasoning, it is what this file
* did until it was measured.
*
* This is the one file whose entire job is to be able to go red, so it checks
* that it can.
*/
$GLOBALS['wc_edm_failures'] = 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";
exit( 1 );
}
$GLOBALS['wc_edm_failures'] = 0;
/** /**
* Assert a condition and keep going, so one run reports every failure. * Assert a condition and keep going, so one run reports every failure.
@@ -24,14 +46,12 @@ $wc_edm_failures = 0;
* @param string $detail Shown when it fails. * @param string $detail Shown when it fails.
*/ */
function wc_edm_ok( $what, $condition, $detail = '' ) { function wc_edm_ok( $what, $condition, $detail = '' ) {
global $wc_edm_failures;
if ( $condition ) { if ( $condition ) {
printf( " ok %s\n", $what ); printf( " ok %s\n", $what );
return; return;
} }
++$wc_edm_failures; ++$GLOBALS['wc_edm_failures'];
printf( " FAIL %s%s\n", $what, '' !== $detail ? " — {$detail}" : '' ); printf( " FAIL %s%s\n", $what, '' !== $detail ? " — {$detail}" : '' );
} }
@@ -345,7 +365,18 @@ wc_edm_is( 'the current version is answered too, not passed over', WC_EDM_VERSIO
* WordPress does with our answer, and a test that needs the internet fails for * WordPress does with our answer, and a test that needs the internet fails for
* reasons that have nothing to do with it. * reasons that have nothing to do with it.
*/ */
// "Check again" must discard the six-hour cache, or the button lies: someone // The cache must not outlive WordPress's own check cadence. It gates by screen
// — one minute on Dashboard → Updates, one hour on the Plugins screen — and
// this filter only runs when that check runs, so anything longer here hides a
// new release underneath a cache the shop owner cannot see. This was six hours
// and produced exactly that: a release published, and a shop shown nothing.
wc_edm_ok(
'the cache is shorter than the interval WordPress re-checks on',
WC_EDM_Updater::TTL <= 15 * MINUTE_IN_SECONDS,
'TTL is ' . WC_EDM_Updater::TTL . 's; WordPress re-checks every 60s on Dashboard → Updates'
);
// "Check again" must discard the cache, or the button lies: someone
// who has just published a release clicks it, sees nothing, and concludes the // who has just published a release clicks it, sees nothing, and concludes the
// whole mechanism is broken. // whole mechanism is broken.
set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => '1.0.0', 'package' => 'x', 'url' => 'x', 'tested' => '' ), 600 ); set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => '1.0.0', 'package' => 'x', 'url' => 'x', 'tested' => '' ), 600 );
@@ -368,6 +399,21 @@ delete_transient( WC_EDM_Updater::TRANSIENT );
// strlen(null) deprecation from class-wp-plugins-list-table.php. It is // strlen(null) deprecation from class-wp-plugins-list-table.php. It is
// WordPress's, not ours, which is why the deprecation check filters on this // WordPress's, not ours, which is why the deprecation check filters on this
// plugin's directory rather than failing on everything it sees. // plugin's directory rather than failing on everything it sees.
// Primed again deliberately. The force-check assertions above deleted it, and
// without this offer_update() makes a real HTTP request — which passes only
// because this plugin's feed repo happens to be public. That would make the
// assertion measure the environment rather than the code under test.
set_transient(
WC_EDM_Updater::TRANSIENT,
array(
'version' => WC_EDM_VERSION,
'package' => 'https://gitea.nolimit.dk/kw/demo/releases/download/v1/demo.zip',
'url' => 'https://gitea.nolimit.dk/kw/demo',
'tested' => '7.1',
),
600
);
delete_site_transient( 'update_plugins' ); delete_site_transient( 'update_plugins' );
wp_update_plugins(); wp_update_plugins();
$transient = get_site_transient( 'update_plugins' ); $transient = get_site_transient( 'update_plugins' );
@@ -407,8 +453,8 @@ delete_transient( WC_EDM_Updater::TRANSIENT );
/* --------------------------------------------------------------- verdict -- */ /* --------------------------------------------------------------- verdict -- */
echo "\n"; echo "\n";
if ( $wc_edm_failures > 0 ) { if ( $GLOBALS['wc_edm_failures'] > 0 ) {
printf( "%d assertion(s) failed on WordPress %s.\n", $wc_edm_failures, get_bloginfo( 'version' ) ); printf( "%d assertion(s) failed on WordPress %s.\n", $GLOBALS['wc_edm_failures'], get_bloginfo( 'version' ) );
exit( 1 ); exit( 1 );
} }
printf( printf(
+4 -3
View File
@@ -4,8 +4,9 @@
* Plugin URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager * Plugin URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
* Update URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager * Update URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match). * Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
* Version: 1.4.2 * Version: 1.4.3
* Author: WEDSMP * Author: Kristian Wind
* Author URI: https://gitea.nolimit.dk/kw
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
* Text Domain: wc-email-discount * Text Domain: wc-email-discount
* Requires at least: 5.8 * Requires at least: 5.8
@@ -27,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'WC_EDM_VERSION', '1.4.2' ); define( 'WC_EDM_VERSION', '1.4.3' );
define( 'WC_EDM_PLUGIN_FILE', __FILE__ ); define( 'WC_EDM_PLUGIN_FILE', __FILE__ );
define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );