Skip to content

Add wp-env as an alternative local development environment#2

Open
dd32 wants to merge 43 commits intomasterfrom
add/claude/wp-env-setup
Open

Add wp-env as an alternative local development environment#2
dd32 wants to merge 43 commits intomasterfrom
add/claude/wp-env-setup

Conversation

@dd32
Copy link
Owner

@dd32 dd32 commented Feb 17, 2026

Summary

Adds @wordpress/env (wp-env) as a simpler alternative to the existing Docker Compose setup for local development. Both environments coexist — the .docker/ setup is unchanged.

What this PR does

  • wp-env configuration (.wp-env.json) — Multisite on port 80, plugins/themes from wordpress.org download URLs, directory mappings for mu-plugins/plugins/themes/sunrise files, and a lifecycle script for database seeding
  • Multi-network setup script (.wp-env/setup.mjs) — Idempotent afterStart lifecycle script that creates 3 WordPress networks (WordCamp, Events, Campus) with specific blog IDs matching production
  • Dynamic sunrise network detectionload_network_sunrise(), sunrise-events.php, and sunrise-wordcamp.php now detect the network from HTTP_HOST instead of static SITE_ID_CURRENT_SITE / WORDCAMP_NETWORK_ID constants. This is backward-compatible since get_domain_network_id() already handles both .test and .org domains
  • PHPUnit bootstrap wp-env support — Detects the wp-env environment (/var/www/html) and adjusts paths for plugins, vendor autoload, and WP test suite location
  • CI migration to wp-env — The unit-php GitHub Actions job now uses wp-env instead of manual MySQL service + install-wp-tests.sh. PHP version matrix is handled via .wp-env.override.json
  • Dependency cleanup — Removes plugins/themes from Composer that are now installed by wp-env (akismet, bbpress, classic-editor, edit-flow, gutenberg, jetpack, etc.). Removes supportflow entirely (deactivated network-wide). Removes all camptix-* payment gateways (not needed for local dev)

Sites available after npx wp-env start

URL Network Description
http://central.wordcamp.test/ WordCamp WordCamp Central
http://narnia.wordcamp.test/2026/ WordCamp Sample WordCamp
http://events.wordpress.test/ Events Events root
http://events.wordpress.test/rome/2024/training/ Events Sample event

Login: admin / password

Prerequisites (one-time)

Add to /etc/hosts:

127.0.0.1 wordcamp.test central.wordcamp.test narnia.wordcamp.test
127.0.0.1 events.wordpress.test campus.wordpress.test

Quick start

yarn                        # Install deps (includes @wordpress/env)
yarn workspaces run build   # Build JS assets
npx wp-env start            # Start environment + seed database

wp-env quirks and workarounds

This section documents the non-obvious wp-env behaviors discovered and the workarounds used. These are worth knowing for anyone maintaining or extending the wp-env setup.

1. WP_TESTS_DOMAIN gets the test port appended

wp-env automatically appends testsPort to WP_TESTS_DOMAIN, WP_SITEURL, and WP_HOME via appendPortToWPConfigs() in its post-processing. With testsPort: 8889, the test domain becomes localhost:8889 instead of localhost.

Only ports 80 and 443 are excluded from appending (see node_modules/@wordpress/env/lib/config/add-or-replace-port.js).

Impact: Tests that check domain names (e.g. Let's Encrypt test_get_domains) got localhost:8889 instead of a clean domain.

Workaround: phpunit-bootstrap.php defines WP_TESTS_DOMAIN, WP_SITEURL, and WP_HOME early (before wp-env's wp-tests-config.php loads) to override the port-appended values. We tried setting these to null in .wp-env.json's tests config, but wp-env validates config values and rejects null — only string | number | boolean are accepted.

2. wp-tests-config.php is generated from wp-config.php

wp-env generates wp-tests-config.php by copying wp-config.php and stripping the require wp-settings.php line (via sed). This means:

  • $table_prefix is wp_, not wptests_ as in standard WP test setups
  • All wp-env config constants (from .wp-env.json) are present in the test config
  • Constants defined in phpunit-bootstrap.php before the WP test suite loads take precedence over wp-tests-config.php values (first define() wins)

Impact: The subroles test hardcoded wptests_capabilities as the user meta key, which didn't match wp-env's wp_ prefix. Also, phpunit-bootstrap.php was defining WORDCAMP_ROOT_BLOG_ID=5 before wp-tests-config.php could set it to 1, causing tests that depend on blog IDs to fail.

Workaround:

  • Subroles test now uses $wpdb->get_blog_prefix() . 'capabilities' instead of a hardcoded key
  • phpunit-bootstrap.php skips defining bootstrap constants when running in wp-env (they come from wp-config.php with correct values)

3. No mu-plugins-private/ directory in wp-env

The 2-autoloader.php autoloader does require_once for files in mu-plugins-private/wporg-mu-plugins/pub-sync/utilities/. These private files don't exist in wp-env or CI.

Impact: Any test that triggers class_exists() for a WordPressdotorg\MU_Plugins\Utilities class would hit a fatal require_once error, even if the test had markTestSkipped() logic (the autoloader runs before the skip check).

Workaround: Added a file_exists() guard in 2-autoloader.php before require_once for the private path. This is safe for production since the file will always exist there.

4. Constants unavailable during mu-plugin loading

wp-env loads mu-plugins before wp-config.php constants are fully available in certain bootstrap phases (e.g. during wp-env start install). Several mu-plugins reference constants like WORDCAMP_ENVIRONMENT unconditionally.

Workaround: Created .wp-env/0-early-mu-plugin.php mapped to wp-content/mu-plugins/0-aaa-wp-env-constants.php (alphabetically first) that defines fallback values for required constants. Added defined() guards in mu-plugins that run early.

5. Multisite subdomain install + localhost = problems

wp-env's multisite uses localhost as the domain. Subdomain install with localhost doesn't work because *.localhost doesn't resolve. The actual multisite setup with custom domains (wordcamp.test, events.wordpress.test) requires the sunrise.php drop-in and /etc/hosts entries.

Workaround: The .wp-env/setup.mjs lifecycle script runs after wp-env start and creates the multi-network topology with proper custom domains via WP-CLI. The sunrise files handle routing requests to the correct network based on HTTP_HOST.

6. CI doesn't have /etc/hosts entries

In CI, there are no custom domain entries. Tests that rely on $_SERVER['HTTP_HOST'] being set to a specific domain need to set it themselves.

Workaround: Tests like test-sunrise-events.php explicitly set $_SERVER['HTTP_HOST'] before calling functions that depend on it. The test environment config uses BLOG_ID_CURRENT_SITE=1 and WORDCAMP_ROOT_BLOG_ID=1 (overriding the dev defaults of 5) so tests don't depend on specific blog IDs that require the full multi-network setup.


Test plan

  • npx wp-env start completes without errors
  • curl -I http://central.wordcamp.test/ returns 200
  • curl -I http://narnia.wordcamp.test/2026/ returns 200
  • curl -I http://events.wordpress.test/ returns 200
  • npx wp-env run cli wp site list shows all 4+ sites
  • npx wp-env run cli wp network list shows 3 networks
  • PHPUnit via wp-env: yarn wp-env:test:php passes
  • CI PHP tests pass on all matrix versions (8.1, 8.4, 8.5)
  • Existing Docker setup still works (no changes to .docker/)

🤖 Generated with Claude Code

dd32 and others added 10 commits February 17, 2026 17:28
Introduces @wordpress/env alongside the existing Docker setup, providing
a simpler way to run the multi-network WordCamp.org environment locally.

Key changes:
- Create .wp-env.json with multisite config, plugin slugs, and mappings
- Add afterStart lifecycle script to seed networks and sites via WP-CLI
- Make sunrise files detect network from HTTP_HOST instead of static
  constants, enabling wp-env (which uses a single wp-config.php) to
  serve all three networks correctly
- Update phpunit-bootstrap.php to detect wp-env paths
- Move wordpress.org plugins/themes from composer to wp-env management
- Add convenience yarn scripts for wp-env operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sunrise.php change to use HTTP_HOST-based network detection calls
get_domain_network_id() which relies on get_top_level_domain(), and
that function references the WORDCAMP_ENVIRONMENT constant. This
constant was not defined in the test bootstrap, causing failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all 8 CampTix payment gateway plugins from composer.json
(both require-dev entries and custom SVN package definitions) and
add them to .wp-env.json using their wordpress.org plugin slugs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Supportflow is explicitly deactivated network-wide in
wcorg-network-plugin-control.php and is not actively used. Remove
it entirely rather than migrating to wp-env.

Move tagregator from Composer to wp-env plugin slug.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PHP 8.5 is fully supported by the official WordPress Docker images
and has beta support in WordPress 6.9+. This matches the latest PHP
version tested in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the manual MySQL service, SVN, install-wp-tests.sh setup with
wp-env. The PHP version matrix is handled via .wp-env.override.json
which overrides the phpVersion per matrix entry.

This eliminates the need for the MySQL service container, SVN
installation, and the custom WP test suite installer script. wp-env
provides WordPress, the PHPUnit test suite, and MySQL out of the box.

The linter workflow is unchanged since it does not run WordPress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These payment gateway plugins are not needed for local development.
Code references to them can remain for production use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install bbpress from the latest wordpress.org release instead of
pinning to the 2.6 SVN branch via a custom Composer package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The wporg-profiles-wp-activity-notifier package is installed from
meta.svn.wordpress.org and requires SVN to be available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env requires full download URLs for wordpress.org plugins and
themes, not bare slugs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dd32 dd32 force-pushed the add/claude/wp-env-setup branch from 96242e6 to dc61918 Compare February 17, 2026 08:08
dd32 and others added 7 commits February 17, 2026 18:14
The p2 theme returns a 404 from downloads.wordpress.org. It is no
longer distributed as a standard theme download.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env runs wp core install before wp config set, so custom
constants like WORDCAMP_ENVIRONMENT are not defined when mu-plugins
first load during installation. Add defined() checks to prevent
fatal errors during wp-env startup.

Also add file_exists() check for pub-sync/loader.php which may not
exist in all environments (e.g. wp-env where mu-plugins-private is
not mapped).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env runs wp core install before wp config set, so custom constants
are not available when mu-plugins first load during installation. This
early-loading mu-plugin provides safe defaults that prevent fatal errors
during the initial WordPress bootstrap.

In production and Docker environments where constants are already
defined in wp-config.php, this file is a no-op.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env starts a full WordPress instance that loads mu-plugins, which
require built JS/CSS assets (blocks, virtual-embeds). Add a build
step before starting wp-env to generate these artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During wp-env initial install, WordPress loads without multisite since
those constants are not yet in wp-config.php. This means multisite
functions like get_site_meta() are undefined. Adding MULTISITE and
SUBDOMAIN_INSTALL as fallback constants ensures WordPress loads the
multisite function stack during initial bootstrap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env runs wp core install before wp config set, and MULTISITE must
be in wp-config.php before wp-settings.php processes the multisite
check. Since mu-plugins load after that check, fallback constants in
a mu-plugin cannot make multisite functions available during the
initial WordPress installation.

The original CI approach (MySQL service + install-wp-tests.sh) avoids
this entirely because PHPUnit loads WordPress via its own bootstrap
where all constants are pre-defined.

Also remove MULTISITE/SUBDOMAIN_INSTALL from the fallback constants
since they have no effect as a mu-plugin (too late in load order).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dd32 and others added 11 commits February 17, 2026 21:26
Instead of a fallback constants mu-plugin (0-aaa-constants.php), guard
individual files against missing constants during wp-env initial
bootstrap (wp core install runs before wp config set):

- sunrise.php: return early if WORDCAMP_ENVIRONMENT is not defined
- cron.php, service-worker-caching.php: add defined() checks
- load-other-mu-plugins.php: add file_exists() for pub-sync/loader.php
  and defined() checks for network constants

Move WORDCAMP_ENVIRONMENT to last in .wp-env.json config so all other
constants are set in wp-config.php first.

Switch CI from manual MySQL + install-wp-tests.sh to wp-env. PHP
version matrix handled via .wp-env.override.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since sunrise loads before mu-plugins, defining WORDCAMP_ENVIRONMENT
there is sufficient. All other constants are already in wp-config.php
because WORDCAMP_ENVIRONMENT is set last in the wp-env config.

Also disable plugin loading during initial setup via filters, and
revert the defensive defined() checks in mu-plugins since they are
no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wp-env runs wp core install before wp config set, so custom constants
aren't in wp-config.php when mu-plugins first load. Instead of adding
defensive checks to production code, map a wp-env-only mu-plugin from
.wp-env/0-early-mu-plugin.php that defines safe defaults.

The file sorts first alphabetically (0-aaa-*) so all constants are
available before other mu-plugins load. In production, this file does
not exist.

Simplify the sunrise.php early return to just define WORDCAMP_ENVIRONMENT
(the only constant sunrise itself needs) since the mu-plugin handles
the rest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move mu-plugin file mapping after directory mapping in .wp-env.json
  so the directory mount doesn't overwrite the individual file.
- Add file_exists guard for mu-plugins-private/wporg-mu-plugins/pub-sync
  which doesn't exist in wp-env environments.
- Fix PHPCS doc comment capitalization in 0-early-mu-plugin.php.
- Add retry to wp-env start for transient 429 rate limiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change require_once to include_once for pub-sync/loader.php so missing
  private repo files emit warnings instead of fatals.
- Add wp_installing() guard to wcorg_enforce_public_blog_option() to
  prevent calling get_site_meta() during initial install when multisite
  functions are not available yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Jetpack Custom CSS module references JETPACK__VERSION which is not
available during wp core install since plugins load after mu-plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During wp core install, mu-plugins load but many multisite functions
are not yet available. Skip loading all sub-folder mu-plugins during
install to prevent cascading fatal errors from init hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tests wp-env instance installs WordPress at localhost, not
wordcamp.test. Override DOMAIN_CURRENT_SITE and blog IDs to match
the default multisite install state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SUBDOMAIN_INSTALL=true is incompatible with localhost domain. Override
to false for the tests environment. Also fix retry logic to destroy
the wp-env instance before retrying to avoid EACCES permission errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tests environment uses localhost which does not support subdomain
multisite. Move SUBDOMAIN_INSTALL=true to the development env config
so it only applies to dev, not tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensure both environments default to path-based multisite. The dev
environment overrides this to true for subdomain support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dd32 and others added 15 commits February 18, 2026 09:33
wp-env multisite install is incompatible with localhost domain when
SUBDOMAIN_INSTALL is true. Since the WordPress PHPUnit test suite
handles multisite via WP_TESTS_MULTISITE, disable wp-env multisite
for the tests environment entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The top-level `multisite: true` was bleeding MULTISITE constant into the
tests environment, causing database table errors during wp-env start.
Moving multisite-specific settings (SUNRISE, SUBDOMAIN_INSTALL,
DOMAIN_CURRENT_SITE, etc.) to env.development ensures the tests env
gets a clean single-site install.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tests env runs as single-site (with WP_TESTS_MULTISITE for PHPUnit).
During wp-env wp config set commands, WordPress loads in single-site
mode where multisite functions like get_site_meta() are unavailable.

- Add is_multisite() check to load-other-mu-plugins.php
- Add is_multisite() check to wcorg_enforce_public_blog_option()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- setup.mjs: catch wp site list error in non-multisite environments
- CI: fix wp-env destroy confirmation (pipe yes instead of --yes flag)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep multisite:true and all config at top level for local dev.
CI overrides multisite:false at top level - the multisite config
constants in wp-config.php are harmless without MULTISITE defined.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WordPress's is_multisite() returns true if SUNRISE or SUBDOMAIN_INSTALL
are defined (even as false), regardless of the MULTISITE constant.
Having these in .wp-env.json config means they get added to wp-config.php
for ALL environments via wp config set, making both dev and tests think
they're multisite even when multisite:false is set via override.

Moving these to the lifecycle script means they're only set when multisite
is actually available (the script exits early for single-site).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The vendor/ mapping and mu-plugins/vendor/ point to the same Docker
mount. Using different paths causes require_once to load the autoloader
twice. Also guard constant definitions to avoid warnings when wp-env
has already set them via wp-config.php.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The vendor mapping created two paths to the same autoloader files,
causing Composer's autoloader class to be declared twice. Instead of
mapping vendor separately, invoke phpunit from its actual location
in mu-plugins/vendor/bin/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mu-plugins-private directory is not available in wp-env. Guard the
require_once with file_exists to prevent fatal errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inside the wp-env container, content is at /var/www/html/wp-content/
not /var/www/html/public_html/wp-content/. Remove the public_html/
prefix from all test directory paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fixes

- Skip bootstrap constants in wp-env (let wp-config.php set correct values)
- Override WP_TESTS_DOMAIN/WP_SITEURL/WP_HOME to prevent wp-env port append
- Add file_exists() guard in autoloader for missing private mu-plugins
- Use dynamic table prefix in SubRoles test instead of hardcoded 'wptests_'
- Set HTTP_HOST in sunrise-events test for get_redirect_url()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant