diff --git a/.github/workflows/build-docker-monorepo.yml b/.github/workflows/build-docker-monorepo.yml new file mode 100644 index 0000000000000..8581042c2c957 --- /dev/null +++ b/.github/workflows/build-docker-monorepo.yml @@ -0,0 +1,100 @@ +name: Build Monorepo Docker +on: + push: + branches: [ 'trunk' ] + paths: + - 'tools/docker/Dockerfile.monorepo' + - 'tools/docker/bin/monorepo' + - '.github/versions.sh' + - '.github/workflows/build-docker-monorepo.yml' + pull_request: + paths: + - 'tools/docker/Dockerfile.monorepo' + - 'tools/docker/bin/monorepo' + - '.github/versions.sh' + - '.github/workflows/build-docker-monorepo.yml' +concurrency: + group: build-docker-monorepo-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and publish Jetpack Monorepo Environment + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + timeout-minutes: 60 + + steps: + - uses: actions/checkout@v4 + + - name: Set up qemu + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: matticbot + password: ${{ secrets.DOCKER_HUB_MATTICBOT_TOKEN }} + + - name: Log in to GitHub Packages + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch build args + id: buildargs + run: | + source .github/versions.sh + source .github/files/gh-funcs.sh + + gh_set_output php-version "$PHP_VERSION" + gh_set_output composer-version "$COMPOSER_VERSION" + gh_set_output node-version "$NODE_VERSION" + gh_set_output pnpm-version "$PNPM_VERSION" + + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + gh_set_output tags "type=raw,latest" + gh_set_output images $'automattic/jetpack-monorepo\nghcr.io/automattic/jetpack-monorepo' + elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + gh_set_output tags "type=ref,event=pr" + gh_set_output images "ghcr.io/automattic/jetpack-monorepo" + else + echo "Unknown GITHUB_EVENT_NAME $GITHUB_EVENT_NAME" + exit 1 + fi + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + flavor: latest=false + tags: ${{ steps.buildargs.outputs.tags }} + images: ${{ steps.buildargs.outputs.images }} + labels: | + org.opencontainers.image.title=Jetpack Monorepo Environment + org.opencontainers.image.description=Environment for building and testing the Jetpack Monorepo. + org.opencontainers.image.documentation=${{ github.server_url }}/${{ github.repository }}/blob/trunk/tools/docker/README.md + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: tools/docker + file: tools/docker/Dockerfile.monorepo + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64,linux/amd64 + build-args: | + PHP_VERSION=${{ steps.buildargs.outputs.php-version }} + COMPOSER_VERSION=${{ steps.buildargs.outputs.composer-version }} + NODE_VERSION=${{ steps.buildargs.outputs.node-version }} + PNPM_VERSION=${{ steps.buildargs.outputs.pnpm-version }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 98e01159a7ad4..3e1b9d303f588 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -211,6 +211,7 @@ jobs: CONFIG_KEY: ${{ secrets.E2E_CONFIG_KEY }} SUITE: ${{ matrix.suite }} PROJECT_NAME: ${{ matrix.project }} + HOST_CWD: ${{ github.workspace }} run: | echo "::group::Decrypt config" pnpm run config:decrypt diff --git a/.gitignore b/.gitignore index 99d7932db767e..fd95946530599 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ phpcs.xml # VS Code setting files *.code-workspace /.vscode/settings.json + +.pnpm-debug.log +.pnpm-error.log diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6efa0c329a846..61003850e0eef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - 'projects/*/*' + - '!projects/js-packages/jetpack-cli' - 'projects/plugins/*/tests/e2e' - 'tools/cli' - 'tools/e2e-commons' 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..27aef833f6a2a --- /dev/null +++ b/projects/js-packages/jetpack-cli/.gitignore @@ -0,0 +1,3 @@ +vendor/ +node_modules/ +package-lock.json 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 new file mode 100755 index 0000000000000..ae2ec21d48e6f --- /dev/null +++ b/projects/js-packages/jetpack-cli/bin/jp.js @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +import { spawnSync } from 'child_process'; +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'; +import updateNotifier from 'update-notifier'; + +// 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 for updates +const notifier = updateNotifier( { + pkg: packageJson, + updateCheckInterval: 1000 * 60 * 60 * 24, // Check once per day +} ); + +// Show update notification +notifier.notify( { + message: + 'Update available for Jetpack CLI: {currentVersion} → {latestVersion}\n' + + 'Run {updateCommand} to update', + isGlobal: true, +} ); + +/** + * Check if a directory is the monorepo root. + * + * @param {string} dir - Directory to check + * @return {boolean} True if this is the monorepo root + */ +const isMonorepoRoot = dir => { + try { + return fs.existsSync( resolve( dir, 'tools/docker/bin/monorepo' ) ); + } catch { + return false; + } +}; + +/** + * Find monorepo root from a starting directory. + * + * @param {string} startDir - Directory to start searching from + * @return {string|null} Path to monorepo root, or null if not found + */ +const findMonorepoRoot = startDir => { + let dir = startDir; + let prevDir; + while ( dir !== prevDir ) { + // Keep going until dirname() stops changing the path + if ( isMonorepoRoot( dir ) ) { + return dir; + } + prevDir = dir; + dir = dirname( dir ); + } + return null; +}; + +/** + * Clone the monorepo. + * + * @param {string} targetDir - Directory to clone into + * @throws {Error} If clone fails + */ +const cloneMonorepo = async targetDir => { + console.log( chalk.blue( 'Cloning Jetpack monorepo...' ) ); + const result = spawnSync( + 'git', + [ 'clone', 'https://github.com/Automattic/jetpack.git', targetDir ], + { stdio: 'inherit' } + ); + + if ( result.status !== 0 ) { + throw new Error( 'Failed to clone repository' ); + } +}; + +/** + * Initialize a new Jetpack development environment. + * + * @throws {Error} If initialization fails + */ +const initJetpack = async () => { + const response = await prompts( { + type: 'text', + name: 'directory', + message: 'Where would you like to clone the Jetpack monorepo?', + initial: './jetpack', + } ); + + if ( ! response.directory ) { + throw new Error( 'Setup cancelled' ); + } + + const targetDir = resolve( process.cwd(), response.directory ); + + if ( fs.existsSync( targetDir ) ) { + throw new Error( `Directory ${ targetDir } already exists` ); + } + + try { + await cloneMonorepo( targetDir ); + + console.log( chalk.green( '\nJetpack monorepo has been cloned successfully!' ) ); + + console.log( '\nNext steps:' ); + + console.log( '1. cd', response.directory ); + + console.log( '2. jp docker up' ); + + console.log( '3. jp docker install' ); + } catch ( error ) { + throw new Error( `Failed to initialize Jetpack: ${ error.message }` ); + } +}; + +// Main execution +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(); + return; + } + + // Try to find monorepo root from current directory + const monorepoRoot = findMonorepoRoot( process.cwd() ); + + if ( ! monorepoRoot ) { + console.error( chalk.red( 'Could not find Jetpack monorepo.' ) ); + + console.log( '\nTo get started:' ); + + console.log( '1. Run', chalk.blue( 'jp init' ), 'to clone the repository' ); + + console.log( ' OR' ); + + console.log( '2. Navigate to an existing Jetpack monorepo directory' ); + throw new Error( 'Monorepo not found' ); + } + + // Handle docker commands that must run on the host machine + if ( args[ 0 ] === 'docker' ) { + const hostCommands = [ 'up', 'down', 'stop', 'clean' ]; + if ( hostCommands.includes( args[ 1 ] ) ) { + // Handle command-specific setup/cleanup + 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 } ); + + // Create empty .env file + fs.writeFileSync( resolve( monorepoRoot, 'tools/docker/.env' ), '' ); + + 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' ); + } + } else if ( args[ 1 ] === 'clean' ) { + // After docker-compose down -v, also remove local files + const projectName = args.includes( '--type=e2e' ) ? 'jetpack_e2e' : 'jetpack_dev'; + const cleanupPaths = [ + resolve( monorepoRoot, 'tools/docker/wordpress/' ), + resolve( monorepoRoot, 'tools/docker/wordpress-develop/*' ), + resolve( monorepoRoot, 'tools/docker/logs/', projectName ), + resolve( monorepoRoot, 'tools/docker/data/', `${ projectName }_mysql` ), + ]; + + // Function to clean up after docker-compose down + const cleanupFiles = () => { + for ( const path of cleanupPaths ) { + try { + fs.rmSync( path, { recursive: true, force: true } ); + } catch ( error ) { + console.warn( + chalk.yellow( `Warning: Could not remove ${ path }: ${ error.message }` ) + ); + } + } + }; + + // Add cleanup to process events to ensure it runs after docker-compose + process.once( 'beforeExit', cleanupFiles ); + + // Replace 'clean' with 'down -v' in the arguments + args.splice( 1, 1, 'down', '-v' ); + } + + // Get project name (from docker.js) + const projectName = args.includes( '--type=e2e' ) ? 'jetpack_e2e' : 'jetpack_dev'; + + // Build environment variables (from docker.js) + const envVars = { + ...process.env, // Start with process.env + }; + + // Add default env vars if they exist + if ( fs.existsSync( resolve( monorepoRoot, 'tools/docker/default.env' ) ) ) { + Object.assign( + envVars, + dotenv.parse( fs.readFileSync( resolve( monorepoRoot, 'tools/docker/default.env' ) ) ) + ); + } + + // Add user overrides from .env if they exist + if ( fs.existsSync( resolve( monorepoRoot, 'tools/docker/.env' ) ) ) { + Object.assign( + envVars, + dotenv.parse( fs.readFileSync( resolve( monorepoRoot, 'tools/docker/.env' ) ) ) + ); + } + + // Only set these specific vars if they're not already set in .env + if ( ! envVars.COMPOSE_PROJECT_NAME ) { + envVars.COMPOSE_PROJECT_NAME = projectName; + } + if ( ! envVars.PORT_WORDPRESS ) { + envVars.PORT_WORDPRESS = args.includes( '--type=e2e' ) ? '8889' : '80'; + } + + // Load versions from .github/versions.sh if not already set + if ( + ! ( + envVars.PHP_VERSION && + envVars.COMPOSER_VERSION && + envVars.NODE_VERSION && + envVars.PNPM_VERSION + ) + ) { + 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, '' ); + } + } ); + + // Only set version vars if they're not already set + if ( ! envVars.PHP_VERSION ) envVars.PHP_VERSION = versionVars.PHP_VERSION; + if ( ! envVars.COMPOSER_VERSION ) envVars.COMPOSER_VERSION = versionVars.COMPOSER_VERSION; + if ( ! envVars.NODE_VERSION ) envVars.NODE_VERSION = versionVars.NODE_VERSION; + if ( ! envVars.PNPM_VERSION ) envVars.PNPM_VERSION = versionVars.PNPM_VERSION; + } + + // Always set HOST_CWD as it's required for Docker context + envVars.HOST_CWD = monorepoRoot; + + // Build the list of compose files to use + const composeFiles = + args[ 0 ] === 'docker' && [ 'build-image', 'install' ].includes( args[ 1 ] ) + ? [ '-f', resolve( monorepoRoot, 'tools/docker/docker-compose-monorepo.yml' ) ] + : [ + '-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' ), + ]; + + // Add dev profile for monorepo service + const composeArgs = [ 'compose', '--profile', 'dev', ...composeFiles, ...args.slice( 1 ) ]; + + const result = spawnSync( 'docker', composeArgs, { + 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' ), + [ 'pnpm', 'jetpack', ...args ], + { + stdio: 'inherit', + shell: true, + cwd: monorepoRoot, // Ensure we're in the monorepo root when running commands + } + ); + + if ( result.status !== 0 ) { + throw new Error( `Command failed with status ${ result.status }` ); + } + } catch ( error ) { + console.error( chalk.red( error.message ) ); + process.exitCode = 1; + } +}; + +main(); 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..734918225c692 --- /dev/null +++ b/projects/js-packages/jetpack-cli/composer.json @@ -0,0 +1,31 @@ +{ + "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": { + "test-js": [ + "echo 'no tests yet'" + ] + }, + "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 new file mode 100644 index 0000000000000..c5b9dd32df106 --- /dev/null +++ b/projects/js-packages/jetpack-cli/package.json @@ -0,0 +1,21 @@ +{ + "name": "@automattic/jetpack-cli", + "version": "0.1.0-alpha", + "description": "Docker-based CLI for Jetpack development", + "bin": { + "jp": "bin/jp.js" + }, + "files": [ + "bin" + ], + "type": "module", + "dependencies": { + "chalk": "^5.4.1", + "dotenv": "^16.3.1", + "prompts": "^2.4.2", + "update-notifier": "^7.0.0" + }, + "publishConfig": { + "access": "public" + } +} 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/cli.js b/tools/cli/commands/cli.js index 5810da97138f9..692079b2427c6 100644 --- a/tools/cli/commands/cli.js +++ b/tools/cli/commands/cli.js @@ -132,6 +132,12 @@ export function cliDefine( yargs ) { 'Symlink the CLI for global use or development.', () => {}, argv => { + if ( process.env.JETPACK_MONOREPO_ENV ) { + console.log( + chalk.yellow( 'CLI linking is not needed within the monorepo container.' ) + ); + return; + } cliLink( argv ); if ( argv.v ) { console.log( argv ); @@ -143,6 +149,12 @@ export function cliDefine( yargs ) { 'Unlink the CLI.', () => {}, argv => { + if ( process.env.JETPACK_MONOREPO_ENV ) { + console.log( + chalk.yellow( 'CLI unlinking is not needed within the monorepo container.' ) + ); + return; + } cliUnlink( argv ); if ( argv.v ) { console.log( argv ); diff --git a/tools/cli/commands/docker.js b/tools/cli/commands/docker.js index 938f3e8d25a19..0d46d986746a7 100644 --- a/tools/cli/commands/docker.js +++ b/tools/cli/commands/docker.js @@ -63,6 +63,13 @@ const buildEnv = argv => { } envOpts.COMPOSE_PROJECT_NAME = getProjectName( argv ); + + // Add versions from versions.sh + const versions = envfile.parse( + fs.readFileSync( `${ dockerFolder }/../../.github/versions.sh`, 'utf8' ) + ); + Object.assign( envOpts, versions ); + return envOpts; }; @@ -547,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. * @@ -554,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 => { @@ -801,9 +817,15 @@ export function dockerDefine( yargs ) { command: 'jt-config', description: 'Set jurassic tube config', handler: argv => execJtCmdHandler( argv ), + } ) + .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 new file mode 100644 index 0000000000000..7d9bc62343575 --- /dev/null +++ b/tools/docker/Dockerfile.monorepo @@ -0,0 +1,91 @@ +FROM ubuntu:24.04 + +# Import version variables from .github/versions.sh +ARG PHP_VERSION +ARG COMPOSER_VERSION +ARG NODE_VERSION +ARG PNPM_VERSION + +ENV LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + JETPACK_MONOREPO_ENV=1 + +WORKDIR /app + +# Install basic packages and PHP +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 ca-certificates \ + && 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 \ + # Add Docker's official GPG key + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \ + && chmod a+r /etc/apt/keyrings/docker.asc \ + # Add Docker repository + && echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt-get update \ + && apt-get --purge install -y \ + git \ + unzip \ + zip \ + docker-ce-cli \ + docker-buildx-plugin \ + docker-compose-plugin \ + jq \ + "php${PHP_VERSION}" \ + "php${PHP_VERSION}-cli" \ + "php${PHP_VERSION}-curl" \ + "php${PHP_VERSION}-dom" \ + "php${PHP_VERSION}-mbstring" \ + "php${PHP_VERSION}-xml" \ + "php${PHP_VERSION}-zip" \ + rsync \ + && apt-get remove --purge --auto-remove -y gpg software-properties-common \ + && find /var/ -name '*-old' -delete && rm -rf /var/log/dpkg.log /var/log/alternatives.log /var/log/apt/ ~/.launchpadlib + +# Install Composer +RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ + && php composer-setup.php --install-dir=/usr/local/bin --filename=composer --version=$COMPOSER_VERSION \ + && php -r "unlink('composer-setup.php');" + +# Install Node.js +RUN --mount=type=cache,target=/var/lib/apt/lists/,sharing=private \ + export DEBIAN_FRONTEND=noninteractive \ + && N=${NODE_VERSION%%.*} \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$N.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && apt-get -q update \ + && VER="$(apt-cache show nodejs | sed -n "/^Version: ${NODE_VERSION}-/ { s/^Version: /=/p; q }" )" \ + && apt-get install -y nodejs$VER + +# Install pnpm +RUN npm install --global pnpm@$PNPM_VERSION \ + && SHELL=/bin/bash pnpm setup + +WORKDIR /workspace + +# Add entrypoint script +COPY bin/monorepo-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/monorepo-entrypoint.sh + +# Set up locale properly +RUN apt-get update && apt-get install -y locales \ + && locale-gen en_US.UTF-8 \ + && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 + +# Set up PNPM global directory +ENV PNPM_HOME=/usr/local/pnpm +ENV PATH="${PNPM_HOME}:${PATH}" + +RUN mkdir -p "$PNPM_HOME" \ + && chmod 777 "$PNPM_HOME" + +ENTRYPOINT ["/usr/local/bin/monorepo-entrypoint.sh"] +CMD ["bash"] diff --git a/tools/docker/bin/monorepo b/tools/docker/bin/monorepo new file mode 100755 index 0000000000000..8a6cc39d39c8d --- /dev/null +++ b/tools/docker/bin/monorepo @@ -0,0 +1,55 @@ +#!/bin/bash + +# Enable debug mode if DEBUG environment variable is set +if [ "${DEBUG:-}" = "1" ]; then + set -x +fi + +# Get the absolute path to the monorepo root +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +MONOREPO_ROOT="$( cd "$SCRIPT_DIR/../../.." && pwd )" + +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 + echo "Building monorepo image locally..." + docker build \ + --build-arg PHP_VERSION="$PHP_VERSION" \ + --build-arg COMPOSER_VERSION="$COMPOSER_VERSION" \ + --build-arg NODE_VERSION="$NODE_VERSION" \ + --build-arg PNPM_VERSION="$PNPM_VERSION" \ + -t jetpack-monorepo:latest \ + -f "$MONOREPO_ROOT/tools/docker/Dockerfile.monorepo" \ + "$MONOREPO_ROOT/tools/docker" + else + echo "Pulling monorepo image..." + docker pull automattic/jetpack-monorepo:latest + docker tag automattic/jetpack-monorepo:latest jetpack-monorepo:latest + fi +fi + +# Run the command in the container +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 \ + -e PNPM_STORE_DIR=/root/.pnpm-store \ + jetpack-monorepo:latest \ + "$@" diff --git a/tools/docker/bin/monorepo-entrypoint.sh b/tools/docker/bin/monorepo-entrypoint.sh new file mode 100644 index 0000000000000..262b10b292298 --- /dev/null +++ b/tools/docker/bin/monorepo-entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Exit on error +set -e + +# Check and set PNPM store location +EXPECTED_STORE="/workspace/tools/docker/data/pnpm-store" +CURRENT_STORE=$(pnpm config get store-dir) + +if [ "$CURRENT_STORE" != "$EXPECTED_STORE" ]; then + echo "Setting PNPM store directory to $EXPECTED_STORE" + mkdir -p "$EXPECTED_STORE" + pnpm config set store-dir "$EXPECTED_STORE" +fi + +# Check if jetpack command is available +if ! pnpm jetpack --help &>/dev/null; then + echo "Setting up Jetpack CLI..." + pnpm install +fi + +# Execute the passed command +exec "$@"