Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Env: Add exec Command #41180

Closed
wants to merge 13 commits into from
33 changes: 32 additions & 1 deletion packages/env/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ module.exports = function cli() {
default: false,
} );

// Make sure any unknown arguments are passed to the command as arguments.
// This allows options to be passed to "run" and "exec" without being quoted.
yargs.parserConfiguration( { 'unknown-options-as-args': true } );

yargs.command(
'start',
wpGreen(
Expand Down Expand Up @@ -171,7 +175,7 @@ module.exports = function cli() {
describe: 'The container to run the command on.',
} );
args.positional( 'command', {
type: 'string',
type: 'array',
describe: 'The command to run.',
} );
},
Expand All @@ -189,6 +193,33 @@ module.exports = function cli() {
'$0 run tests-cli bash',
'Open a bash session in the WordPress tests instance.'
);

yargs.command(
'exec <environment> <script> [script-args...]',
'Runs the selected script in a given environment. All arguments after the script param are passed along.',
( args ) => {
args.positional( 'environment', {
type: 'string',
describe: 'Which environment to execute the script in.',
choices: [ 'development', 'tests' ],
} );
args.positional( 'script', {
type: 'string',
describe: 'Which script to execute.',
} );
args.positional( 'script-args', {
type: 'array',
describe: 'The arguments to be passed to the script.',
default: [],
} );
},
withSpinner( env.exec )
);
yargs.example(
'$0 exec tests test:unit --filter=packages',
'Runs the script defined for `test:unit` in the tests environment with the `--filter=packages` argument.'
);

yargs.command(
'destroy',
wpRed(
Expand Down
67 changes: 67 additions & 0 deletions packages/env/lib/commands/exec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Internal dependencies
*/
const initConfig = require( '../init-config' );
const spawnDockerComposeRunCommand = require( '../spawn-docker-compose-run-command' );

/**
* @typedef {import('../config').Config} Config
*/

/**
* Runs a script in the given environment.
*
* @param {Object} options
* @param {string} options.environment The environment to run the script in.
* @param {string[]} options.script The script to run.
* @param {string[]} options.scriptArgs The arguments to pass to the script.
* @param {boolean} options.debug True if debug mode is enabled.
* @param {Object} options.spinner A CLI spinner which indicates progress.
*/
module.exports = async function run( {
environment,
script,
scriptArgs,
spinner,
debug,
} ) {
const config = await initConfig( { spinner, debug } );

// We want the script from the correct environment so that we can execute it.
const envConfig = config.env[ environment ];
noahtallen marked this conversation as resolved.
Show resolved Hide resolved
if ( ! envConfig ) {
spinner.fail(
`The environment '${ environment }' could not be found.`
);
return;
}
const execScript = envConfig.scripts[ script ];
if ( ! execScript ) {
spinner.fail(
`The environment '${ environment }' has no script named '${ script }'.`
);
return;
}

// Figure out the working directory for the command to be executed in.
let workingDir = execScript.cwd;
if ( workingDir && ! workingDir.startsWith( '/' ) ) {
workingDir = '/var/www/html/' + workingDir;
noahtallen marked this conversation as resolved.
Show resolved Hide resolved
}

// We're going to run the script in the seelcted container.
ObliviousHarmony marked this conversation as resolved.
Show resolved Hide resolved
const container = environment === 'tests' ? 'tests-wordpress' : 'wordpress';
const spawnCommand = [ execScript.script, ...scriptArgs ];
ObliviousHarmony marked this conversation as resolved.
Show resolved Hide resolved

spinner.info( `Starting script "${ script }" in "${ environment }".` );

await spawnDockerComposeRunCommand(
config,
container,
workingDir,
spawnCommand,
spinner
);

spinner.text = `Ran \`${ script }\` in '${ environment }'.`;
};
2 changes: 2 additions & 0 deletions packages/env/lib/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const start = require( './start' );
const stop = require( './stop' );
const clean = require( './clean' );
const run = require( './run' );
const exec = require( './exec' );
const destroy = require( './destroy' );
const logs = require( './logs' );
const installPath = require( './install-path' );
Expand All @@ -14,6 +15,7 @@ module.exports = {
stop,
clean,
run,
exec,
destroy,
logs,
installPath,
Expand Down
74 changes: 13 additions & 61 deletions packages/env/lib/commands/run.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
/**
* External dependencies
*/
const { spawn } = require( 'child_process' );

/**
* Internal dependencies
*/
const initConfig = require( '../init-config' );
const spawnDockerComposeRunCommand = require( '../spawn-docker-compose-run-command' );

/**
* @typedef {import('../config').Config} Config
Expand All @@ -24,76 +20,32 @@ const initConfig = require( '../init-config' );
module.exports = async function run( { container, command, spinner, debug } ) {
const config = await initConfig( { spinner, debug } );

command = command.join( ' ' );

// Shows a contextual tip for the given command.
showCommandTips( command, container, spinner );
showCommandTips( container, command, spinner );

await spawnCommandDirectly( {
await spawnDockerComposeRunCommand(
config,
container,
null,
command,
spinner,
config,
} );
spinner
);

spinner.text = `Ran \`${ command }\` in '${ container }'.`;
};

/**
* Runs an arbitrary command on the given Docker container.
*
* @param {Object} options
* @param {string} options.container The Docker container to run the command on.
* @param {string} options.command The command to run.
* @param {Config} options.config The wp-env configuration.
* @param {Object} options.spinner A CLI spinner which indicates progress.
*/
function spawnCommandDirectly( { container, command, config, spinner } ) {
const composeCommand = [
'-f',
config.dockerComposeConfigPath,
'run',
'--rm',
container,
...command.split( ' ' ), // The command will fail if passed as a complete string.
];

return new Promise( ( resolve, reject ) => {
// Note: since the npm docker-compose package uses the -T option, we
// cannot use it to spawn an interactive command. Thus, we run docker-
// compose on the CLI directly.
const childProc = spawn(
'docker-compose',
composeCommand,
{
stdio: 'inherit',
shell: true,
},
spinner
);
childProc.on( 'error', reject );
childProc.on( 'exit', ( code ) => {
// Code 130 is set if the user tries to exit with ctrl-c before using
// ctrl-d (so it is not an error which should fail the script.)
if ( code === 0 || code === 130 ) {
resolve();
} else {
reject( `Command failed with exit code ${ code }` );
}
} );
} );
}

/**
* This shows a contextual tip for the command being run. Certain commands (like
* bash) may have weird behavior (exit with ctrl-d instead of ctrl-c or ctrl-z),
* so we want the user to have that information without having to ask someone.
*
* @param {string} command The command for which to show a tip.
* @param {string} container The container the command will be run on.
* @param {Object} spinner A spinner object to show progress.
* @param {string} container The container the command will be run on.
* @param {string[]} command The command for which to show a tip.
* @param {Object} spinner A spinner object to show progress.
*/
function showCommandTips( command, container, spinner ) {
function showCommandTips( container, command, spinner ) {
command = command.join( ' ' );

if ( ! command.length ) {
return;
}
Expand Down
24 changes: 17 additions & 7 deletions packages/env/lib/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ const md5 = require( '../md5' );
* Base-level config for any particular environment. (development/tests/etc)
*
* @typedef WPServiceConfig
* @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.<string, WPSource>} 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 {?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.<string, WPSource>} 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 {Object.<string, WPServiceScript>} scripts Scripts that can be executed in the environment.
*/

/**
Expand All @@ -53,6 +54,14 @@ const md5 = require( '../md5' );
* @property {string} basename Name that identifies the WordPress installation, plugin or theme.
*/

/**
* A script that can be executed in an environment.
*
* @typedef WPServiceScript
* @property {string} script The script to execute.
* @property {?string} cwd The working directory that the script should be ran in.
*/

/**
* Reads, parses, and validates the given .wp-env.json file into a wp-env config
* object for internal use.
Expand Down Expand Up @@ -94,6 +103,7 @@ module.exports = async function readConfig( configPath ) {
port: 8889,
},
},
scripts: {},
};

// The specified base configuration from .wp-env.json or from the local
Expand Down
12 changes: 12 additions & 0 deletions packages/env/lib/config/parse-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ module.exports = function parseConfig( config, options ) {
},
{}
),
scripts: Object.entries( config.scripts ).reduce(
ObliviousHarmony marked this conversation as resolved.
Show resolved Hide resolved
( result, [ command, script ] ) => {
if ( typeof script === 'string' ) {
result[ command ] = { script };
} else {
result[ command ] = script;
}

return result;
},
{}
),
};
};

Expand Down
2 changes: 2 additions & 0 deletions packages/env/lib/config/test/__snapshots__/config.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Object {
"phpVersion": null,
"pluginSources": Array [],
"port": 2000,
"scripts": Object {},
"themeSources": Array [],
},
"tests": Object {
Expand All @@ -49,6 +50,7 @@ Object {
"phpVersion": null,
"pluginSources": Array [],
"port": 1000,
"scripts": Object {},
"themeSources": Array [],
},
},
Expand Down
Loading