diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 602530c541d4f..b70e8fdd0dd12 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -8,6 +8,13 @@ - Automatically add the environment's port to `WP_TESTS_DOMAIN`. - `run` command now has a `--env-cwd` option to set the working directory in the container for the command to execute from. +### New feature + +- Create a `postInstall` property in `.wp-env.json` files for setting arbitrary commands to run after configuring WordPress. +- Add a `WP_ENV_POST_INSTALL` environment variable to override the current `postInstall` configuration. +- Run the `postInstall` command on `wp-env start` when WordPress is being configured. This can happen when your config changes, WordPress updates, or you pass the `--update` flag. +- Run the `postInstall` command on `wp-env clean` when given the `--post-install` option. + ## 5.16.0 (2023-04-12) ## 5.15.0 (2023-03-29) diff --git a/packages/env/README.md b/packages/env/README.md index 7a22dfe88428c..8c223fe60a5fa 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -270,23 +270,27 @@ The start command installs and initializes the WordPress environment, which incl ```sh wp-env start -Starts WordPress for development on port 8888 (override with WP_ENV_PORT) and -tests on port 8889 (override with WP_ENV_TESTS_PORT). The current working -directory must be a WordPress installation, a plugin, a theme, or contain a -.wp-env.json file. After first install, use the '--update' flag to download -updates to mapped sources and to re-apply WordPress configuration options. +Starts WordPress for development on port 8888 (​http://localhost:8888​) +(override with WP_ENV_PORT) and tests on port 8889 (​http://localhost:8889​) +(override with WP_ENV_TESTS_PORT). The current working directory must be a +WordPress installation, a plugin, a theme, or contain a .wp-env.json file. After +first install, use the '--update' flag to download updates to mapped sources and +to re-apply WordPress configuration options. Options: - --help Show help [boolean] - --version Show version number [boolean] - --debug Enable debug output. [boolean] [default: false] - --update Download source updates and apply WordPress configuration. + --help Show help [boolean] + --version Show version number [boolean] + --debug Enable debug output. [boolean] [default: false] + --post-install Execute any configured post-install command when configuring + WordPress. [boolean] [default: true] + --update Download source updates and apply WordPress configuration. [boolean] [default: false] - --xdebug Enables Xdebug. If not passed, Xdebug is turned off. If no modes - are set, uses "debug". You may set multiple Xdebug modes by passing - them in a comma-separated list: `--xdebug=develop,coverage`. See - https://xdebug.org/docs/all_settings#mode for information about - Xdebug modes. [string] + --xdebug Enables Xdebug. If not passed, Xdebug is turned off. If no + modes are set, uses "debug". You may set multiple Xdebug modes + by passing them in a comma-separated list: + `--xdebug=develop,coverage`. See + https://xdebug.org/docs/all_settings#mode for information + about Xdebug modes. [string] ``` ### `wp-env stop` @@ -307,6 +311,13 @@ Cleans the WordPress databases. Positionals: environment Which environments' databases to clean. [string] [choices: "all", "development", "tests"] [default: "tests"] + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --debug Enable debug output. [boolean] [default: false] + --post-install Execute the environments' configured post-install command if + configured. [boolean] [default: false] ``` ### `wp-env run [container] [command]` @@ -461,16 +472,17 @@ You can customize the WordPress installation, plugins and themes that the develo `.wp-env.json` supports fields for options applicable to both the tests and development instances. -| Field | Type | Default | Description | -| -------------- | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | -| `"phpVersion"` | `string\|null` | `null` | The PHP version to use. If `null` is specified, `wp-env` will use the default version used with production release of WordPress. | -| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | -| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | -| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | -| `"testsPort"` | `integer` | `8889` | The port number for the test site. You'll access the instance through the port: 'http://localhost:8889'. | -| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | -| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | +| Field | Type | Default | Description | +| --------------- | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | +| `"phpVersion"` | `string\|null` | `null` | The PHP version to use. If `null` is specified, `wp-env` will use the default version used with production release of WordPress. | +| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | +| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | +| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | +| `"testsPort"` | `integer` | `8889` | The port number for the test site. You'll access the instance through the port: 'http://localhost:8889'. | +| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | +| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | +| `"postInstall"` | `string\|null` | `null` | The command that should be executed after configuring WordPress. | _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PORT`) take precedent over the .wp-env.json values._ @@ -668,6 +680,26 @@ You can tell `wp-env` to use a specific PHP version for compatibility and testin } ``` +#### Post-Install Command + +You can define a command for `wp-env` to run after configuring WordPress. This will happen the first time you run `wp-env start`, whenever you change the configuration, +and when you use the `--update` option. You can also instruct `wp-env` to run the command on `wp-env clean` with the `--post-install` option. +The `WP_ENV_POST_INSTALL` environment variable can be used to override the configured command. + +```json +{ + "postInstall": "echo 'Running Post-Install'" +} +``` + +Additional environment variables are given to the command to provide context about the execution: + +- `WP_ENV_POST_INSTALL_CONTEXT` contains the command that triggered the execution, either `start` or `clean`. +- `WP_ENV_POST_INSTALL_ENVIRONMENT` contains the WordPress environment that the command is being executed for, such as `development` or `tests`. +- `WP_ENV_POST_INSTALL_DEBUG` contains a boolean indicating whether or not `wp-env` is in debug mode. + +> **Warning:** This command can be executed frequently. It should be resilient and not fail if a step has already been completed. For example, you should take care not to add data to the database if it was already done. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js index 68316855c6bed..27b087b1709d4 100644 --- a/packages/env/lib/cli.js +++ b/packages/env/lib/cli.js @@ -43,6 +43,10 @@ const withSpinner = // Error is a validation error. That means the user did something wrong. spinner.fail( error.message ); process.exit( 1 ); + } else if ( error instanceof env.PostInstallError ) { + // Error is a post-install command error. This means their command failed and they should be notified. + spinner.fail( error.message ); + process.exit( 1 ); } else if ( error && typeof error === 'object' && @@ -112,6 +116,12 @@ module.exports = function cli() { ) }} (override with WP_ENV_TESTS_PORT). The current working directory must be a WordPress installation, a plugin, a theme, or contain a .wp-env.json file. After first install, use the '--update' flag to download updates to mapped sources and to re-apply WordPress configuration options.` ), ( args ) => { + args.option( 'post-install', { + type: 'boolean', + describe: + 'Execute any configured post-install command when configuring WordPress.', + default: true, + } ); args.option( 'update', { type: 'boolean', describe: @@ -145,6 +155,12 @@ module.exports = function cli() { choices: [ 'all', 'development', 'tests' ], default: 'tests', } ); + args.option( 'post-install', { + type: 'boolean', + describe: + "Execute the environments' configured post-install command if configured.", + default: false, + } ); }, withSpinner( env.clean ) ); diff --git a/packages/env/lib/commands/clean.js b/packages/env/lib/commands/clean.js index adde9d072d9eb..85e103aeafe6d 100644 --- a/packages/env/lib/commands/clean.js +++ b/packages/env/lib/commands/clean.js @@ -19,10 +19,16 @@ const { configureWordPress, resetDatabase } = require( '../wordpress' ); * * @param {Object} options * @param {WPEnvironmentSelection} options.environment The environment to clean. Either 'development', 'tests', or 'all'. + * @param {boolean} options.postInstall Indicates whether or not we should run the post-install command. * @param {Object} options.spinner A CLI spinner which indicates progress. * @param {boolean} options.debug True if debug mode is enabled. */ -module.exports = async function clean( { environment, spinner, debug } ) { +module.exports = async function clean( { + environment, + postInstall, + spinner, + debug, +} ) { const config = await initConfig( { spinner, debug } ); const description = `${ environment } environment${ @@ -42,7 +48,13 @@ module.exports = async function clean( { environment, spinner, debug } ) { if ( environment === 'all' || environment === 'development' ) { tasks.push( resetDatabase( 'development', config ) - .then( () => configureWordPress( 'development', config ) ) + .then( () => + configureWordPress( + 'development', + config, + postInstall ? 'clean' : null + ) + ) .catch( () => {} ) ); } @@ -50,7 +62,13 @@ module.exports = async function clean( { environment, spinner, debug } ) { if ( environment === 'all' || environment === 'tests' ) { tasks.push( resetDatabase( 'tests', config ) - .then( () => configureWordPress( 'tests', config ) ) + .then( () => + configureWordPress( + 'tests', + config, + postInstall ? 'clean' : null + ) + ) .catch( () => {} ) ); } diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index a217d5492d2b4..c0002864088a9 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -40,12 +40,19 @@ const CONFIG_CACHE_KEY = 'config_checksum'; * Starts the development server. * * @param {Object} options - * @param {Object} options.spinner A CLI spinner which indicates progress. - * @param {boolean} options.debug True if debug mode is enabled. - * @param {boolean} options.update If true, update sources. - * @param {string} options.xdebug The Xdebug mode to set. + * @param {boolean} options.postInstall Indicates whether or not we should skip the post-install command. + * @param {Object} options.spinner A CLI spinner which indicates progress. + * @param {boolean} options.debug True if debug mode is enabled. + * @param {boolean} options.update If true, update sources. + * @param {string} options.xdebug The Xdebug mode to set. */ -module.exports = async function start( { spinner, debug, update, xdebug } ) { +module.exports = async function start( { + spinner, + postInstall, + debug, + update, + xdebug, +} ) { spinner.text = 'Reading configuration.'; await checkForLegacyInstall( spinner ); @@ -178,12 +185,30 @@ module.exports = async function start( { spinner, debug, update, xdebug } ) { // Retry WordPress installation in case MySQL *still* wasn't ready. await Promise.all( [ - retry( () => configureWordPress( 'development', config, spinner ), { - times: 2, - } ), - retry( () => configureWordPress( 'tests', config, spinner ), { - times: 2, - } ), + retry( + () => + configureWordPress( + 'development', + config, + postInstall ? 'start' : null, + spinner + ), + { + times: 2, + } + ), + retry( + () => + configureWordPress( + 'tests', + config, + postInstall ? 'start' : null, + spinner + ), + { + times: 2, + } + ), ] ); // Set the cache key once everything has been configured. diff --git a/packages/env/lib/config/config.js b/packages/env/lib/config/config.js index 4018830f853a0..8c0aa7f1836ee 100644 --- a/packages/env/lib/config/config.js +++ b/packages/env/lib/config/config.js @@ -34,13 +34,15 @@ const md5 = require( '../md5' ); * Base-level config for any particular environment. (development/tests/etc) * * @typedef WPServiceConfig + * @property {number} port The port to use. + * @property {string} phpVersion Version of PHP to use in the environments, of the format 0.0. + * * @property {WPSource} coreSource The WordPress installation to load in the environment. * @property {WPSource[]} pluginSources Plugins to load in the environment. * @property {WPSource[]} themeSources Themes to load in the environment. - * @property {number} port The port to use. * @property {Object} config Mapping of wp-config.php constants to their desired values. * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. - * @property {string} phpVersion Version of PHP to use in the environments, of the format 0.0. + * @property {string|null} postInstall The command to run after configuring WordPress. */ /** @@ -104,6 +106,7 @@ module.exports = async function readConfig( configPath ) { WP_SITEURL: 'http://localhost', WP_HOME: 'http://localhost', }, + postInstall: null, env: { development: {}, // No overrides needed, but it should exist. tests: { @@ -293,6 +296,11 @@ function withOverrides( config ) { addConfigPort( 'WP_SITEURL' ); addConfigPort( 'WP_HOME' ); + if ( process.env.WP_ENV_POST_INSTALL ) { + config.env.development.postInstall = process.env.WP_ENV_POST_INSTALL; + config.env.tests.postInstall = process.env.WP_ENV_POST_INSTALL; + } + return config; } diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index 9528ac8b3528c..a5724bacdb145 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -56,6 +56,7 @@ module.exports = async function parseConfig( config, options ) { }, {} ), + postInstall: config.postInstall, }; }; diff --git a/packages/env/lib/config/test/__snapshots__/config.js.snap b/packages/env/lib/config/test/__snapshots__/config.js.snap index c7bcdbd94d7ff..217482f6747ee 100644 --- a/packages/env/lib/config/test/__snapshots__/config.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config.js.snap @@ -25,6 +25,7 @@ exports[`readConfig config file should match snapshot 1`] = ` "phpVersion": null, "pluginSources": [], "port": 2000, + "postInstall": null, "themeSources": [], }, "tests": { @@ -47,6 +48,7 @@ exports[`readConfig config file should match snapshot 1`] = ` "phpVersion": null, "pluginSources": [], "port": 1000, + "postInstall": null, "themeSources": [], }, }, diff --git a/packages/env/lib/config/validate-config.js b/packages/env/lib/config/validate-config.js index 74a5fb79b5deb..583eb753d0238 100644 --- a/packages/env/lib/config/validate-config.js +++ b/packages/env/lib/config/validate-config.js @@ -86,6 +86,15 @@ function validateConfig( config, envLocation ) { checkValidURL( envPrefix, config.config, 'WP_SITEURL' ); checkValidURL( envPrefix, config.config, 'WP_HOME' ); + if ( + config.postInstall !== null && + typeof config.postInstall !== 'string' + ) { + throw new ValidationError( + `Invalid .wp-env.json: "${ envPrefix }postInstall" must be null or a string.` + ); + } + return config; } diff --git a/packages/env/lib/env.js b/packages/env/lib/env.js index 3942a30710937..0f0f5459f9bab 100644 --- a/packages/env/lib/env.js +++ b/packages/env/lib/env.js @@ -2,10 +2,12 @@ /** * Internal dependencies */ -const { ValidationError } = require( './config' ); const commands = require( './commands' ); +const { ValidationError } = require( './config' ); +const { PostInstallError } = require( './wordpress' ); module.exports = { ...commands, ValidationError, + PostInstallError, }; diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js index fc8f6db567077..4c2ef059765a4 100644 --- a/packages/env/lib/wordpress.js +++ b/packages/env/lib/wordpress.js @@ -6,6 +6,7 @@ const util = require( 'util' ); const fs = require( 'fs' ).promises; const path = require( 'path' ); const got = require( 'got' ); +const { execSync } = require( 'child_process' ); /** * Promisified dependencies @@ -20,6 +21,11 @@ const copyDir = util.promisify( require( 'copy-dir' ) ); * @typedef {'development'|'tests'|'all'} WPEnvironmentSelection */ +/** + * Error subtype which indicates that the post install command failed. + */ +class PostInstallError extends Error {} + /** * Checks a WordPress database connection. An error is thrown if the test is * unsuccessful. @@ -39,11 +45,17 @@ async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) { * activating all plugins, and activating the first theme. These steps are * performed sequentially so as to not overload the WordPress instance. * - * @param {WPEnvironment} environment The environment to configure. Either 'development' or 'tests'. - * @param {WPConfig} config The wp-env config object. - * @param {Object} spinner A CLI spinner which indicates progress. + * @param {WPEnvironment} environment The environment to configure. Either 'development' or 'tests'. + * @param {WPConfig} config The wp-env config object. + * @param {string|null} postInstallContext The context that will be set when executing postInstall command. When null, postInstall won't run. + * @param {Object} spinner A CLI spinner which indicates progress. */ -async function configureWordPress( environment, config, spinner ) { +async function configureWordPress( + environment, + config, + postInstallContext, + spinner +) { const installCommand = `wp core install --url="${ config.env[ environment ].config.WP_SITEURL }" --title="${ config.name }" --admin_user=admin --admin_password=password --admin_email=wordpress@example.com --skip-email`; // -eo pipefail exits the command as soon as anything fails in bash. @@ -105,6 +117,36 @@ async function configureWordPress( environment, config, spinner ) { log: config.debug, } ); + + // At this point, WordPress has been configured and the environment should be ready. + // We're going to support a "postInstall" configuration option so that users can + // have the environment arbitrarily execute command for additional setup. + if ( postInstallContext && config.env[ environment ].postInstall ) { + try { + let output = execSync( config.env[ environment ].postInstall, { + encoding: 'utf-8', + stdio: 'pipe', + env: { + ...process.env, + WP_ENV_POST_INSTALL_CONTEXT: postInstallContext, + WP_ENV_POST_INSTALL_ENVIRONMENT: environment, + WP_ENV_POST_INSTALL_DEBUG: config.debug, + }, + } ); + + // Remove any trailing whitespace for nicer output. + output = output.trimRight(); + + // We don't need to bother with any output if there isn't any! + if ( output ) { + spinner.info( `${ environment } Post-Install:\n${ output }` ); + } + } catch ( e ) { + throw new PostInstallError( + `${ environment } Post-Install:\n${ e.stderr }` + ); + } + } } /** @@ -284,6 +326,7 @@ async function getLatestWordPressVersion() { } module.exports = { + PostInstallError, hasSameCoreSource, checkDatabaseConnection, configureWordPress,