Local development environments for WordPress.org projects, powered by wp-env.
On macOS, use Colima. It's free and open source and needs no account.
brew install colima docker docker-buildx docker-compose
colima start --cpu 4 --memory 4Homebrew installs Buildx and Compose as Docker CLI plugins outside the CLI's search path. On Apple Silicon, point the CLI at them by adding this property to ~/.docker/config.json, keeping whatever else is already in the file:
"cliPluginsExtraDirs": [ "/opt/homebrew/lib/docker/cli-plugins" ]Colima remembers the CPU and memory settings, so later starts are just colima start. To start it at login instead, run brew services start colima.
On Windows, consider using Rancher Desktop with its dockerd (moby) engine, or Docker Desktop. On Linux, use Docker Engine.
From the environments/ directory:
npm installA local instance of the WordPress.org Plugin Directory with the plugin directory plugin, theme, and supporting mu-plugins.
Start:
npm run plugins:env startImport a plugin by slug:
npm run plugins:import -- akismetRe-seed plugins (clears import flag, then re-imports):
npm run plugins:refreshAccess: http://localhost:8888
WP CLI:
npx wp-env run cli wp <command>Run tests:
npm run plugins:testA local instance of the WordPress.org Theme Directory with the theme directory plugin, Theme Check, the wporg-themes-2024 frontend theme, and supporting mu-plugins. Themes are imported from the live WordPress.org themes API.
Start:
npm run themes:env startRe-import themes (on demand, without clearing existing data):
npm run themes:importRe-seed themes (clears import flag, then re-imports):
npm run themes:refreshAccess: http://localhost:8888
WP CLI:
npm run themes:env -- run cli -- wp <command>Run tests:
npm run themes:testA local instance of jobs.wordpress.net with the JobsWP plugin, theme, sample job categories, and sample job posts.
Start:
npm run jobs:env startAccess: http://localhost:8888
WP CLI:
npm run jobs:env -- run cli -- wp <command>Run tests (the JobsWP plugin's PHPUnit suite, in a dedicated test environment):
npm run jobs:testA local instance of browsehappy.com with the theme.
Start:
npm run browsehappy:env startAccess: http://localhost:8888
A local instance of translate.wordpress.org with GlotPress, the wporg-gp-* plugins active on production, and the pub/wporg theme.
Start:
npm run translate:env startFirst start auto-imports hello-dolly (plugin) and twentytwenty (theme) so the WordPress Plugins and WordPress Themes project containers have real fixtures. It also seeds a few demo Translation Events (active, upcoming, past, and draft) with hosts and attendees.
Access: http://localhost:8888
Users: admin / password is a GlotPress global administrator, so it can approve translations everywhere and never sees a permission check fail. translator / password is a plain subscriber with no GlotPress permissions — use it to check what a contributor sees, such as suggestions going to waiting instead of current. The dev login button fills in admin, so type the contributor credentials by hand.
Import a plugin or theme's translations on demand:
npm run translate:import -- plugin akismet
npm run translate:import -- theme twentytwentyfourSeed demo events on demand (idempotent):
npm run translate:seed-eventsRe-seed (clears the seed flag so the next start re-imports fixtures):
npm run translate:refreshWP CLI:
npm run translate:env -- run cli -- wp <command>Run tests (the Translation Events plugin's PHPUnit suite, in a dedicated test environment):
npm run translate:testLocal overrides: create translate/.wp-env.override.json (git-ignored) to override config values like WP_HOME / WP_SITEURL for testing behind a custom hostname.
Translation Events 2024 design: the events routes render the legacy templates unless the new block theme is enabled. To preview it, add "config": { "TRANSLATION_EVENTS_NEW_DESIGN": true } to translate/.wp-env.override.json and restart.
A local instance of the WordPress.org Support Forums with bbPress, the wporg-support-2024 theme, and the wporg-bbp-* supporting plugins. It runs as a multisite network, because the forums read the plugin and theme directories out of sibling sub-sites.
Start:
npm run support:env startAccess: http://localhost:8888
Re-seed (clears the seed flag, then re-runs the seed):
npm run support:refreshWP CLI:
npm run support:env -- run cli -- wp <command>Add --url=localhost:8888/rosetta (or /plugins, /themes) to target a sub-site.
The network:
| Blog | Path | Purpose |
|---|---|---|
| 1 | / |
The global support forums — the site under development |
| 2 | /plugins |
Plugin Directory, a dependency of the /plugin/<slug>/ forum views |
| 3 | /themes |
Theme Directory, a dependency of the /theme/<slug>/ forum views |
| 4 | /rosetta |
A locale ("Rosetta") forum, for the locale-only code paths |
The blog IDs are pinned in .wp-env.json (WPORG_PLUGIN_DIRECTORY_BLOGID and friends), so the seed creates the sub-sites in that order and fails loudly if they come out differently.
Note that the plugin and theme directories here exist only as forum dependencies. Use the Plugin Directory and Theme Directory environments to work on the directories themselves.
On production each locale forum is its own network with IS_ROSETTA_NETWORK defined, which a single wp-config.php cannot express. mocks/mu-plugins/wporg-support-env.php defines it for the blog named by WPORG_LOCAL_ROSETTA_BLOGID instead.
Forum IDs: the Plugins, Themes and Reviews forums are created as the post IDs that Plugin::PLUGINS_FORUM_ID and Support_Compat::HIDDEN_FORUMS hard-code for production (21261, 21262, 21272, plus two legacy IDs). The directory compat views, the review forum, and the hidden-forum filtering all key off those, so they cannot be left to auto-increment.
What to try: hello-dolly is seeded on the plugin directory with a committer, a contributor and a support rep; twentytwentyfour is seeded on the theme directory with an author. Visit /plugin/hello-dolly/, /theme/twentytwentyfour/, and either followed by reviews/. Ratings submitted through the review form persist in the local ratings table, so the star filters and rating edits work.
Local boundaries. The environment deliberately does not reach production:
- Badge assignments would otherwise be a live POST to
profiles.wordpress.org.Badge_Automationregisters its hooks locally, becauseassign_badge()lives in the mountedmu-plugins/pub/profile-helpers.php, andProfiles\queue()dispatches synchronously for anything that is notproductionwhileapi()only redirects the URL forstaging.mocks/mu-plugins/wporg-profiles-local.phpanswers those requests and records the associations in local tables. - Outbound mail is short-circuited; the forums mail on subscriptions, moderation and reports.
WPORG_Ratingsis a local stand-in.Ratings_Compatguards onclass_exists()alone, so the stub implements every method it calls rather than a subset.- SSO, two-factor account management and the rest of the Profiles service are not reproduced.
User accounts:
All accounts use the password password.
| Username | Role on / |
Role on /rosetta |
Notes |
|---|---|---|---|
admin |
Network administrator | Network administrator | Full network admin access |
keymaster |
bbp_keymaster |
bbp_participant |
Top-level forum admin; can manage all forum content |
moderator |
bbp_moderator |
bbp_participant |
Can moderate topics and replies |
rosettakeymaster |
bbp_participant |
bbp_keymaster |
Keymaster of the locale forum |
rosettamoderator |
bbp_participant |
bbp_moderator |
Moderator of the locale forum |
pluginauthor |
bbp_participant |
bbp_participant |
Committer on the seeded Hello Dolly plugin |
plugincontributor |
bbp_participant |
bbp_participant |
Contributor on the seeded Hello Dolly plugin |
pluginsupport |
bbp_participant |
bbp_participant |
Support rep on the seeded Hello Dolly plugin |
themeauthor |
bbp_participant |
bbp_participant |
Author of the seeded Twenty Twenty-Four theme |
themesupport |
bbp_participant |
bbp_participant |
Theme support rep; no production equivalent yet |
visitor |
bbp_participant |
bbp_participant |
Regular forum visitor |
Block editor: the forums use the block editor through Blocks Everywhere, which is not installed here — it needs a Gutenberg old enough to break against WordPress trunk until Automattic/blocks-everywhere#211 lands. Plugin::__construct() loads the block support only when that plugin is present, so the environment runs on bbPress' plain editor until then.
A test-only environment for the shared single sign-on code in common/includes/wporg-sso/. The SSO is a library rather than a plugin, so it is mounted at wp-content/wporg-sso instead of being activated, and its PHPUnit suite runs from there.
WP_ENVIRONMENT_TYPE is set to production so the SSO uses the hosts it uses in production (login.wordpress.org and friends) rather than the shortcuts it takes on local installs.
Run tests:
npm run sso:testThe Handbook plugin has its own .wp-env.json in wordpress.org/public_html/wp-content/plugins/handbook/.
Start:
cd wordpress.org/public_html/wp-content/plugins/handbook
npx wp-env startRun tests: use the test environment in this directory instead — it starts a dedicated instance and runs the suite in one step:
npm run handbook:testA local FreeScout with the modules from freescout.wordpress.net/. FreeScout isn't WordPress, so this environment uses Docker Compose directly instead of wp-env.
Start:
npm run freescout:startThen open http://127.0.0.1:8890 and log in with WordPress.org: a mock login page lets you pick an account, e.g. admin, which is connected to the FreeScout admin. Its other accounts cover new users (reviewer), no two-factor authentication (no2fa), and blocked accounts (blocked). Replies sent to senders land in Mailpit at http://127.0.0.1:8891.
What's running:
| Service | Purpose |
|---|---|
app |
FreeScout's latest release on nginx and PHP-FPM 8.4, with the scheduler and queue worker production would run from cron. |
db |
MariaDB 11.4, with a separate freescout-test database for the tests. |
greenmail |
Mail server FreeScout fetches the Plugins and Themes mailboxes from, over IMAP. Two sample emails are delivered on first start. |
mailpit |
Catches outgoing mail. |
mock-api |
Stands in for api.wordpress.org/dotorg/freescout/: checks request signatures, answers sidebar requests with panels recorded from the real endpoints for a real account (obenland's), and logs webhook events (npm run freescout:logs). Also stands in for login.wordpress.org's SAML identity provider at http://127.0.0.1:8892/idp, signing with a key that's only for local development. |
freescout.wordpress.net/Modules/ is mounted as FreeScout's Modules/ folder, like production's checkout. Every start switches all modules on and runs freescout:after-app-update, as production does after a deploy. After switching branches, run npm run freescout:setup: FreeScout errors on every page while it still has a removed module cached.
Setup also adds sample data, set up like the HelpScout helpdesk but with made-up people and conversations. It adds the Learn WordPress mailbox, without incoming email. It also adds four made-up agents and 39 made-up conversations in Plugins, Themes and Learn WordPress from the last eight weeks, with replies, notes, and closed, pending, and spam conversations. Five of the Plugins ones are plugin reviews, one for each state of the Plugin Review panel: issues waiting for an update, a rename, an ownership check, an update uploaded, and an approval. It only adds what's missing, and only to a FreeScout on 127.0.0.1 or localhost.
Premium modules: to try them, copy them in from a clone of the private freescout-paid-modules repository. They must never be committed here, so exclude each one first. From environments/:
paid=~/path/to/freescout-paid-modules
exclude="$(git rev-parse --path-format=absolute --git-common-dir)/info/exclude"
for module in "$paid"/Modules/*/; do
target="freescout.wordpress.net/Modules/$(basename "$module")"
grep -qxF "$target/" "$exclude" || echo "$target/" >> "$exclude"
rsync -a --delete "$module" "../$target/"
done
npm run freescout:setupSetup switches them on, and adds sample data for each one that's there, with HelpScout's names:
- Tags: some of HelpScout's tags and colors, on conversations.
- Custom Fields: the Learn WordPress fields, with values. Plugins and Themes have none.
- Saved Replies: some of HelpScout's saved replies in Plugins and Themes, with short made-up texts.
- Teams: the Plugin, Themes and Learn teams, with conversations assigned to some. The admin is in Plugin Reviews, so they get the Plugin Review panel. Names longer than FreeScout's 20 characters are shortened.
- Custom Folders: "New WordPress" in Plugins, for replies to new WordPress version emails.
- Mentions: a note in which one agent mentions another.
- Workflows: some of HelpScout's workflows, with made-up rules. Automatic ones only run for new email with matching subjects, or a tag an agent adds; none run for the sample conversations.
- Reports: response and resolution times, ready right away.
- API & Webhooks: a read-only API key for the admin, and a webhook for new conversations to the mock API, which turns it away.
Slack stays unconfigured: it needs a Slack app.
To send more mail in:
npm run freescout:env -- exec app bash -c 'printf "Subject: Hi\r\n\r\nHello\r\n" | curl -s --url smtp://greenmail:3025 --mail-from someone@example.org --mail-rcpt plugins@wordpress.test --upload-file -'Real WordPress.org data: with access to the helpdesk's API secret (FREESCOUT_SECRET on api.wordpress.org), the sidebar can show real WordPress.org data instead of the mock's. Store it in the macOS Keychain once: paste the secret when asked (twice). Pasting it there keeps it out of your shell history; add -U to replace a stored secret.
security add-generic-password -a "$USER" -s wporg-freescout-api -wThen start FreeScout against api.wordpress.org:
WPORG_API_URL=https://api.wordpress.org/dotorg/freescout/ WPORG_API_SECRET="$(security find-generic-password -s wporg-freescout-api -w)" WPORG_USERNAME=<your username> npm run freescout:startThis is production data, so WPOrgWebhooks stays off: local conversations would count toward production's contributor stats. The mock's accounts don't exist there, so add WPORG_USERNAME=<your username> to connect the FreeScout admin to your own account, and type that username at the mock login. Logging in syncs the admin's name and email from WordPress.org. Run npm run freescout:start without the variables to go back to the mock; the tests always use their own values.
If your hosts file points api.wordpress.org at a sandbox, FreeScout reaches it without your proxy and gets a 403; comment the entry out while testing.
Run tests:
npm run freescout:testOther FreeScout versions: FREESCOUT_REF takes any FreeScout branch or release tag, e.g. FREESCOUT_REF=master npm run freescout:start. Each gets its own database and storage; pass the same FREESCOUT_REF to stop or destroy it. Artisan: npm run freescout:artisan -- <command>. Stop / destroy: npm run freescout:stop, npm run freescout:destroy.
# Stop an environment (replace plugins with jobs, etc.)
npm run plugins:env stop
npm run jobs:env stop
# Destroy an environment (removes all data)
npm run plugins:env destroy
npm run jobs:env destroy
# View logs
npm run plugins:env logs
npm run jobs:env logsAll commands should be run from the environments/ directory unless otherwise noted.