diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9ab00c2a5fa1..a51262c2ff014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -941,8 +941,11 @@ importers: projects/js-packages/jetpack-cli: dependencies: chalk: - specifier: ^4.1.2 - version: 4.1.2 + specifier: ^5.4.1 + version: 5.4.1 + dotenv: + specifier: ^16.3.1 + version: 16.4.7 prompts: specifier: ^2.4.2 version: 2.4.2 @@ -9588,6 +9591,10 @@ packages: resolution: {integrity: sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==} engines: {node: '>=12'} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dunder-proto@1.0.0: resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==} engines: {node: '>= 0.4'} @@ -21746,6 +21753,8 @@ snapshots: dotenv@16.0.2: {} + dotenv@16.4.7: {} + dunder-proto@1.0.0: dependencies: call-bind-apply-helpers: 1.0.1 diff --git a/projects/js-packages/jetpack-cli/.gitattributes b/projects/js-packages/jetpack-cli/.gitattributes new file mode 100644 index 0000000000000..992b114f7ffa8 --- /dev/null +++ b/projects/js-packages/jetpack-cli/.gitattributes @@ -0,0 +1,6 @@ +# Files not needed to be distributed in the package. +.gitattributes export-ignore +node_modules export-ignore + +# Files to exclude from the mirror repo +/changelog/** production-exclude diff --git a/projects/js-packages/jetpack-cli/.gitignore b/projects/js-packages/jetpack-cli/.gitignore new file mode 100644 index 0000000000000..140fd587d2d52 --- /dev/null +++ b/projects/js-packages/jetpack-cli/.gitignore @@ -0,0 +1,2 @@ +vendor/ +node_modules/ diff --git a/projects/js-packages/jetpack-cli/CHANGELOG.md b/projects/js-packages/jetpack-cli/CHANGELOG.md new file mode 100644 index 0000000000000..721294abd00ad --- /dev/null +++ b/projects/js-packages/jetpack-cli/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + diff --git a/projects/js-packages/jetpack-cli/README.md b/projects/js-packages/jetpack-cli/README.md new file mode 100644 index 0000000000000..71453fa986d24 --- /dev/null +++ b/projects/js-packages/jetpack-cli/README.md @@ -0,0 +1,18 @@ +# Jetpack + + +## How to install Jetpack plugin on your site +### Installation From Git Repo + +## Contribute + +## Get Help + +## Security + +Need to report a security vulnerability? Go to [https://automattic.com/security/](https://automattic.com/security/) or directly to our security bug bounty site [https://hackerone.com/automattic](https://hackerone.com/automattic). + +## License + +Licensed under [GNU General Public License v2 (or later)](./LICENSE.txt). + diff --git a/projects/js-packages/jetpack-cli/bin/jp.js b/projects/js-packages/jetpack-cli/bin/jp.js index b955faeae8120..4c98478ae1975 100755 --- a/projects/js-packages/jetpack-cli/bin/jp.js +++ b/projects/js-packages/jetpack-cli/bin/jp.js @@ -1,12 +1,18 @@ #!/usr/bin/env node import { spawnSync } from 'child_process'; -import fs from 'fs'; +import fs, { readFileSync } from 'fs'; import { dirname, resolve } from 'path'; import process from 'process'; +import { fileURLToPath } from 'url'; import chalk from 'chalk'; +import dotenv from 'dotenv'; import prompts from 'prompts'; +// Get package.json path relative to this file +const __dirname = dirname( fileURLToPath( import.meta.url ) ); +const packageJson = JSON.parse( readFileSync( resolve( __dirname, '../package.json' ), 'utf8' ) ); + /** * Check if a directory is the monorepo root. * @@ -45,7 +51,6 @@ const findMonorepoRoot = startDir => { * @throws {Error} If clone fails */ const cloneMonorepo = async targetDir => { - // eslint-disable-next-line no-console console.log( chalk.blue( 'Cloning Jetpack monorepo...' ) ); const result = spawnSync( 'git', @@ -83,15 +88,15 @@ const initJetpack = async () => { try { await cloneMonorepo( targetDir ); - // eslint-disable-next-line no-console + console.log( chalk.green( '\nJetpack monorepo has been cloned successfully!' ) ); - // eslint-disable-next-line no-console + console.log( '\nNext steps:' ); - // eslint-disable-next-line no-console + console.log( '1. cd', response.directory ); - // eslint-disable-next-line no-console + console.log( '2. jp docker up' ); - // eslint-disable-next-line no-console + console.log( '3. jp docker install' ); } catch ( error ) { throw new Error( `Failed to initialize Jetpack: ${ error.message }` ); @@ -103,6 +108,12 @@ const main = async () => { try { const args = process.argv.slice( 2 ); + // Handle version flag + if ( args[ 0 ] === '--version' || args[ 0 ] === '-v' ) { + console.log( chalk.green( packageJson.version ) ); + return; + } + // Handle 'init' command specially if ( args[ 0 ] === 'init' ) { await initJetpack(); @@ -113,19 +124,145 @@ const main = async () => { const monorepoRoot = findMonorepoRoot( process.cwd() ); if ( ! monorepoRoot ) { - // eslint-disable-next-line no-console console.error( chalk.red( 'Could not find Jetpack monorepo.' ) ); - // eslint-disable-next-line no-console + console.log( '\nTo get started:' ); - // eslint-disable-next-line no-console + console.log( '1. Run', chalk.blue( 'jp init' ), 'to clone the repository' ); - // eslint-disable-next-line no-console + console.log( ' OR' ); - // eslint-disable-next-line no-console + console.log( '2. Navigate to an existing Jetpack monorepo directory' ); throw new Error( 'Monorepo not found' ); } + // Handle docker commands on the host + if ( args[ 0 ] === 'docker' ) { + // Commands that should run in the container + const containerCommands = [ 'build-image', 'install' ]; + if ( containerCommands.includes( args[ 1 ] ) ) { + const result = spawnSync( + resolve( monorepoRoot, 'tools/docker/bin/monorepo' ), + [ 'pnpm', 'jetpack', ...args ], + { + stdio: 'inherit', + shell: true, + cwd: monorepoRoot, + } + ); + + if ( result.status !== 0 ) { + throw new Error( `Command failed with status ${ result.status }` ); + } + return; + } + + // Run config generation first if this is an 'up' command + if ( args[ 1 ] === 'up' ) { + // Create required directories + fs.mkdirSync( resolve( monorepoRoot, 'tools/docker/data/jetpack_dev_mysql' ), { + recursive: true, + } ); + fs.mkdirSync( resolve( monorepoRoot, 'tools/docker/data/ssh.keys' ), { recursive: true } ); + fs.mkdirSync( resolve( monorepoRoot, 'tools/docker/wordpress' ), { recursive: true } ); + + const images = [ + { name: 'mariadb:lts' }, + { name: 'automattic/jetpack-wordpress-dev:latest' }, + { name: 'phpmyadmin/phpmyadmin:latest', platform: 'linux/amd64' }, + { name: 'maildev/maildev', platform: 'linux/amd64' }, + { name: 'atmoz/sftp', platform: 'linux/amd64' }, + ]; + + for ( const image of images ) { + const inspect = spawnSync( 'docker', [ 'image', 'inspect', image.name ], { + stdio: 'ignore', + } ); + if ( inspect.status !== 0 ) { + console.log( chalk.blue( `Pulling ${ image.name }...` ) ); + const pullArgs = [ 'pull', image.name ]; + if ( image.platform ) { + pullArgs.splice( 1, 0, '--platform', image.platform ); + } + const pull = spawnSync( 'docker', pullArgs, { stdio: 'inherit' } ); + if ( pull.status !== 0 ) { + throw new Error( `Failed to pull ${ image.name }` ); + } + } + } + + const configResult = spawnSync( + resolve( monorepoRoot, 'tools/docker/bin/monorepo' ), + [ 'pnpm', 'jetpack', 'docker', 'config' ], + { + stdio: 'inherit', + shell: true, + cwd: monorepoRoot, + } + ); + + if ( configResult.status !== 0 ) { + throw new Error( 'Failed to generate Docker config' ); + } + } + + // Get project name (from docker.js) + const projectName = args.includes( '--type=e2e' ) ? 'jetpack_e2e' : 'jetpack_dev'; + + // Load versions from .github/versions.sh + const versionsPath = resolve( monorepoRoot, '.github/versions.sh' ); + const versions = fs.readFileSync( versionsPath, 'utf8' ); + const versionVars = {}; + versions.split( '\n' ).forEach( line => { + const match = line.match( /^([A-Z_]+)=(.+)$/ ); + if ( match ) { + versionVars[ match[ 1 ] ] = match[ 2 ].replace( /['"]/g, '' ); + } + } ); + + // Build environment variables (from docker.js) + const envVars = { + ...process.env, + // Load from default.env + ...( fs.existsSync( resolve( monorepoRoot, 'tools/docker/default.env' ) ) + ? dotenv.parse( fs.readFileSync( resolve( monorepoRoot, 'tools/docker/default.env' ) ) ) + : {} ), + // Load from .env if it exists + ...( fs.existsSync( resolve( monorepoRoot, 'tools/docker/.env' ) ) + ? dotenv.parse( fs.readFileSync( resolve( monorepoRoot, 'tools/docker/.env' ) ) ) + : {} ), + HOST_CWD: monorepoRoot, + PHP_VERSION: versionVars.PHP_VERSION, + COMPOSER_VERSION: versionVars.COMPOSER_VERSION, + NODE_VERSION: versionVars.NODE_VERSION, + PNPM_VERSION: versionVars.PNPM_VERSION, + COMPOSE_PROJECT_NAME: projectName, + PORT_WORDPRESS: args.includes( '--type=e2e' ) ? '8889' : '80', + }; + + // Build the list of compose files to use + const composeFiles = [ + '-f', + resolve( monorepoRoot, 'tools/docker/docker-compose.yml' ), + '-f', + resolve( monorepoRoot, 'tools/docker/compose-mappings.built.yml' ), + '-f', + resolve( monorepoRoot, 'tools/docker/compose-extras.built.yml' ), + ]; + + const result = spawnSync( 'docker', [ 'compose', ...composeFiles, ...args.slice( 1 ) ], { + stdio: 'inherit', + shell: true, + cwd: resolve( monorepoRoot, 'tools/docker' ), + env: envVars, + } ); + + if ( result.status !== 0 ) { + throw new Error( `Docker command failed with status ${ result.status }` ); + } + return; + } + // Run the monorepo script with the original arguments const result = spawnSync( resolve( monorepoRoot, 'tools/docker/bin/monorepo' ), @@ -141,7 +278,6 @@ const main = async () => { throw new Error( `Command failed with status ${ result.status }` ); } } catch ( error ) { - // eslint-disable-next-line no-console console.error( chalk.red( error.message ) ); process.exitCode = 1; } diff --git a/projects/js-packages/jetpack-cli/changelog/.gitkeep b/projects/js-packages/jetpack-cli/changelog/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/projects/js-packages/jetpack-cli/changelog/initial-version b/projects/js-packages/jetpack-cli/changelog/initial-version new file mode 100644 index 0000000000000..fb1837c901e51 --- /dev/null +++ b/projects/js-packages/jetpack-cli/changelog/initial-version @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Initial version. diff --git a/projects/js-packages/jetpack-cli/composer.json b/projects/js-packages/jetpack-cli/composer.json new file mode 100644 index 0000000000000..42127f0f1f562 --- /dev/null +++ b/projects/js-packages/jetpack-cli/composer.json @@ -0,0 +1,37 @@ +{ + "name": "automattic/jetpack-cli", + "description": "Development tools for the Jetpack monorepo", + "type": "library", + "license": "GPL-2.0-or-later", + "require": {}, + "require-dev": { + "automattic/jetpack-changelogger": "@dev" + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "./vendor/phpunit/phpunit/phpunit --colors=always" + ], + "test-coverage": [ + "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\"" + ], + "test-php": [ + "@composer phpunit" + ] + }, + "repositories": [ + { + "type": "path", + "url": "../../packages/*", + "options": { + "monorepo": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/projects/js-packages/jetpack-cli/eslint.config.mjs b/projects/js-packages/jetpack-cli/eslint.config.mjs new file mode 100644 index 0000000000000..5ad3c9460cc3f --- /dev/null +++ b/projects/js-packages/jetpack-cli/eslint.config.mjs @@ -0,0 +1,11 @@ +import makeBaseConfig from 'jetpack-js-tools/eslintrc/base.mjs'; + +export default [ + ...makeBaseConfig( import.meta.url, { envs: [ 'node' ] } ), + { + rules: { + 'no-console': 'off', + 'n/no-process-exit': 'off', + }, + }, +]; diff --git a/projects/js-packages/jetpack-cli/package.json b/projects/js-packages/jetpack-cli/package.json index aa1e4d78e476e..bd1ea239b98cc 100644 --- a/projects/js-packages/jetpack-cli/package.json +++ b/projects/js-packages/jetpack-cli/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-cli", - "version": "0.1.0-beta.1", + "version": "0.1.0-alpha", "description": "Docker-based CLI for Jetpack development", "bin": { "jp": "bin/jp.js" @@ -10,7 +10,8 @@ ], "type": "module", "dependencies": { - "chalk": "^4.1.2", + "chalk": "^5.4.1", + "dotenv": "^16.3.1", "prompts": "^2.4.2" }, "publishConfig": { diff --git a/projects/js-packages/jetpack-cli/src/index.jsx b/projects/js-packages/jetpack-cli/src/index.jsx new file mode 100644 index 0000000000000..9ad1e06860e5c --- /dev/null +++ b/projects/js-packages/jetpack-cli/src/index.jsx @@ -0,0 +1,2 @@ +// Put your code in this `src/` folder! +// Feel free to delete or rename this file. diff --git a/projects/js-packages/jetpack-cli/tests/index.test.js b/projects/js-packages/jetpack-cli/tests/index.test.js new file mode 100644 index 0000000000000..d34c1ab3fc541 --- /dev/null +++ b/projects/js-packages/jetpack-cli/tests/index.test.js @@ -0,0 +1,7 @@ +// We recommend using `jest` for testing. If you're testing React code, we recommend `@testing-library/react` and related packages. +// Please match the versions used elsewhere in the monorepo. +// +// Please don't add new uses of `mocha`, `chai`, `sinon`, `enzyme`, and so on. We're trying to standardize on one testing framework. +// +// The default setup is to have files named like "name.test.js" (or .jsx, .ts, or .tsx) in this `tests/` directory. +// But you could instead put them in `src/`, or put files like "name.js" (or .jsx, .ts, or .tsx) in `test` or `__tests__` directories somewhere. diff --git a/projects/js-packages/jetpack-cli/tests/jest.config.cjs b/projects/js-packages/jetpack-cli/tests/jest.config.cjs new file mode 100644 index 0000000000000..b5ceacda1f7e0 --- /dev/null +++ b/projects/js-packages/jetpack-cli/tests/jest.config.cjs @@ -0,0 +1,7 @@ +const path = require( 'path' ); +const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); + +module.exports = { + ...baseConfig, + rootDir: path.join( __dirname, '..' ), +}; diff --git a/tools/cli/commands/docker.js b/tools/cli/commands/docker.js index 9d0291aee259c..8626a47a37427 100644 --- a/tools/cli/commands/docker.js +++ b/tools/cli/commands/docker.js @@ -554,6 +554,15 @@ const execJtCmdHandler = argv => { } }; +/** + * Generate Docker configuration files. + * + * @param {object} argv - The command line arguments + */ +async function generateConfig( argv ) { + await setConfig( argv ); +} + /** * Definition for the Docker commands. * @@ -561,7 +570,7 @@ const execJtCmdHandler = argv => { * @return {object} Yargs with the Docker commands defined. */ export function dockerDefine( yargs ) { - yargs.command( { + return yargs.command( { command: 'docker ', description: 'Docker stuff', builder: yarg => { @@ -832,9 +841,15 @@ export function dockerDefine( yargs ) { const envOpts = buildEnv( argv ); composeExecutor( argv, opts, envOpts ); }, + } ) + .command( { + command: 'config', + description: 'Generate Docker configuration files', + builder: yargCmd => defaultOpts( yargCmd ), + handler: async argv => { + await generateConfig( argv ); + }, } ); }, } ); - - return yargs; } diff --git a/tools/docker/Dockerfile.monorepo b/tools/docker/Dockerfile.monorepo index 9517dd98e3f53..6214b49eec44c 100644 --- a/tools/docker/Dockerfile.monorepo +++ b/tools/docker/Dockerfile.monorepo @@ -16,6 +16,11 @@ RUN --mount=type=cache,target=/var/lib/apt/lists/,sharing=private \ export DEBIAN_FRONTEND=noninteractive \ && apt-get update \ && apt-get install -y curl gpg language-pack-en-base software-properties-common \ + # Install Docker CLI + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && chmod a+r /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" > /etc/apt/sources.list.d/docker.list \ && add-apt-repository ppa:ondrej/php \ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ && apt-get update \ @@ -23,6 +28,8 @@ RUN --mount=type=cache,target=/var/lib/apt/lists/,sharing=private \ git \ unzip \ zip \ + docker-ce-cli \ + docker-compose-plugin \ "php${PHP_VERSION}" \ "php${PHP_VERSION}-cli" \ "php${PHP_VERSION}-curl" \ diff --git a/tools/docker/bin/monorepo b/tools/docker/bin/monorepo index 0dbd651285523..8a6cc39d39c8d 100755 --- a/tools/docker/bin/monorepo +++ b/tools/docker/bin/monorepo @@ -14,6 +14,9 @@ echo "Running command in monorepo container: $*" # Source the versions file source "$MONOREPO_ROOT/.github/versions.sh" +# Export variables needed by docker-compose +export HOST_CWD="$MONOREPO_ROOT" + # Build the image if it doesn't exist if ! docker image inspect jetpack-monorepo:latest >/dev/null 2>&1; then if [ "${BUILD_LOCAL:-}" = "1" ]; then @@ -37,9 +40,13 @@ fi docker run --rm -it \ -v "$MONOREPO_ROOT:/workspace" \ -v "$MONOREPO_ROOT/tools/docker/data/monorepo:/root" \ + -v /var/run/docker.sock:/var/run/docker.sock \ -w /workspace \ -e TERM=$TERM \ -e COLORTERM=$COLORTERM \ + -e DOCKER_ROOT="$MONOREPO_ROOT/tools/docker" \ + -e HOST_CWD="$MONOREPO_ROOT" \ + -e WORKSPACE_PATH="$MONOREPO_ROOT" \ -e NPM_CONFIG_USERCONFIG=/root/.npmrc \ -e NPM_CONFIG_CACHE=/root/.npm \ -e PNPM_HOME=/root/.local/share/pnpm \ diff --git a/tools/docker/docker-compose.yml b/tools/docker/docker-compose.yml index f0eb1ac222808..9e22a06fe6a17 100644 --- a/tools/docker/docker-compose.yml +++ b/tools/docker/docker-compose.yml @@ -52,7 +52,7 @@ services: monorepo: build: - context: . + context: ${HOST_CWD}/tools/docker dockerfile: Dockerfile.monorepo args: PHP_VERSION: ${PHP_VERSION} @@ -60,5 +60,5 @@ services: NODE_VERSION: ${NODE_VERSION} PNPM_VERSION: ${PNPM_VERSION} volumes: - - ../..:${WORKSPACE_PATH:-/workspace} - working_dir: ${WORKSPACE_PATH:-/workspace} + - ${HOST_CWD}:/usr/local/src/jetpack-monorepo + working_dir: /usr/local/src/jetpack-monorepo