- CRITICAL: This repository is now Woo Tax-first; active maintenance and new development focus on tax functionality.
- CRITICAL: Shipping functionality is deprecated for new installs.
- CRITICAL: Shipping label purchase functionality remains supported only for grandfathered installs that purchased at least one label before deprecation.
- CRITICAL: Live rates remain supported only for grandfathered accounts.
- MUST NOT introduce new shipping product surface area for non-grandfathered installs.
- PHP plugin bootstrap:
woocommerce-services.php - PHP classes:
classes/andsrc/ - JS client code:
client/ - Build artifacts:
dist/ - Tests:
tests/
- Node/npm:
npm install,npm run dist,npm run test-client,npm run eslint - Composer:
composer install,composer test,composer check-all(PHPCS — see Linting)
- CRITICAL: This repo is tied to an old Node runtime; use
.nvmrc(10.18.1) for local work unless a migration is explicitly planned. - CRITICAL: The old Node requirement is intentional because this project depends on a forked legacy Calypso package in the
wp-calypsosubmodule (package name:wp-calypso). - CRITICAL: Always run
source ~/.nvm/nvm.sh && nvm usebefore any command that invokes Node (e.g.,npm install,npm run,git push— the pre-push hook runsnpm test). Without this, tests will fail with Node version incompatibilities. - MUST NOT perform incidental Node/toolchain upgrades while making feature or bugfix changes.
npm install
composer install
npm run dist
npm run test-client
npm run eslint
composer test # PHPUnit; auto-checks the test env and repairs only what's broken
composer test:setup # one-shot: bring up the Dockerized DB + install WP/WC test deps
composer check-all # PHPCS (see Linting below)composer testis the single entry point. It runstests/bin/run-tests.sh, which health-checks the environment (tests/bin/check-test-env.sh) and repairs only the components that fail their probe before running PHPUnit. No manual setup step is needed on a healthy machine.- Output format:
composer testuses PHPUnit's compact progress output when its stdout is captured (CI logs, pre-commit hooks, AI agents) and the readable--testdoxformat at an interactive terminal. Force it either way withTESTDOX=1/TESTDOX=0(e.g.TESTDOX=1 composer test). - The test DB is a throwaway MariaDB container defined in
tests/docker-compose.yml(published on127.0.0.1:4416; credentials intests/test.env). Docker is required; a local mysql client is not (the installer materialises shims that proxy through the container when no client is present). composer test:setupinstalls/repairs every missing component; for a full rebuild that also wipes the DB volume runbash tests/bin/install-wc-tests.sh --force.- WP/WC are installed under the system temp dir (honors
TMPDIR). The WC clone builds with its own modern Node via the WC checkout's.nvmrc; this is independent of the plugin's own pinned Node (.nvmrc= 10.18.1) and PHPUnit needs no Node at all. - By default the installer clones the latest WooCommerce release, whereas CI tests
the 3 latest WC minors — so a local
composer testcan run against a different WC than the merge gate. To reproduce a specific version, pin it on a forced (re)install:WC_VERSION=9.4.0 bash tests/bin/install-wc-tests.sh --force(EXPECTED_WP_VERSIONpins WordPress the same way).
The PHPCS ruleset is split into concern-specific files, all importing .phpcs.common.xml (shared file/exclude/arg/config). Run check-all (the phpcs.xml.dist aggregator) before pushing, or a single scope while iterating:
composer run check-all # Aggregator (phpcs.xml.dist): runs security + identity + php together
composer run check-php # Generic PHP rules only (WC-Core + WP-Core/Extra/Docs, minus security & i18n)
composer run check-php:fix # Auto-fix for the check-php scope
composer run check-security # Security sniffs (escaping, sanitization, nonces, eval)
composer run check-identity # Plugin identity: text_domain (i18n) + global prefix whitelistThe husky pre-commit hook (bin/wc-phpcbf.sh via lint-staged) auto-fixes staged PHP using the same phpcs.xml.dist aggregator, so pre-commit fixes stay aligned with check-all.
The test-env scripts (tests/bin/*.sh) and hook helpers (bin/*.sh) run on Linux/CI (GNU userland) and on contributors' machines (stock macOS ships BSD grep/sed). CI is GNU-only, so BSD-only breakage never shows up in the merge gate — keep both in mind when editing any shell script:
- Use POSIX character classes, not GNU regex shorthands:
[[:space:]]/[[:digit:]]/[[:alnum:]]instead of\s/\d/\w. BSDgrep -E/sed -Etreat\sas a literals, so patterns silently stop matching — this cost us a full-reinstall misfire in thewp-tests-config.phpDB_HOST probe. sed -iis not portable: GNU issed -i, BSD needs a backup suffix (sed -i .bak). Branch on[[ "$(uname -s)" == 'Darwin' ]]and remove the.bakafterward — seerepair_wp_config()ininstall-wc-tests.sh.- Prefer ERE (
grep -E/sed -E) with POSIX quantifiers; avoid GNU-only escapes like\+,\?,\|in BRE. When a script carries regex, test it on macOS or reason through the BSD case before merging.
- Base new behavior on tax-only mode rules and existing shipping eligibility checks.
- Keep shipping logic gated by eligibility; preserve grandfathered behavior.
- Keep changes in source files; do not hand-edit generated build outputs in
dist/. - Follow existing WordPress/WooCommerce coding standards and linting rules in this repository.
Any change to a public or externally exposed class, interface, function, or method signature is high-risk and must state its backward-compatibility impact in the PR description - regardless of which namespace the symbol lives in. The WC_Connect_* classes live in the global namespace and are referenced by third-party code; treat all of them as externally exposed. Newer code under Automattic\WCServices\* is not automatically safe either: a modern namespace is a code-organization choice, not a privacy guarantee, and anything reachable from outside can already have consumers.
Treat a symbol as externally exposed when it is implemented or consumed outside this repository - by other plugins, themes, or site snippets - even if it looks internal. When in doubt, assume it is exposed and state the BC impact.
Adding a method to an interface that external code can implement must be flagged explicitly. It is a backward-incompatible change: existing implementers fatal on load because they no longer satisfy the contract. Likewise, removing a required method from an interface is breaking for existing implementers (they carry a now-dead method, which static analysis such as PHPStan will flag). Prefer a non-breaking alternative: add the method to the concrete class rather than the interface, introduce a separate new interface, or supply a default implementation via an abstract base class.
Deprecate, don't rename. For existing public symbols (classes, interfaces, methods, constants, hooks), never rename or remove them in place. Mark the old symbol @deprecated, introduce the replacement alongside it, and keep both working through a deprecation window so external consumers have time to migrate.
This rule exists because WooCommerce 10.9.0 was reverted on WP Cloud: a required method added to a published interface fataled every older WooCommerce Stripe Gateway version that implemented it. The same failure mode applies to any contract this plugin publishes.
Deprecation also cuts the other way here: because shipping is compatibility-only for grandfathered installs, removing or narrowing an existing shipping surface is a BC change for those stores, not cleanup. See Product Scope.
WordPress exposes more contracts than class and function signatures. The following are equally binding: a change to any of them is high-risk and requires the same backward-compatibility impact statement in the PR description.
Hooks and filters are public contracts. Every do_action and apply_filters call is an interface that third-party callbacks depend on, including this plugin's wc_connect_*, wc_services_*, wcservices_*, and wcship_* hooks. Removing a hook, renaming it, or removing/reordering its arguments breaks every attached callback. Changing when or whether a hook fires can break consumers that depend on its timing. Additive is the safe path: append new arguments at the end, never remove or reorder existing ones. To retire a hook, fire it through do_action_deprecated() / apply_filters_deprecated() for a deprecation window instead of deleting it.
Do not assume global state. Tax calculation is reachable from the cart and checkout, the Store API, the REST API, cron, WP-CLI, and webhooks, and not all of those set the globals a front-end request does ($post, $wp_query, an initialized session or cart). A newly introduced read of a global, or of WC()->... state, in a path reachable outside a standard request is a fatal or a silent misbehavior in the contexts that do not set it - REST order updates are a repeat offender in this repo. Guard the exact dependency explicitly: use function_exists/class_exists for symbols, isset for variables, did_action for lifecycle state, and verify that WC() and the required component are initialized before dereferencing WC()->....
Do not assume single-site. Multisite changes where data lives: site-scoped vs network-scoped options (get_option vs get_site_option), per-site tables, user roles and capabilities, and upload paths all differ. This plugin's settings, service schemas, and connection state are site-scoped, so a network-activated install holds one set per site. A change that reads or writes site state must state in its PR whether it behaves correctly under multisite - and if it was not tested there, say so explicitly.
Do not assume install layout. WordPress could be configured to run in a subdirectory, with relocated wp-content, and behind reverse proxies. Never build paths or URLs by concatenation from the domain root; derive them (plugins_url(), plugin_dir_path(), wp_upload_dir(), and mind the home_url() vs site_url() distinction). This matters for dist/ asset URLs and for any store URL sent to the Connect server. A path that works on a root install and breaks elsewhere is a compatibility bug, not an edge case.
- Identify the contract you are touching: signature, hook, global/scope expectation, site topology, or install layout.
- Assume unseen consumers. You cannot enumerate third-party code; if the surface is reachable from outside this plugin, someone consumes it.
- Prefer the additive path (new optional method, appended hook argument, new symbol + deprecation) over changing what exists.
- State the impact in the PR description: what changed, who could consume it, and why it is safe or what the deprecation path is.
- If you cannot establish the impact, stop and flag it to the user as needing review.
has_only_tax_functionality()andshould_load_shipping_features()inwoocommerce-services.phpare core gates for tax-only vs shipping-enabled behavior.- Shipping behavior must remain compatibility-only for eligible legacy stores.
- Tax behavior is the primary development path.
- Accidentally enabling shipping UI/features for non-grandfathered stores.
- Regressing legacy label purchase support for eligible installs.
- Treating shipping migrations as feature expansion instead of compatibility maintenance.
- Use
create-prfor PR preparation workflows. See.agents/skills/create-pr/SKILL.md. - Use
write-changelogfor changelog entry workflows. See.agents/skills/write-changelog/SKILL.md.