diff --git a/.editorconfig b/.editorconfig index 27c8ac4..7b7dab7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,6 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[*.{js,css,scss,yml,json,.babelrc}] +[*.{js,css,scss,yml,json,.babelrc,.xml}] indent_style = space indent_size = 2 diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml deleted file mode 100644 index 78fc770..0000000 --- a/.github/workflows/coding-standards.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Coding Standards - -on: - push: - branches: - - develop - pull_request: - schedule: - - cron: '0 0 * * *' - -jobs: - coding-standards: - uses: alleyinteractive/.github/.github/workflows/php-coding-standards.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..136d32b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,64 @@ +name: Testing Suite + +on: + push: + branches: + - develop + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + pr-tests: + # Don't run on draft PRs + if: github.event.pull_request.draft == false + # Timeout after 10 minutes + timeout-minutes: 10 + # Define a matrix of PHP/WordPress versions to test against + strategy: + fail-fast: false + matrix: + php: [8.2, 8.3] + wordpress: ["latest"] + runs-on: ubuntu-latest + # Cancel any existing runs of this workflow + concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}-P${{ matrix.php }}-WP${{ matrix.wordpress }} + cancel-in-progress: true + # Name the job in the matrix + name: "PR Tests PHP ${{ matrix.php }} WordPress ${{ matrix.wordpress }}" + steps: + - uses: actions/checkout@v4 + + - name: Run General Tests + # See https://github.com/alleyinteractive/action-test-general for more options + uses: alleyinteractive/action-test-general@develop + + - name: Run PHP Tests + # See https://github.com/alleyinteractive/action-test-php for more options + uses: alleyinteractive/action-test-php@develop + with: + php-version: '${{ matrix.php }}' + wordpress-version: '${{ matrix.wordpress }}' + skip-wordpress-install: 'true' + + # This required job ensures that all PR checks have passed before merging. + all-pr-checks-passed: + name: All PR checks passed + needs: + - pr-tests + runs-on: ubuntu-latest + if: always() + steps: + - name: Check job statuses + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + echo "One or more jobs failed" + exit 1 + elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more jobs were cancelled" + exit 1 + else + echo "All jobs passed or were skipped" + exit 0 + fi diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml deleted file mode 100644 index 882166a..0000000 --- a/.github/workflows/unit-test.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Testing Suite - -on: - push: - branches: - - develop - pull_request: - schedule: - - cron: '0 0 * * *' - -jobs: - php-tests: - strategy: - matrix: - php: [8.0] - wordpress: ["latest"] - uses: alleyinteractive/.github/.github/workflows/php-tests.yml@main - with: - php: ${{ matrix.php }} - wordpress: ${{ matrix.wordpress }} diff --git a/.github/workflows/upload-phar.yml b/.github/workflows/upload-phar.yml new file mode 100644 index 0000000..3c19b33 --- /dev/null +++ b/.github/workflows/upload-phar.yml @@ -0,0 +1,33 @@ +name: Upload Phar to Release + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer, box + + - name: Install Composer dependencies + run: composer install + + - name: Build Phar + run: composer build + + # TODO: Add tests here + + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + build/mantle-installer.phar diff --git a/.gitignore b/.gitignore index 4dd6deb..a324d64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -/vendor/ +.DS_Store +.phpunit.result.cache .vscode/ +/vendor/ +build composer.lock -.phpunit.result.cache -.DS_Store diff --git a/README.md b/README.md index b342641..f2cb4c6 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,4 @@ Documentation can be found inside the [Mantle documentation](https://mantle.alley.co/). -![Testing Suite](https://github.com/alleyinteractive/mantle-installer/workflows/Testing%20Suite/badge.svg) -![Coding Standards](https://github.com/alleyinteractive/mantle-installer/workflows/Coding%20Standards/badge.svg) +[![Testing Suite](https://github.com/alleyinteractive/mantle-installer/actions/workflows/tests.yml/badge.svg)](https://github.com/alleyinteractive/mantle-installer/actions/workflows/tests.yml) diff --git a/bin/mantle b/bin/mantle index 57a1c91..b8a83f6 100755 --- a/bin/mantle +++ b/bin/mantle @@ -7,7 +7,7 @@ if ( file_exists( __DIR__ . '/../../../autoload.php' ) ) { require __DIR__ . '/../vendor/autoload.php'; } -$app = new Symfony\Component\Console\Application( 'Mantle Installer', '1.0.5' ); -$app->add( new Mantle\Installer\Console\Install_Command() ); +$app = new Symfony\Component\Console\Application( 'Mantle Installer', '1.1.0' ); +$app->add( new \Mantle\Installer\InstallCommand() ); $app->run(); diff --git a/bin/wp-cli.phar b/bin/wp-cli.phar deleted file mode 100755 index 514ba41..0000000 Binary files a/bin/wp-cli.phar and /dev/null differ diff --git a/box.json b/box.json new file mode 100644 index 0000000..091ead2 --- /dev/null +++ b/box.json @@ -0,0 +1,12 @@ +{ + "alias": "mantle-installer.phar", + "check-requirements": false, + "chmod": "0700", + "exclude-composer-files": false, + "directories": [ + "bin", + "src", + "vendor" + ], + "output": "build/mantle-installer.phar" +} diff --git a/composer.json b/composer.json index e9e1f2d..8296c46 100644 --- a/composer.json +++ b/composer.json @@ -9,31 +9,38 @@ } ], "require": { - "php": "^8.0", - "symfony/console": "^5.0", - "symfony/process": "^5.0" + "php": "^8.2", + "symfony/console": "^7.0", + "symfony/process": "^7.0" }, "require-dev": { - "alleyinteractive/alley-coding-standards": "^1.0", - "phpunit/phpunit": "^9.3" + "alleyinteractive/alley-coding-standards": "^2.0", + "phpunit/phpunit": "^10.0", + "symfony/var-dumper": "^7.0" }, - "config": { - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Mantle\\Installer\\": "src/" } }, - "autoload": { - "files": [ - "src/class-install-command.php" - ] + "autoload-dev": { + "psr-4": { + "Mantle\\Installer\\Tests\\": "tests/" + } }, - "minimum-stability": "dev", - "prefer-stable": true, "bin": [ "bin/mantle" ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + }, "scripts": { + "build": "box compile", "phpcs": "phpcs --standard=./phpcs.xml .", "phpunit": "phpunit", "test": [ diff --git a/phpcs.xml b/phpcs.xml index b8860d5..2f7d805 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -8,5 +8,7 @@ + + diff --git a/phpunit.xml b/phpunit.xml index aa6af59..b7d93f9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,18 @@ + - - - tests/test-install-command.php - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" + bootstrap="vendor/autoload.php" + backupGlobals="false" + colors="true" + failOnDeprecation="true" + failOnNotice="true" + failOnWarning="true" + failOnPhpunitDeprecation="true" +> + + + tests/InstallCommandTest.php + + diff --git a/src/class-install-command.php b/src/InstallCommand.php similarity index 80% rename from src/class-install-command.php rename to src/InstallCommand.php index b8bb07e..5ac9fbb 100644 --- a/src/class-install-command.php +++ b/src/InstallCommand.php @@ -1,11 +1,11 @@ addOption( 'install', 'i', InputOption::VALUE_NONE, 'Install WordPress in the current location if it doesn\'t exist.' ) ->addOption( 'no-must-use', 'no-mu', InputOption::VALUE_OPTIONAL, 'Don\'t load Mantle as a must-use plugin.', false ) ->addOption( 'dev', 'd', InputOption::VALUE_NONE, 'Setup mantle for development on the framework.' ) - ->addOption( 'mantle-version', null, InputOption::VALUE_OPTIONAL, 'Version of alleyinteractive/mantle to install.', 'latest' ); + ->addOption( 'mantle-version', null, InputOption::VALUE_OPTIONAL, 'Version of alleyinteractive/mantle to install.' ); } /** @@ -41,6 +48,8 @@ protected function configure() { * @return int */ protected function execute( InputInterface $input, OutputInterface $output ): int { + $this->style = new SymfonyStyle( $input, $output ); + $output->write( PHP_EOL . " @@ -54,7 +63,7 @@ protected function execute( InputInterface $input, OutputInterface $output ): in if ( $this->check_if_hiring() ) { $output->write( - "Alley is hiring! Apply today at https://alley.co/careers/. \n\n" + "Alley is hiring! Apply today at https://alley.com/careers/. \n\n" ); } @@ -65,9 +74,7 @@ protected function execute( InputInterface $input, OutputInterface $output ): in return static::FAILURE; } - $this->install_mantle( $wordpress_root, $input, $output ); - - return static::SUCCESS; + return $this->install_mantle( $wordpress_root, $input, $output ); } /** @@ -94,7 +101,15 @@ protected function get_wordpress_root( InputInterface $input, OutputInterface $o } } - // Check if we are inside of the default Homestead WordPress environement. + // If the current directory is 'wp-content', use the parent directory. + if ( 'wp-content' === basename( $abspath ) && is_dir( dirname( $abspath ) . '/wp-includes' ) ) { + $abspath = dirname( $abspath ); + + $output->writeln( "Using [{$abspath}] as the WordPress installation." ); + return $abspath; + } + + // Check if we are inside of the default Homestead WordPress environment. if ( is_dir( $abspath . '/wp-content/' ) && is_dir( $abspath . '/wp/' ) && @@ -104,24 +119,22 @@ protected function get_wordpress_root( InputInterface $input, OutputInterface $o return $abspath; } - $style = new SymfonyStyle( $input, $output ); - // Bail if the folder already exists. if ( $name && is_dir( $name ) && ! $input->getOption( 'force' ) ) { - $style->error( "Directory already exists: [{$abspath}] Use --force to override." ); + $this->style->error( "Directory already exists: [{$abspath}] Use --force to override." ); return null; } // Ask the user if we should be installing if not already specified. if ( $input->getOption( 'install' ) - || $style->confirm( "Would you like to install WordPress at [{$abspath}]", true ) + || $this->style->confirm( "Would you like to install WordPress at [{$abspath}]", true ) ) { $this->install_wordpress( $abspath, $input, $output ); return $abspath; } - return $style->ask( + return $this->style->ask( 'Please specify your WordPress installation:', null, /** @@ -157,7 +170,14 @@ function ( $dir ) { protected function install_wordpress( string $dir, InputInterface $input, OutputInterface $output ): bool { $output->writeln( "Installing WordPress at {$dir}...\n\n" ); - $process = $this->run_commands( [ $this->find_wp_cli() . ' core download --force --path=' . $dir ], $input, $output ); + $commands = [ + 'mkdir /tmp/mantle-installer || true', + 'curl --clobber -o /tmp/mantle-installer/wordpress-latest.tar.gz https://wordpress.org/latest.tar.gz', + "mkdir -p {$dir} || true", + "tar --strip-components=1 -zxmf /tmp/mantle-installer/wordpress-latest.tar.gz -C {$dir}", + ]; + + $process = $this->run_commands( $commands, $input, $output ); if ( ! $process->isSuccessful() ) { throw new RuntimeException( 'Error downloading WordPress: ' . $process->getExitCodeText() ); @@ -166,38 +186,6 @@ protected function install_wordpress( string $dir, InputInterface $input, Output return true; } - /** - * Find wp-cli. - * - * @return string - */ - protected function find_wp_cli(): string { - // Check if the wp-cli path was set in an environment variable. - if ( $wp_cli = getenv( 'WP_CLI_PATH' ) ) { - return $wp_cli; - } - - $path = getcwd() . '/wp-cli.phar'; - - if ( file_exists( $path ) ) { - return '"' . PHP_BINARY . '" ' . $path; - } - - // Check if wp-cli is installed globally. - $path = exec( 'which wp' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec - - if ( $path ) { - return $path; - } - - // Fallback to the one installed with the package. - if ( file_exists( __DIR__ . '/../bin/wp-cli.phar' ) ) { - return '"' . PHP_BINARY . '" ' . __DIR__ . '/../bin/wp-cli.phar'; - } - - return 'wp'; - } - /** * Get the composer command for the environment. * @@ -253,21 +241,36 @@ function ( $type, $line ) use ( $output ) { * * @throws RuntimeException Thrown on error. */ - protected function install_mantle( string $dir, InputInterface $input, OutputInterface $output ) { + protected function install_mantle( string $dir, InputInterface $input, OutputInterface $output ): int { $wp_content = $dir . '/wp-content'; + $name = $input->getArgument( 'name' )[0] ?? null; + + if ( ! $name ) { + $name = 'mantle'; + + // Confirm the argument 'name' if not passed before assuming a default. + if ( ! $this->style->confirm( "No named was passed to the installer. Do you want to install Mantle at {$wp_content}/plugins/{$name}", true ) ) { + $this->style->error( 'Mantle installation aborted. To specific a name, pass it as an argument: `mantle new my-plugin`.' ); + + return self::FAILURE; + } + } - $name = $input->getArgument( 'name' )[0] ?? 'mantle'; $mantle_dir = "{$wp_content}/plugins/{$name}"; $framework_dir = "{$wp_content}/plugins/{$name}-framework"; // Check if Mantle exists at the current location. if ( is_dir( $mantle_dir ) && file_exists( $mantle_dir . '/composer.json' ) ) { - throw new RuntimeException( "Mantle is already installed: [{$mantle_dir}]" ); + $this->style->error( "Mantle is already installed: [{$mantle_dir}]" ); + + return self::FAILURE; } // Check if the directory is empty. if ( is_dir( $mantle_dir ) && count( scandir( $mantle_dir ) ) > 2 ) { - throw new RuntimeException( "Directory is not empty: [{$mantle_dir}]" ); + $this->style->error( "Directory is not empty: [{$mantle_dir}]" ); + + return self::FAILURE; } $version = $input->getOption( 'mantle-version' ) ? ':' . $input->getOption( 'mantle-version' ) : ''; @@ -280,7 +283,9 @@ protected function install_mantle( string $dir, InputInterface $input, OutputInt // Setup the application for local development on the framework. if ( $input->getOption( 'dev' ) ) { if ( is_dir( $framework_dir ) && file_exists( "{$framework_dir}/composer.json" ) ) { - throw new RuntimeException( "Mantle Framework is already installed: [{$framework_dir}'" ); + $this->style->error( "Mantle Framework is already installed: [{$framework_dir}'" ); + + return self::FAILURE; } $commands = [ @@ -301,7 +306,9 @@ protected function install_mantle( string $dir, InputInterface $input, OutputInt $process = $this->run_commands( $commands, $input, $output ); if ( ! $process->isSuccessful() ) { - throw new RuntimeException( 'Error installing Mantle: ' . $process->getExitCodeText() ); + $this->style->error( 'Error installing Mantle: ' . $process->getExitCodeText() ); + + return self::FAILURE; } $output->writeln( "Mantle installed successfully at {$mantle_dir}." ); @@ -324,15 +331,21 @@ protected function install_mantle( string $dir, InputInterface $input, OutputInt $mu_plugin = "{$mu_plugins}/{$name}-loader.php"; if ( file_exists( $mu_plugin ) ) { - throw new RuntimeException( "Mantle MU Plugin loader already exists: [{$mu_plugin}]" ); + $this->style->error( "Mantle MU Plugin loader already exists: [{$mu_plugin}]" ); + + return self::FAILURE; } if ( false === file_put_contents( $mu_plugin, trim( $this->get_mu_plugin_loader( $name ) ) ) ) { // phpcs:ignore - throw new RuntimeException( "Error writing must-use plugin loader: [{$mu_plugin}]" ); + $this->style->error( "Error writing must-use plugin loader: [{$mu_plugin}]" ); + + return self::FAILURE; } $output->writeln( "Must-use plugin created: {$mu_plugin}" ); } + + return self::SUCCESS; } /** diff --git a/tests/test-install-command.php b/tests/InstallCommandTest.php similarity index 88% rename from tests/test-install-command.php rename to tests/InstallCommandTest.php index 9fa7c7d..6e439be 100644 --- a/tests/test-install-command.php +++ b/tests/InstallCommandTest.php @@ -1,13 +1,13 @@ assertFileExists( "{$output}/new-site/wp-content/mu-plugins/new-site-loader.php" ); } - public function test_install_wordpress_dev() { + public function test_install_wordpress_dev(): void { $output = __DIR__ . '/output'; chdir( $output ); @@ -87,7 +87,7 @@ public function test_install_wordpress_dev() { protected function get_tester(): CommandTester { $app = new Application( 'Mantle Installer' ); - $app->add( new Install_Command() ); + $app->add( new InstallCommand() ); return new CommandTester( $app->find( 'new' ) ); }